Mountain/ProcessManagement/
CocoonManagement.rs1use 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
82const 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
94struct 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
115lazy_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
124pub 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
175async 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 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 let mut NodeCommand = Command::new("node");
245
246 let mut EnvironmentVariables = HashMap::new();
247
248 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 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 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 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 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 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 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 sleep(Duration::from_millis(200)).await;
365
366 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 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 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 let SideCarId = SideCarIdentifier.clone();
408 tokio::spawn(async move {
409 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 {
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 {
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 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
450async 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 if state_guard.ChildProcess.is_some() {
464 let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
466
467 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 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 state_guard.IsRunning = false;
485 state_guard.ChildProcess = None;
486
487 {
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 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 dev_log!("cocoon", "[CocoonHealth] Cocoon process is healthy [PID: {}]", process_id.unwrap_or(0));
501 },
502 Err(e) => {
503 dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
505
506 {
508 let mut health = COCOON_HEALTH.lock().await;
509 health.add_issue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
510 }
511 },
512 }
513 } else {
514 dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor — exiting monitor loop");
520 drop(state_guard);
521 return;
522 }
523 }
524}