Skip to main content

Mountain/ProcessManagement/
CocoonManagement.rs

1//! # Cocoon Management
2//!
3//! This module provides comprehensive lifecycle management for the Cocoon
4//! sidecar process, which serves as the VS Code extension host within the
5//! Mountain editor.
6//!
7//! ## Overview
8//!
9//! Cocoon is a Node.js-based process that provides compatibility with VS Code
10//! extensions. This module handles:
11//!
12//! - **Process Spawning**: Launching Node.js with the Cocoon bootstrap script
13//! - **Environment Configuration**: Setting up environment variables for IPC
14//!   and logging
15//! - **Communication Setup**: Establishing gRPC/Vine connections on port 50052
16//! - **Health Monitoring**: Tracking process state and handling failures
17//! - **Lifecycle Management**: Graceful shutdown and restart capabilities
18//! - **IO Redirection**: Capturing stdout/stderr for logging and debugging
19//!
20//! ## Process Communication
21//!
22//! The Cocoon process communicates via:
23//! - gRPC on port 50052 (configured via MOUNTAIN_GRPC_PORT/COCOON_GRPC_PORT)
24//! - Vine protocol for cross-process messaging
25//! - Standard streams for logging (VSCODE_PIPE_LOGGING)
26//!
27//! ## Dependencies
28//!
29//! - `scripts/cocoon/bootstrap-fork.js`: Bootstrap script for launching Cocoon
30//! - Node.js runtime: Required for executing Cocoon
31//! - Vine gRPC server: Must be running on port 50051 for handshake
32//!
33//! ## Error Handling
34//!
35//! The module provides graceful degradation:
36//! - If the bootstrap script is missing, returns `FileSystemNotFound` error
37//! - If Node.js cannot be spawned, returns `IPCError`
38//! - If gRPC connection fails, returns `IPCError` with context
39//!
40//! # Module Contents
41//!
42//! - [`InitializeCocoon`]: Main entry point for Cocoon initialization
43//! - `LaunchAndManageCocoonSideCar`: Process spawning and lifecycle
44//! management
45//!
46//! ## Example
47//!
48//! ```rust,no_run
49//! use crate::Source::ProcessManagement::CocoonManagement::InitializeCocoon;
50//!
51//! // Initialize Cocoon with application handle and environment
52//! match InitializeCocoon(&app_handle, &environment).await {
53//! 	Ok(()) => println!("Cocoon initialized successfully"),
54//! 	Err(e) => eprintln!("Cocoon initialization failed: {:?}", e),
55//! }
56//! ```
57
58use std::{collections::HashMap, process::Stdio, sync::Arc, time::Duration};
59
60use CommonLibrary::Error::CommonError::CommonError;
61use tauri::{
62	AppHandle,
63	Manager,
64	Wry,
65	path::{BaseDirectory, PathResolver},
66};
67use tokio::{
68	io::{AsyncBufReadExt, BufReader},
69	process::{Child, Command},
70	sync::Mutex,
71	time::sleep,
72};
73
74use super::InitializationData;
75use crate::dev_log;
76use crate::{
77	Environment::MountainEnvironment::MountainEnvironment,
78	IPC::Common::HealthStatus::{HealthIssue, HealthMonitor},
79	Vine,
80};
81
82/// Configuration constants for Cocoon process management
83const COCOON_SIDE_CAR_IDENTIFIER:&str = "cocoon-main";
84const COCOON_GRPC_PORT:u16 = 50052;
85const MOUNTAIN_GRPC_PORT:u16 = 50051;
86const GRPC_CONNECT_RETRY_INTERVAL_MS:u64 = 1000;
87const GRPC_CONNECT_MAX_ATTEMPTS:u32 = 20;
88const BOOTSTRAP_SCRIPT_PATH:&str = "scripts/cocoon/bootstrap-fork.js";
89const HANDSHAKE_TIMEOUT_MS:u64 = 60000;
90const HEALTH_CHECK_INTERVAL_SECONDS:u64 = 5;
91const MAX_RESTART_ATTEMPTS:u32 = 3;
92const RESTART_WINDOW_SECONDS:u64 = 300;
93
94/// Global state for tracking Cocoon process lifecycle
95struct CocoonProcessState {
96	ChildProcess:Option<Child>,
97	IsRunning:bool,
98	StartTime:Option<tokio::time::Instant>,
99	RestartCount:u32,
100	LastRestartTime:Option<tokio::time::Instant>,
101}
102
103impl Default for CocoonProcessState {
104	fn default() -> Self {
105		Self {
106			ChildProcess:None,
107			IsRunning:false,
108			StartTime:None,
109			RestartCount:0,
110			LastRestartTime:None,
111		}
112	}
113}
114
115// Global state for Cocoon process management
116lazy_static::lazy_static! {
117	static ref COCOON_STATE: Arc<Mutex<CocoonProcessState>> =
118		Arc::new(Mutex::new(CocoonProcessState::default()));
119
120	static ref COCOON_HEALTH: Arc<Mutex<HealthMonitor>> =
121		Arc::new(Mutex::new(HealthMonitor::new()));
122}
123
124/// The main entry point for initializing the Cocoon sidecar process manager.
125///
126/// This orchestrates the complete initialization sequence including:
127/// - Validating feature flags and dependencies
128/// - Launching the Cocoon process with proper configuration
129/// - Establishing gRPC communication
130/// - Performing the initialization handshake
131/// - Setting up process health monitoring
132///
133/// # Arguments
134///
135/// * `ApplicationHandle` - Tauri application handle for path resolution
136/// * `Environment` - Mountain environment containing application state and
137///   services
138///
139/// # Returns
140///
141/// * `Ok(())` - Cocoon initialized successfully and ready to accept extension
142///   requests
143/// * `Err(CommonError)` - Initialization failed with detailed error context
144///
145/// # Errors
146///
147/// - `FileSystemNotFound`: Bootstrap script not found
148/// - `IPCError`: Failed to spawn process or establish gRPC connection
149///
150/// # Example
151///
152/// ```rust,no_run
153/// use crate::Source::ProcessManagement::CocoonManagement::InitializeCocoon;
154///
155/// InitializeCocoon(&app_handle, &environment).await?;
156/// ```
157pub async fn InitializeCocoon(
158	ApplicationHandle:&AppHandle,
159	Environment:&Arc<MountainEnvironment>,
160) -> Result<(), CommonError> {
161	dev_log!("cocoon", "[CocoonManagement] Initializing Cocoon sidecar manager...");
162
163	#[cfg(feature = "ExtensionHostCocoon")]
164	{
165		LaunchAndManageCocoonSideCar(ApplicationHandle.clone(), Environment.clone()).await
166	}
167
168	#[cfg(not(feature = "ExtensionHostCocoon"))]
169	{
170		dev_log!("cocoon", "[CocoonManagement] 'ExtensionHostCocoon' feature is disabled. Cocoon will not be launched.");
171		Ok(())
172	}
173}
174
175/// Spawns the Cocoon process, manages its communication channels, and performs
176/// the complete initialization handshake sequence.
177///
178/// This function implements the complete Cocoon lifecycle:
179/// 1. Validates bootstrap script availability
180/// 2. Constructs environment variables for IPC and logging
181/// 3. Spawns Node.js process with proper IO redirection
182/// 4. Captures stdout/stderr for logging
183/// 5. Waits for gRPC server to be ready
184/// 6. Establishes Vine connection
185/// 7. Sends initialization payload and validates response
186///
187/// # Arguments
188///
189/// * `ApplicationHandle` - Tauri application handle for resolving resource
190///   paths
191/// * `Environment` - Mountain environment containing application state
192///
193/// # Returns
194///
195/// * `Ok(())` - Cocoon process spawned, connected, and initialized successfully
196/// * `Err(CommonError)` - Any failure during the initialization sequence
197///
198/// # Errors
199///
200/// - `FileSystemNotFound`: Bootstrap script not found in resources
201/// - `IPCError`: Failed to spawn process, connect gRPC, or complete handshake
202///
203/// # Lifecycle
204///
205/// The process runs as a background task with IO redirection for logging.
206/// Process failures are logged but not automatically restarted (callers should
207/// implement restart strategies based on their requirements).
208async fn LaunchAndManageCocoonSideCar(
209	ApplicationHandle:AppHandle,
210	Environment:Arc<MountainEnvironment>,
211) -> Result<(), CommonError> {
212	let SideCarIdentifier = COCOON_SIDE_CAR_IDENTIFIER.to_string();
213	let path_resolver:PathResolver<Wry> = ApplicationHandle.path().clone();
214
215	// Resolve bootstrap script path.
216	// 1) Try Tauri bundled resources (production builds).
217	// 2) Fallback: resolve relative to the executable (dev builds). Dev layout:
218	//    Target/debug/binary → ../../scripts/cocoon/bootstrap-fork.js
219	let ScriptPath = path_resolver
220		.resolve(BOOTSTRAP_SCRIPT_PATH, BaseDirectory::Resource)
221		.ok()
222		.filter(|P| P.exists())
223		.or_else(|| {
224			std::env::current_exe().ok().and_then(|Exe| {
225				let MountainRoot = Exe.parent()?.parent()?.parent()?;
226				let Candidate = MountainRoot.join(BOOTSTRAP_SCRIPT_PATH);
227				if Candidate.exists() { Some(Candidate) } else { None }
228			})
229		})
230		.ok_or_else(|| {
231			CommonError::FileSystemNotFound(
232				format!(
233					"Cocoon bootstrap script '{}' not found in resources or relative to executable",
234					BOOTSTRAP_SCRIPT_PATH
235				)
236				.into(),
237			)
238		})?;
239
240	dev_log!("cocoon", "[CocoonManagement] Found bootstrap script at: {}", ScriptPath.display());
241	crate::dev_log!("cocoon", "bootstrap script: {}", ScriptPath.display());
242
243	// Build Node.js command with comprehensive environment configuration
244	let mut NodeCommand = Command::new("node");
245
246	let mut EnvironmentVariables = HashMap::new();
247
248	// VS Code protocol environment variables for extension host compatibility
249	EnvironmentVariables.insert("VSCODE_PIPE_LOGGING".to_string(), "true".to_string());
250	EnvironmentVariables.insert("VSCODE_VERBOSE_LOGGING".to_string(), "true".to_string());
251	EnvironmentVariables.insert("VSCODE_PARENT_PID".to_string(), std::process::id().to_string());
252
253	// gRPC port configuration for Vine communication
254	EnvironmentVariables.insert("MOUNTAIN_GRPC_PORT".to_string(), MOUNTAIN_GRPC_PORT.to_string());
255	EnvironmentVariables.insert("COCOON_GRPC_PORT".to_string(), COCOON_GRPC_PORT.to_string());
256
257	// Preserve PATH so `node` resolves. env_clear() was stripping it.
258	if let Ok(Path) = std::env::var("PATH") {
259		EnvironmentVariables.insert("PATH".to_string(), Path);
260	}
261	if let Ok(Home) = std::env::var("HOME") {
262		EnvironmentVariables.insert("HOME".to_string(), Home);
263	}
264
265	NodeCommand
266		.arg(&ScriptPath)
267		.env_clear()
268		.envs(EnvironmentVariables)
269		.stdin(Stdio::piped())
270		.stdout(Stdio::piped())
271		.stderr(Stdio::piped());
272
273	// Spawn the process with error handling
274	let mut ChildProcess = NodeCommand.spawn().map_err(|Error| {
275		CommonError::IPCError {
276			Description:format!("Failed to spawn Cocoon process: {} (is Node.js installed and in PATH?)", Error),
277		}
278	})?;
279
280	let ProcessId = ChildProcess.id().unwrap_or(0);
281	dev_log!("cocoon", "[CocoonManagement] Cocoon process spawned [PID: {}]", ProcessId);
282	crate::dev_log!("cocoon", "spawned PID={}", ProcessId);
283
284	// Capture stdout for trace logging
285	if let Some(stdout) = ChildProcess.stdout.take() {
286		tokio::spawn(async move {
287			let Reader = BufReader::new(stdout);
288			let mut Lines = Reader.lines();
289
290			while let Ok(Some(Line)) = Lines.next_line().await {
291				dev_log!("cocoon", "[Cocoon stdout] {}", Line);
292			}
293		});
294	}
295
296	// Capture stderr for warn-level logging
297	if let Some(stderr) = ChildProcess.stderr.take() {
298		tokio::spawn(async move {
299			let Reader = BufReader::new(stderr);
300			let mut Lines = Reader.lines();
301
302			while let Ok(Some(Line)) = Lines.next_line().await {
303				dev_log!("cocoon", "warn: [Cocoon stderr] {}", Line);
304			}
305		});
306	}
307
308	// Establish Vine connection to Cocoon with retry loop
309	let GRPCAddress = format!("127.0.0.1:{}", COCOON_GRPC_PORT);
310	dev_log!("cocoon", 
311		"[CocoonManagement] Connecting to Cocoon gRPC at {} (up to {} attempts, {}ms interval)...",
312		GRPCAddress, GRPC_CONNECT_MAX_ATTEMPTS, GRPC_CONNECT_RETRY_INTERVAL_MS
313	);
314
315	let mut ConnectAttempt = 0u32;
316	let mut LastError = None;
317
318	loop {
319		ConnectAttempt += 1;
320		crate::dev_log!(
321			"grpc",
322			"connecting to Cocoon at {} (attempt {}/{})",
323			GRPCAddress,
324			ConnectAttempt,
325			GRPC_CONNECT_MAX_ATTEMPTS
326		);
327
328		match Vine::Client::ConnectToSideCar(SideCarIdentifier.clone(), GRPCAddress.clone()).await
329		{
330			Ok(()) => {
331				crate::dev_log!("grpc", "connected to Cocoon on attempt {}", ConnectAttempt);
332				break;
333			},
334			Err(Error) => {
335				crate::dev_log!(
336					"grpc",
337					"attempt {}/{} failed: {}",
338					ConnectAttempt,
339					GRPC_CONNECT_MAX_ATTEMPTS,
340					Error
341				);
342				LastError = Some(Error);
343
344				if ConnectAttempt >= GRPC_CONNECT_MAX_ATTEMPTS {
345					return Err(CommonError::IPCError {
346						Description:format!(
347							"Failed to connect to Cocoon gRPC at {} after {} attempts: {} (is Cocoon running?)",
348							GRPCAddress,
349							GRPC_CONNECT_MAX_ATTEMPTS,
350							LastError.unwrap()
351						),
352					});
353				}
354
355				sleep(Duration::from_millis(GRPC_CONNECT_RETRY_INTERVAL_MS)).await;
356			},
357		}
358	}
359
360	dev_log!("cocoon", "[CocoonManagement] Connected to Cocoon. Sending initialization data...");
361
362	// Brief delay to ensure Cocoon's gRPC service handlers are fully registered
363	// after bindAsync resolves (race condition on fast connections like attempt 1)
364	sleep(Duration::from_millis(200)).await;
365
366	// Construct initialization payload
367	let MainInitializationData = InitializationData::ConstructExtensionHostInitializationData(&Environment)
368		.await
369		.map_err(|Error| {
370			CommonError::IPCError { Description:format!("Failed to construct initialization data: {}", Error) }
371		})?;
372
373	// Send initialization request with timeout
374	let Response = Vine::Client::SendRequest(
375		&SideCarIdentifier,
376		"InitializeExtensionHost".to_string(),
377		MainInitializationData,
378		HANDSHAKE_TIMEOUT_MS,
379	)
380	.await
381	.map_err(|Error| {
382		CommonError::IPCError {
383			Description:format!("Failed to send initialization request to Cocoon: {}", Error),
384		}
385	})?;
386
387	// Validate handshake response
388	match Response.as_str() {
389		Some("initialized") => {
390			dev_log!("cocoon", "[CocoonManagement] Cocoon handshake complete. Extension host is ready.");
391		},
392		Some(other) => {
393			return Err(CommonError::IPCError {
394				Description:format!("Cocoon initialization failed with unexpected response: {}", other),
395			});
396		},
397		None => {
398			return Err(CommonError::IPCError {
399				Description:"Cocoon initialization failed: no response received".to_string(),
400			});
401		},
402	}
403
404	// Trigger startup extension activation. Cocoon is fully reactive —
405	// it won't activate any extensions until Mountain tells it to.
406	// Fire-and-forget: don't block on activation, and don't fail init if it errors.
407	let SideCarId = SideCarIdentifier.clone();
408	tokio::spawn(async move {
409		// Small delay to let Cocoon finish processing the init response
410		sleep(Duration::from_millis(500)).await;
411
412		crate::dev_log!("exthost", "Sending $activateByEvent(\"*\") to Cocoon");
413
414		if let Err(Error) = Vine::Client::SendRequest(
415			&SideCarId,
416			"$activateByEvent".to_string(),
417			serde_json::json!({ "activationEvent": "*" }),
418			30_000,
419		).await {
420			dev_log!("cocoon", "warn: [CocoonManagement] $activateByEvent(\"*\") failed: {}", Error);
421		} else {
422			dev_log!("cocoon", "[CocoonManagement] Startup extensions activation triggered");
423		}
424	});
425
426	// Store process handle for health monitoring and management
427	{
428		let mut state = COCOON_STATE.lock().await;
429		state.ChildProcess = Some(ChildProcess);
430		state.IsRunning = true;
431		state.StartTime = Some(tokio::time::Instant::now());
432		dev_log!("cocoon", "[CocoonManagement] Process state updated: Running");
433	}
434
435	// Reset health monitor on successful initialization
436	{
437		let mut health = COCOON_HEALTH.lock().await;
438		health.clear_issues();
439		dev_log!("cocoon", "[CocoonManagement] Health monitor reset to active state");
440	}
441
442	// Start background health monitoring
443	let state_clone = Arc::clone(&COCOON_STATE);
444	tokio::spawn(monitor_cocoon_health_task(state_clone));
445	dev_log!("cocoon", "[CocoonManagement] Background health monitoring started");
446
447	Ok(())
448}
449
450/// Background task that monitors Cocoon process health and logs crashes.
451///
452/// Once the child process has exited (or never existed), the monitor no
453/// longer has anything useful to say — it exits quietly instead of
454/// flooding the log with "No Cocoon process to monitor" every 5s, which
455/// was rendering the dev log unreadable after any Cocoon crash.
456async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
457	loop {
458		tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
459
460		let mut state_guard = state.lock().await;
461
462		// Check if we have a child process to monitor
463		if state_guard.ChildProcess.is_some() {
464			// Get process ID before checking status
465			let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
466
467			// Check if process is still running
468			let exit_status = {
469				let child = state_guard.ChildProcess.as_mut().unwrap();
470				child.try_wait()
471			};
472
473			match exit_status {
474				Ok(Some(exit_code)) => {
475					// Process has exited (crashed or terminated)
476					let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
477					let exit_code_num = exit_code.code().unwrap_or(-1);
478					dev_log!("cocoon", "warn: [CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
479						process_id.unwrap_or(0),
480						exit_code_num,
481						uptime);
482
483					// Update state
484					state_guard.IsRunning = false;
485					state_guard.ChildProcess = None;
486
487					// Report health issue
488					{
489						let mut health = COCOON_HEALTH.lock().await;
490						health.add_issue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
491						dev_log!("cocoon", "warn: [CocoonHealth] Health score: {}", health.health_score);
492					}
493
494					// Log that automatic restart would be needed
495					dev_log!("cocoon", "warn: [CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted manually or \
496						 via application reinitialization");
497				},
498				Ok(None) => {
499					// Process is still running
500					dev_log!("cocoon", "[CocoonHealth] Cocoon process is healthy [PID: {}]", process_id.unwrap_or(0));
501				},
502				Err(e) => {
503					// Error checking process status
504					dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
505
506					// Report health issue
507					{
508						let mut health = COCOON_HEALTH.lock().await;
509						health.add_issue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
510					}
511				},
512			}
513		} else {
514			// No child process exists — log exactly once, then exit the
515			// monitor loop. Prior behaviour: flood the log with
516			// "No Cocoon process to monitor" every 5s forever after a
517			// crash, making the dev log unreadable. A future respawn will
518			// spawn a fresh monitor via `StartCocoon`.
519			dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor — exiting monitor loop");
520			drop(state_guard);
521			return;
522		}
523	}
524}