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 log::{info, trace, warn};
62use tauri::{
63	AppHandle,
64	Manager,
65	Wry,
66	path::{BaseDirectory, PathResolver},
67};
68use tokio::{
69	io::{AsyncBufReadExt, BufReader},
70	process::{Child, Command},
71	sync::Mutex,
72	time::sleep,
73};
74
75use super::InitializationData;
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_SERVER_READY_DELAY_MS:u64 = 2000;
87const BOOTSTRAP_SCRIPT_PATH:&str = "scripts/cocoon/bootstrap-fork.js";
88const HANDSHAKE_TIMEOUT_MS:u64 = 60000;
89const HEALTH_CHECK_INTERVAL_SECONDS:u64 = 5;
90const MAX_RESTART_ATTEMPTS:u32 = 3;
91const RESTART_WINDOW_SECONDS:u64 = 300;
92
93/// Global state for tracking Cocoon process lifecycle
94struct CocoonProcessState {
95	ChildProcess:Option<Child>,
96	IsRunning:bool,
97	StartTime:Option<tokio::time::Instant>,
98	RestartCount:u32,
99	LastRestartTime:Option<tokio::time::Instant>,
100}
101
102impl Default for CocoonProcessState {
103	fn default() -> Self {
104		Self {
105			ChildProcess:None,
106			IsRunning:false,
107			StartTime:None,
108			RestartCount:0,
109			LastRestartTime:None,
110		}
111	}
112}
113
114/// Global state for Cocoon process management
115lazy_static::lazy_static! {
116	static ref COCOON_STATE: Arc<Mutex<CocoonProcessState>> =
117		Arc::new(Mutex::new(CocoonProcessState::default()));
118
119	static ref COCOON_HEALTH: Arc<Mutex<HealthMonitor>> =
120		Arc::new(Mutex::new(HealthMonitor::new()));
121}
122
123/// The main entry point for initializing the Cocoon sidecar process manager.
124///
125/// This orchestrates the complete initialization sequence including:
126/// - Validating feature flags and dependencies
127/// - Launching the Cocoon process with proper configuration
128/// - Establishing gRPC communication
129/// - Performing the initialization handshake
130/// - Setting up process health monitoring
131///
132/// # Arguments
133///
134/// * `ApplicationHandle` - Tauri application handle for path resolution
135/// * `Environment` - Mountain environment containing application state and
136///   services
137///
138/// # Returns
139///
140/// * `Ok(())` - Cocoon initialized successfully and ready to accept extension
141///   requests
142/// * `Err(CommonError)` - Initialization failed with detailed error context
143///
144/// # Errors
145///
146/// - `FileSystemNotFound`: Bootstrap script not found
147/// - `IPCError`: Failed to spawn process or establish gRPC connection
148///
149/// # Example
150///
151/// ```rust,no_run
152/// use crate::Source::ProcessManagement::CocoonManagement::InitializeCocoon;
153///
154/// InitializeCocoon(&app_handle, &environment).await?;
155/// ```
156pub async fn InitializeCocoon(
157	ApplicationHandle:&AppHandle,
158	Environment:&Arc<MountainEnvironment>,
159) -> Result<(), CommonError> {
160	info!("[CocoonManagement] Initializing Cocoon sidecar manager...");
161
162	#[cfg(feature = "ExtensionHostCocoon")]
163	{
164		LaunchAndManageCocoonSideCar(ApplicationHandle.clone(), Environment.clone()).await
165	}
166
167	#[cfg(not(feature = "ExtensionHostCocoon"))]
168	{
169		info!("[CocoonManagement] 'ExtensionHostCocoon' feature is disabled. Cocoon will not be launched.");
170		Ok(())
171	}
172}
173
174/// Spawns the Cocoon process, manages its communication channels, and performs
175/// the complete initialization handshake sequence.
176///
177/// This function implements the complete Cocoon lifecycle:
178/// 1. Validates bootstrap script availability
179/// 2. Constructs environment variables for IPC and logging
180/// 3. Spawns Node.js process with proper IO redirection
181/// 4. Captures stdout/stderr for logging
182/// 5. Waits for gRPC server to be ready
183/// 6. Establishes Vine connection
184/// 7. Sends initialization payload and validates response
185///
186/// # Arguments
187///
188/// * `ApplicationHandle` - Tauri application handle for resolving resource
189///   paths
190/// * `Environment` - Mountain environment containing application state
191///
192/// # Returns
193///
194/// * `Ok(())` - Cocoon process spawned, connected, and initialized successfully
195/// * `Err(CommonError)` - Any failure during the initialization sequence
196///
197/// # Errors
198///
199/// - `FileSystemNotFound`: Bootstrap script not found in resources
200/// - `IPCError`: Failed to spawn process, connect gRPC, or complete handshake
201///
202/// # Lifecycle
203///
204/// The process runs as a background task with IO redirection for logging.
205/// Process failures are logged but not automatically restarted (callers should
206/// implement restart strategies based on their requirements).
207async fn LaunchAndManageCocoonSideCar(
208	ApplicationHandle:AppHandle,
209	Environment:Arc<MountainEnvironment>,
210) -> Result<(), CommonError> {
211	let SideCarIdentifier = COCOON_SIDE_CAR_IDENTIFIER.to_string();
212	let path_resolver:PathResolver<Wry> = ApplicationHandle.path().clone();
213
214	// Resolve bootstrap script path with validation
215	let ScriptPath = path_resolver
216		.resolve(BOOTSTRAP_SCRIPT_PATH, BaseDirectory::Resource)
217		.map_err(|Error| {
218			CommonError::FileSystemNotFound(
219				format!("Failed to resolve bootstrap script '{}': {}", BOOTSTRAP_SCRIPT_PATH, Error).into(),
220			)
221		})?;
222
223	if !ScriptPath.exists() {
224		return Err(CommonError::FileSystemNotFound(
225			format!("Cocoon bootstrap script not found at: {}", ScriptPath.display()).into(),
226		));
227	}
228
229	info!("[CocoonManagement] Found bootstrap script at: {}", ScriptPath.display());
230
231	// Build Node.js command with comprehensive environment configuration
232	let mut NodeCommand = Command::new("node");
233
234	let mut EnvironmentVariables = HashMap::new();
235
236	// VS Code protocol environment variables for extension host compatibility
237	EnvironmentVariables.insert("VSCODE_PIPE_LOGGING".to_string(), "true".to_string());
238	EnvironmentVariables.insert("VSCODE_VERBOSE_LOGGING".to_string(), "true".to_string());
239	EnvironmentVariables.insert("VSCODE_PARENT_PID".to_string(), std::process::id().to_string());
240
241	// gRPC port configuration for Vine communication
242	EnvironmentVariables.insert("MOUNTAIN_GRPC_PORT".to_string(), MOUNTAIN_GRPC_PORT.to_string());
243	EnvironmentVariables.insert("COCOON_GRPC_PORT".to_string(), COCOON_GRPC_PORT.to_string());
244
245	NodeCommand
246		.arg(&ScriptPath)
247		.env_clear()
248		.envs(EnvironmentVariables)
249		.stdin(Stdio::piped())
250		.stdout(Stdio::piped())
251		.stderr(Stdio::piped());
252
253	// Spawn the process with error handling
254	let mut ChildProcess = NodeCommand.spawn().map_err(|Error| {
255		CommonError::IPCError {
256			Description:format!("Failed to spawn Cocoon process: {} (is Node.js installed and in PATH?)", Error),
257		}
258	})?;
259
260	let ProcessId = ChildProcess.id().unwrap_or(0);
261	info!("[CocoonManagement] Cocoon process spawned [PID: {}]", ProcessId);
262
263	// Capture stdout for trace logging
264	if let Some(stdout) = ChildProcess.stdout.take() {
265		tokio::spawn(async move {
266			let Reader = BufReader::new(stdout);
267			let mut Lines = Reader.lines();
268
269			while let Ok(Some(Line)) = Lines.next_line().await {
270				trace!("[Cocoon stdout] {}", Line);
271			}
272		});
273	}
274
275	// Capture stderr for warn-level logging
276	if let Some(stderr) = ChildProcess.stderr.take() {
277		tokio::spawn(async move {
278			let Reader = BufReader::new(stderr);
279			let mut Lines = Reader.lines();
280
281			while let Ok(Some(Line)) = Lines.next_line().await {
282				warn!("[Cocoon stderr] {}", Line);
283			}
284		});
285	}
286
287	// Wait for gRPC server to initialize and listen
288	info!(
289		"[CocoonManagement] Waiting {}ms for Cocoon gRPC server to start...",
290		GRPC_SERVER_READY_DELAY_MS
291	);
292	sleep(Duration::from_millis(GRPC_SERVER_READY_DELAY_MS)).await;
293
294	// Establish Vine connection to Cocoon
295	let GRPCAddress = format!("127.0.0.1:{}", COCOON_GRPC_PORT);
296	info!("[CocoonManagement] Connecting to Cocoon gRPC server at: {}", GRPCAddress);
297
298	Vine::Client::ConnectToSideCar(SideCarIdentifier.clone(), GRPCAddress.clone())
299		.await
300		.map_err(|Error| {
301			CommonError::IPCError {
302				Description:format!(
303					"Failed to connect to Cocoon gRPC server at {}: {} (is Cocoon running?)",
304					GRPCAddress, Error
305				),
306			}
307		})?;
308
309	info!("[CocoonManagement] Connected to Cocoon. Sending initialization data...");
310
311	// Construct initialization payload
312	let MainInitializationData = InitializationData::ConstructExtensionHostInitializationData(&Environment)
313		.await
314		.map_err(|Error| {
315			CommonError::IPCError { Description:format!("Failed to construct initialization data: {}", Error) }
316		})?;
317
318	// Send initialization request with timeout
319	let Response = Vine::Client::SendRequest(
320		&SideCarIdentifier,
321		"InitializeExtensionHost".to_string(),
322		MainInitializationData,
323		HANDSHAKE_TIMEOUT_MS,
324	)
325	.await
326	.map_err(|Error| {
327		CommonError::IPCError {
328			Description:format!("Failed to send initialization request to Cocoon: {}", Error),
329		}
330	})?;
331
332	// Validate handshake response
333	match Response.as_str() {
334		Some("initialized") => {
335			info!("[CocoonManagement] Cocoon handshake complete. Extension host is ready.");
336		},
337		Some(other) => {
338			return Err(CommonError::IPCError {
339				Description:format!("Cocoon initialization failed with unexpected response: {}", other),
340			});
341		},
342		None => {
343			return Err(CommonError::IPCError {
344				Description:"Cocoon initialization failed: no response received".to_string(),
345			});
346		},
347	}
348
349	// Store process handle for health monitoring and management
350	{
351		let mut state = COCOON_STATE.lock().await;
352		state.ChildProcess = Some(ChildProcess);
353		state.IsRunning = true;
354		state.StartTime = Some(tokio::time::Instant::now());
355		info!("[CocoonManagement] Process state updated: Running");
356	}
357
358	// Reset health monitor on successful initialization
359	{
360		let mut health = COCOON_HEALTH.lock().await;
361		health.clear_issues();
362		info!("[CocoonManagement] Health monitor reset to active state");
363	}
364
365	// Start background health monitoring
366	let state_clone = Arc::clone(&COCOON_STATE);
367	tokio::spawn(monitor_cocoon_health_task(state_clone));
368	info!("[CocoonManagement] Background health monitoring started");
369
370	Ok(())
371}
372
373/// Background task that monitors Cocoon process health and logs crashes
374async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
375	loop {
376		tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
377
378		let mut state_guard = state.lock().await;
379
380		// Check if we have a child process to monitor
381		if state_guard.ChildProcess.is_some() {
382			// Get process ID before checking status
383			let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
384
385			// Check if process is still running
386			let exit_status = {
387				let child = state_guard.ChildProcess.as_mut().unwrap();
388				child.try_wait()
389			};
390
391			match exit_status {
392				Ok(Some(exit_code)) => {
393					// Process has exited (crashed or terminated)
394					let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
395					let exit_code_num = exit_code.code().unwrap_or(-1);
396					warn!(
397						"[CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
398						process_id.unwrap_or(0),
399						exit_code_num,
400						uptime
401					);
402
403					// Update state
404					state_guard.IsRunning = false;
405					state_guard.ChildProcess = None;
406
407					// Report health issue
408					{
409						let mut health = COCOON_HEALTH.lock().await;
410						health.add_issue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
411						warn!("[CocoonHealth] Health score: {}", health.health_score);
412					}
413
414					// Log that automatic restart would be needed
415					warn!(
416						"[CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted manually or \
417						 via application reinitialization"
418					);
419				},
420				Ok(None) => {
421					// Process is still running
422					trace!("[CocoonHealth] Cocoon process is healthy [PID: {}]", process_id.unwrap_or(0));
423				},
424				Err(e) => {
425					// Error checking process status
426					warn!("[CocoonHealth] Error checking process status: {}", e);
427
428					// Report health issue
429					{
430						let mut health = COCOON_HEALTH.lock().await;
431						health.add_issue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
432					}
433				},
434			}
435		} else {
436			// No child process exists
437			trace!("[CocoonHealth] No Cocoon process to monitor");
438		}
439	}
440}