Mountain/ProcessManagement/
CocoonManagement.rs1use 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
82const 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
93struct 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
114lazy_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
123pub 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
174async 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 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 let mut NodeCommand = Command::new("node");
233
234 let mut EnvironmentVariables = HashMap::new();
235
236 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 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 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 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 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 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 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 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 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 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 {
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 {
360 let mut health = COCOON_HEALTH.lock().await;
361 health.clear_issues();
362 info!("[CocoonManagement] Health monitor reset to active state");
363 }
364
365 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
373async 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 if state_guard.ChildProcess.is_some() {
382 let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
384
385 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 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 state_guard.IsRunning = false;
405 state_guard.ChildProcess = None;
406
407 {
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 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 trace!("[CocoonHealth] Cocoon process is healthy [PID: {}]", process_id.unwrap_or(0));
423 },
424 Err(e) => {
425 warn!("[CocoonHealth] Error checking process status: {}", e);
427
428 {
430 let mut health = COCOON_HEALTH.lock().await;
431 health.add_issue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
432 }
433 },
434 }
435 } else {
436 trace!("[CocoonHealth] No Cocoon process to monitor");
438 }
439 }
440}