1use 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, NodeResolver};
75use crate::{
76 Environment::MountainEnvironment::MountainEnvironment,
77 IPC::Common::HealthStatus::{HealthIssue::Enum as HealthIssue, HealthMonitor::Struct as HealthMonitor},
78 ProcessManagement::ExtractDevTag::Fn as ExtractDevTag,
79 Vine,
80 dev_log,
81};
82
83const COCOON_SIDE_CAR_IDENTIFIER:&str = "cocoon-main";
85
86const COCOON_GRPC_PORT:u16 = 50052;
87
88const MOUNTAIN_GRPC_PORT:u16 = 50051;
89
90const BOOTSTRAP_SCRIPT_PATH:&str = "scripts/cocoon/bootstrap-fork.js";
91
92const GRPC_CONNECT_INITIAL_MS:u64 = 50;
104
105const GRPC_CONNECT_MAX_DELAY_MS:u64 = 2_000;
106
107const GRPC_CONNECT_BUDGET_MS:u64 = 20_000;
108
109const COCOON_BUNDLE_PROBE:&str = "../Cocoon/Target/Bootstrap/Implementation/Cocoon/Main.js";
116
117const HANDSHAKE_TIMEOUT_MS:u64 = 60000;
118
119const HEALTH_CHECK_INTERVAL_SECONDS:u64 = 5;
120
121#[allow(dead_code)]
122const MAX_RESTART_ATTEMPTS:u32 = 3;
123
124#[allow(dead_code)]
125const RESTART_WINDOW_SECONDS:u64 = 300;
126
127#[allow(dead_code)]
129struct CocoonProcessState {
130 ChildProcess:Option<Child>,
131
132 IsRunning:bool,
133
134 StartTime:Option<tokio::time::Instant>,
135
136 RestartCount:u32,
137
138 LastRestartTime:Option<tokio::time::Instant>,
139}
140
141impl Default for CocoonProcessState {
142 fn default() -> Self {
143 Self {
144 ChildProcess:None,
145
146 IsRunning:false,
147
148 StartTime:None,
149
150 RestartCount:0,
151
152 LastRestartTime:None,
153 }
154 }
155}
156
157lazy_static::lazy_static! {
159
160 static ref COCOON_STATE: Arc<Mutex<CocoonProcessState>> =
161 Arc::new(Mutex::new(CocoonProcessState::default()));
162
163 static ref COCOON_HEALTH: Arc<Mutex<HealthMonitor>> =
164 Arc::new(Mutex::new(HealthMonitor::new()));
165}
166
167static COCOON_PID:std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
172
173pub fn GetCocoonPid() -> Option<u32> {
176 match COCOON_PID.load(std::sync::atomic::Ordering::Relaxed) {
177 0 => None,
178
179 Pid => Some(Pid),
180 }
181}
182
183pub async fn InitializeCocoon(
217 ApplicationHandle:&AppHandle,
218
219 Environment:&Arc<MountainEnvironment>,
220) -> Result<(), CommonError> {
221 dev_log!("cocoon", "[CocoonManagement] Initializing Cocoon sidecar manager...");
222
223 if matches!(std::env::var("Spawn").as_deref(), Ok("0") | Ok("false")) {
229 dev_log!("cocoon", "[CocoonManagement] Skipping spawn (Spawn=false)");
230
231 return Ok(());
232 }
233
234 #[cfg(feature = "ExtensionHostCocoon")]
235 {
236 LaunchAndManageCocoonSideCar(ApplicationHandle.clone(), Environment.clone()).await
237 }
238
239 #[cfg(not(feature = "ExtensionHostCocoon"))]
240 {
241 dev_log!(
242 "cocoon",
243 "[CocoonManagement] 'ExtensionHostCocoon' feature is disabled. Cocoon will not be launched."
244 );
245
246 Ok(())
247 }
248}
249
250async fn LaunchAndManageCocoonSideCar(
284 ApplicationHandle:AppHandle,
285
286 Environment:Arc<MountainEnvironment>,
287) -> Result<(), CommonError> {
288 let SideCarIdentifier = COCOON_SIDE_CAR_IDENTIFIER.to_string();
289
290 let path_resolver:PathResolver<Wry> = ApplicationHandle.path().clone();
291
292 let ScriptPath = path_resolver
297 .resolve(BOOTSTRAP_SCRIPT_PATH, BaseDirectory::Resource)
298 .ok()
299 .filter(|P| P.exists())
300 .or_else(|| {
301 std::env::current_exe().ok().and_then(|Exe| {
302 let MountainRoot = Exe.parent()?.parent()?.parent()?;
303 let Candidate = MountainRoot.join(BOOTSTRAP_SCRIPT_PATH);
304 if Candidate.exists() { Some(Candidate) } else { None }
305 })
306 })
307 .ok_or_else(|| {
308 CommonError::FileSystemNotFound(
309 format!(
310 "Cocoon bootstrap script '{}' not found in resources or relative to executable",
311 BOOTSTRAP_SCRIPT_PATH
312 )
313 .into(),
314 )
315 })?;
316
317 dev_log!(
318 "cocoon",
319 "[CocoonManagement] Found bootstrap script at: {}",
320 ScriptPath.display()
321 );
322
323 crate::dev_log!("cocoon", "bootstrap script: {}", ScriptPath.display());
324
325 if let Some(BootstrapDirectory) = ScriptPath.parent() {
334 let ProbePath = BootstrapDirectory.join("../..").join(COCOON_BUNDLE_PROBE);
335
336 if !ProbePath.exists() {
337 return Err(CommonError::IPCError {
338 Description:format!(
339 "Cocoon bundle is missing at {}. Run `pnpm run prepublishOnly --filter=@codeeditorland/cocoon` \
340 (or the full `./Maintain/Debug/Build.sh --profile debug-electron`) before launching - node will \
341 fail to import without it and Mountain will fall into degraded mode with zero extensions \
342 available. Root cause is typically an esbuild failure in an upstream Cocoon source file or a \
343 stale `rm -rf Element/Cocoon/Target` without a rebuild.",
344 ProbePath.display()
345 ),
346 });
347 }
348
349 dev_log!("cocoon", "[CocoonManagement] pre-flight OK: bundle at {}", ProbePath.display());
350 }
351
352 SweepStaleCocoon(COCOON_GRPC_PORT);
362
363 let ResolvedNodeBinary = NodeResolver::ResolveNodeBinary::Fn(&ApplicationHandle);
367
368 let mut NodeCommand = Command::new(&ResolvedNodeBinary.Path);
370
371 let mut EnvironmentVariables = HashMap::new();
372
373 EnvironmentVariables.insert("VSCODE_PIPE_LOGGING".to_string(), "true".to_string());
375
376 EnvironmentVariables.insert("VSCODE_VERBOSE_LOGGING".to_string(), "true".to_string());
377
378 EnvironmentVariables.insert("VSCODE_PARENT_PID".to_string(), std::process::id().to_string());
379
380 EnvironmentVariables.insert("MOUNTAIN_GRPC_PORT".to_string(), MOUNTAIN_GRPC_PORT.to_string());
382
383 EnvironmentVariables.insert("COCOON_GRPC_PORT".to_string(), COCOON_GRPC_PORT.to_string());
384
385 if let Ok(Path) = std::env::var("PATH") {
387 EnvironmentVariables.insert("PATH".to_string(), Path);
388 }
389
390 if let Ok(Home) = std::env::var("HOME") {
391 EnvironmentVariables.insert("HOME".to_string(), Home);
392 }
393
394 const LandEnvAllowList:&[&str] = &[
412 "Authorize",
413 "Beam",
414 "Report",
415 "Brand",
416 "Replay",
417 "Ask",
418 "Throttle",
419 "Buffer",
420 "Batch",
421 "Cap",
422 "Capture",
423 "OTLPEndpoint",
424 "OTLPEnabled",
425 "Pick",
426 "Require",
427 "Lodge",
428 "Extend",
429 "Probe",
430 "Ship",
431 "Wire",
432 "Install",
433 "Mute",
434 "Skip",
435 "Spawn",
436 "Render",
437 "Walk",
438 "Trace",
439 "Record",
440 "Profile",
441 "Diagnose",
442 "Resolve",
443 "Open",
444 "Warn",
445 "Catch",
446 "Source",
447 "Track",
448 "Defer",
449 "Boot",
450 "Pack",
451 ];
452
453 for (Key, Value) in std::env::vars() {
454 if Key.starts_with("Product")
455 || Key.starts_with("Tier")
456 || Key.starts_with("Network")
457 || LandEnvAllowList.contains(&Key.as_str())
458 {
459 EnvironmentVariables.insert(Key, Value);
460 }
461 }
462
463 for Key in ["NODE_ENV", "TAURI_ENV_DEBUG"] {
469 if let Ok(Value) = std::env::var(Key) {
470 EnvironmentVariables.insert(Key.to_string(), Value);
471 }
472 }
473
474 NodeCommand
475 .arg(&ScriptPath)
476 .env_clear()
477 .envs(EnvironmentVariables)
478 .stdin(Stdio::piped())
479 .stdout(Stdio::piped())
480 .stderr(Stdio::piped());
481
482 let mut ChildProcess = NodeCommand.spawn().map_err(|Error| {
484 CommonError::IPCError {
485 Description:format!(
486 "Failed to spawn Cocoon with node={} (source={}): {}. Override with Pick=/absolute/path or install \
487 Node.js.",
488 ResolvedNodeBinary.Path.display(),
489 ResolvedNodeBinary.Source.AsLabel(),
490 Error
491 ),
492 }
493 })?;
494
495 let ProcessId = ChildProcess.id().unwrap_or(0);
496
497 COCOON_PID.store(ProcessId, std::sync::atomic::Ordering::Relaxed);
498
499 dev_log!("cocoon", "[CocoonManagement] Cocoon process spawned [PID: {}]", ProcessId);
500
501 crate::dev_log!("cocoon", "spawned PID={}", ProcessId);
502
503 if let Some(stdout) = ChildProcess.stdout.take() {
515 tokio::spawn(async move {
516 let Reader = BufReader::new(stdout);
517 let mut Lines = Reader.lines();
518
519 while let Ok(Some(Line)) = Lines.next_line().await {
520 if let Some(ForwardedTag) = ExtractDevTag(&Line) {
521 match ForwardedTag.as_str() {
526 "bootstrap-stage" => dev_log!("bootstrap-stage", "[Cocoon stdout] {}", Line),
527 "ext-activate" => dev_log!("ext-activate", "[Cocoon stdout] {}", Line),
528 "config-prime" => dev_log!("config-prime", "[Cocoon stdout] {}", Line),
529 "breaker" => dev_log!("breaker", "[Cocoon stdout] {}", Line),
530 _ => dev_log!("cocoon", "[Cocoon stdout] {}", Line),
531 }
532 } else {
533 dev_log!("cocoon", "[Cocoon stdout] {}", Line);
534 }
535 }
536 });
537 }
538
539 if let Some(stderr) = ChildProcess.stderr.take() {
562 tokio::spawn(async move {
563 let Reader = BufReader::new(stderr);
564 let mut Lines = Reader.lines();
565 let mut SuppressStackFrames = false;
566
567 while let Ok(Some(Line)) = Lines.next_line().await {
568 let Trimmed = Line.trim_start();
569 let IsStackFrame = Trimmed.starts_with("at ")
570 || Trimmed.starts_with("code: '")
571 || Trimmed == "}"
572 || Trimmed.is_empty();
573 if SuppressStackFrames && IsStackFrame {
574 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
575 continue;
576 }
577 SuppressStackFrames = false;
580
581 let IsBenignSingleLine = Line.contains(": is already signed")
582 || Line.contains(": replacing existing signature")
583 || Line.contains("DeprecationWarning:")
584 || Line.contains("--trace-deprecation")
585 || Line.contains("--trace-warnings");
586 let IsBenignStackHead = Line.contains("EntryNotFound (FileSystemError):")
587 || Line.contains("FileNotFound (FileSystemError):")
588 || Line.contains("[LandFix:UnhandledRejection]")
589 || Line.starts_with("[Patcher] unhandledRejection:")
590 || Line.starts_with("[Patcher] uncaughtException:");
591 if IsBenignStackHead {
592 SuppressStackFrames = true;
593 }
594 if IsBenignSingleLine || IsBenignStackHead {
595 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
596 } else {
597 dev_log!("cocoon", "warn: [Cocoon stderr] {}", Line);
598 }
599 }
600 });
601 }
602
603 let GRPCAddress = format!("127.0.0.1:{}", COCOON_GRPC_PORT);
622
623 dev_log!(
624 "cocoon",
625 "[CocoonManagement] Connecting to Cocoon gRPC at {} (exponential backoff, budget={}ms)...",
626 GRPCAddress,
627 GRPC_CONNECT_BUDGET_MS
628 );
629
630 let ConnectStart = tokio::time::Instant::now();
631
632 let mut CurrentDelayMs:u64 = GRPC_CONNECT_INITIAL_MS;
633
634 let mut ConnectAttempt = 0u32;
635
636 loop {
637 ConnectAttempt += 1;
638
639 crate::dev_log!(
640 "grpc",
641 "connecting to Cocoon at {} (attempt {}, elapsed={}ms)",
642 GRPCAddress,
643 ConnectAttempt,
644 ConnectStart.elapsed().as_millis()
645 );
646
647 match Vine::Client::ConnectToSideCar::Fn(SideCarIdentifier.clone(), GRPCAddress.clone()).await {
648 Ok(()) => {
649 crate::dev_log!(
650 "grpc",
651 "connected to Cocoon on attempt {} (elapsed={}ms)",
652 ConnectAttempt,
653 ConnectStart.elapsed().as_millis()
654 );
655
656 break;
657 },
658
659 Err(Error) => {
660 match ChildProcess.try_wait() {
666 Ok(Some(ExitStatus)) => {
667 let ExitCode = ExitStatus.code().unwrap_or(-1);
668
669 crate::dev_log!(
670 "grpc",
671 "attempt {} aborted: Cocoon Node process exited with code={} after {}ms - stderr above \
672 (if any) explains why",
673 ConnectAttempt,
674 ExitCode,
675 ConnectStart.elapsed().as_millis()
676 );
677
678 return Err(CommonError::IPCError {
679 Description:format!(
680 "Cocoon spawned but exited with code {} before Mountain could connect. See \
681 `[DEV:COCOON] warn: [Cocoon stderr] …` lines above for the Node-side error - \
682 typically a missing bundle (\"Cannot find module …\") or an ESM/CJS import drift \
683 after a partial build.",
684 ExitCode
685 ),
686 });
687 },
688
689 Ok(None) => { },
690
691 Err(WaitErr) => {
692 crate::dev_log!("grpc", "warn: try_wait on Cocoon child failed: {} (continuing)", WaitErr);
697 },
698 }
699
700 let Elapsed = ConnectStart.elapsed().as_millis() as u64;
701
702 if Elapsed >= GRPC_CONNECT_BUDGET_MS {
703 crate::dev_log!(
704 "grpc",
705 "attempt {} timed out (budget {}ms exhausted): {}",
706 ConnectAttempt,
707 GRPC_CONNECT_BUDGET_MS,
708 Error
709 );
710
711 return Err(CommonError::IPCError {
712 Description:format!(
713 "Failed to connect to Cocoon gRPC at {} after {} attempts over {}ms: {} (is Cocoon \
714 running? check `[DEV:COCOON]` log lines for stderr, or re-run with the debug-electron \
715 build profile if the bundle is stale)",
716 GRPCAddress, ConnectAttempt, GRPC_CONNECT_BUDGET_MS, Error
717 ),
718 });
719 }
720
721 crate::dev_log!(
722 "grpc",
723 "attempt {} pending (Cocoon still booting): {}, backing off {}ms",
724 ConnectAttempt,
725 Error,
726 CurrentDelayMs
727 );
728
729 sleep(Duration::from_millis(CurrentDelayMs)).await;
730
731 CurrentDelayMs = (CurrentDelayMs * 2).min(GRPC_CONNECT_MAX_DELAY_MS);
735 },
736 }
737 }
738
739 dev_log!(
740 "cocoon",
741 "[CocoonManagement] Connected to Cocoon. Sending initialization data..."
742 );
743
744 sleep(Duration::from_millis(200)).await;
747
748 let MainInitializationData = InitializationData::ConstructExtensionHostInitializationData(&Environment)
750 .await
751 .map_err(|Error| {
752 CommonError::IPCError { Description:format!("Failed to construct initialization data: {}", Error) }
753 })?;
754
755 let Response = Vine::Client::SendRequest::Fn(
757 &SideCarIdentifier,
758 "InitializeExtensionHost".to_string(),
759 MainInitializationData,
760 HANDSHAKE_TIMEOUT_MS,
761 )
762 .await
763 .map_err(|Error| {
764 CommonError::IPCError {
765 Description:format!("Failed to send initialization request to Cocoon: {}", Error),
766 }
767 })?;
768
769 match Response.as_str() {
771 Some("initialized") => {
772 dev_log!(
773 "cocoon",
774 "[CocoonManagement] Cocoon handshake complete. Extension host is ready."
775 );
776 },
777
778 Some(other) => {
779 return Err(CommonError::IPCError {
780 Description:format!("Cocoon initialization failed with unexpected response: {}", other),
781 });
782 },
783
784 None => {
785 return Err(CommonError::IPCError {
786 Description:"Cocoon initialization failed: no response received".to_string(),
787 });
788 },
789 }
790
791 let SideCarId = SideCarIdentifier.clone();
807
808 let EnvironmentForActivation = Environment.clone();
809
810 tokio::spawn(async move {
811 sleep(Duration::from_millis(500)).await;
813
814 crate::dev_log!("exthost", "Sending $activateByEvent(\"*\") to Cocoon");
815
816 if let Err(Error) = Vine::Client::SendRequest::Fn(
817 &SideCarId,
818 "$activateByEvent".to_string(),
819 serde_json::json!({ "activationEvent": "*" }),
820 30_000,
821 )
822 .await
823 {
824 dev_log!("cocoon", "warn: [CocoonManagement] $activateByEvent(\"*\") failed: {}", Error);
825 return;
826 }
827 dev_log!("cocoon", "[CocoonManagement] Startup extensions activation (*) triggered");
828
829 let WorkspacePatterns = {
838 let AppState = &EnvironmentForActivation.ApplicationState;
839 let Folders:Vec<std::path::PathBuf> = AppState
840 .Workspace
841 .WorkspaceFolders
842 .lock()
843 .ok()
844 .map(|Guard| {
845 Guard
846 .iter()
847 .filter_map(|Folder| Folder.URI.to_file_path().ok())
848 .collect::<Vec<_>>()
849 })
850 .unwrap_or_default();
851
852 let Patterns:Vec<String> = AppState
853 .Extension
854 .ScannedExtensions
855 .ScannedExtensions
856 .lock()
857 .ok()
858 .map(|Guard| {
859 let mut Set:std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
860 for Description in Guard.values() {
861 if let Some(Events) = &Description.ActivationEvents {
862 for Event in Events {
863 if let Some(Pattern) = Event.strip_prefix("workspaceContains:") {
864 Set.insert(Pattern.to_string());
865 }
866 }
867 }
868 }
869 Set.into_iter().collect()
870 })
871 .unwrap_or_default();
872
873 (Folders, Patterns)
874 };
875
876 let (WorkspaceFolders, Patterns):(Vec<std::path::PathBuf>, Vec<String>) = WorkspacePatterns;
877 if !WorkspaceFolders.is_empty() && !Patterns.is_empty() {
878 let Matched = FindMatchingWorkspaceContainsPatterns(&WorkspaceFolders, &Patterns);
879 dev_log!(
880 "exthost",
881 "[CocoonManagement] workspaceContains scan: {} pattern(s) matched across {} folder(s)",
882 Matched.len(),
883 WorkspaceFolders.len()
884 );
885 for Pattern in Matched {
886 let Event = format!("workspaceContains:{}", Pattern);
887 if let Err(Error) = Vine::Client::SendRequest::Fn(
888 &SideCarId,
889 "$activateByEvent".to_string(),
890 serde_json::json!({ "activationEvent": Event }),
891 30_000,
892 )
893 .await
894 {
895 dev_log!(
896 "cocoon",
897 "warn: [CocoonManagement] $activateByEvent({}) failed: {}",
898 Event,
899 Error
900 );
901 }
902 }
903 }
904
905 sleep(Duration::from_millis(2_000)).await;
909 if let Err(Error) = Vine::Client::SendRequest::Fn(
910 &SideCarId,
911 "$activateByEvent".to_string(),
912 serde_json::json!({ "activationEvent": "onStartupFinished" }),
913 30_000,
914 )
915 .await
916 {
917 dev_log!(
918 "cocoon",
919 "warn: [CocoonManagement] $activateByEvent(onStartupFinished) failed: {}",
920 Error
921 );
922 } else {
923 dev_log!("cocoon", "[CocoonManagement] onStartupFinished activation triggered");
924 }
925 });
926
927 {
929 let mut state = COCOON_STATE.lock().await;
930
931 state.ChildProcess = Some(ChildProcess);
932
933 state.IsRunning = true;
934
935 state.StartTime = Some(tokio::time::Instant::now());
936
937 dev_log!("cocoon", "[CocoonManagement] Process state updated: Running");
938 }
939
940 {
942 let mut health = COCOON_HEALTH.lock().await;
943
944 health.ClearIssues();
945
946 dev_log!("cocoon", "[CocoonManagement] Health monitor reset to active state");
947 }
948
949 let state_clone = Arc::clone(&COCOON_STATE);
951
952 tokio::spawn(monitor_cocoon_health_task(state_clone));
953
954 dev_log!("cocoon", "[CocoonManagement] Background health monitoring started");
955
956 Ok(())
957}
958
959async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
966 loop {
967 tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
968
969 let mut state_guard = state.lock().await;
970
971 if state_guard.ChildProcess.is_some() {
973 let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
975
976 let exit_status = {
978 let child = state_guard.ChildProcess.as_mut().unwrap();
979
980 child.try_wait()
981 };
982
983 match exit_status {
984 Ok(Some(exit_code)) => {
985 let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
987
988 let exit_code_num = exit_code.code().unwrap_or(-1);
989
990 dev_log!(
991 "cocoon",
992 "warn: [CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
993 process_id.unwrap_or(0),
994 exit_code_num,
995 uptime
996 );
997
998 state_guard.IsRunning = false;
1000
1001 state_guard.ChildProcess = None;
1002
1003 COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
1004
1005 {
1007 let mut health = COCOON_HEALTH.lock().await;
1008
1009 health.AddIssue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
1010
1011 dev_log!("cocoon", "warn: [CocoonHealth] Health score: {}", health.HealthScore);
1012 }
1013
1014 dev_log!(
1016 "cocoon",
1017 "warn: [CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted \
1018 manually or via application reinitialization"
1019 );
1020 },
1021
1022 Ok(None) => {
1023 dev_log!(
1025 "cocoon",
1026 "[CocoonHealth] Cocoon process is healthy [PID: {}]",
1027 process_id.unwrap_or(0)
1028 );
1029 },
1030
1031 Err(e) => {
1032 dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
1034
1035 {
1037 let mut health = COCOON_HEALTH.lock().await;
1038
1039 health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
1040 }
1041 },
1042 }
1043 } else {
1044 dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
1050
1051 drop(state_guard);
1052
1053 return;
1054 }
1055 }
1056}
1057
1058pub async fn HardKillCocoon() {
1068 let mut State = COCOON_STATE.lock().await;
1069
1070 if let Some(mut Child) = State.ChildProcess.take() {
1071 let Pid = Child.id().unwrap_or(0);
1072
1073 match Child.try_wait() {
1074 Ok(Some(_Status)) => {
1075 dev_log!("cocoon", "[CocoonShutdown] Child PID {} already exited; clearing handle.", Pid);
1076 },
1077
1078 Ok(None) => {
1079 dev_log!(
1080 "cocoon",
1081 "[CocoonShutdown] Child PID {} still alive after $shutdown; sending SIGKILL.",
1082 Pid
1083 );
1084
1085 if let Err(Error) = Child.start_kill() {
1086 dev_log!("cocoon", "warn: [CocoonShutdown] start_kill failed on PID {}: {}", Pid, Error);
1087 }
1088
1089 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), Child.wait()).await;
1091 },
1092
1093 Err(Error) => {
1094 dev_log!("cocoon", "warn: [CocoonShutdown] try_wait failed on PID {}: {}", Pid, Error);
1095 },
1096 }
1097 }
1098
1099 State.IsRunning = false;
1100}
1101
1102fn SweepStaleCocoon(Port:u16) {
1118 use std::{net::TcpStream, time::Duration};
1119
1120 let Addr = format!("127.0.0.1:{}", Port);
1121
1122 let Probe =
1125 TcpStream::connect_timeout(&Addr.parse().expect("valid socket addr literal"), Duration::from_millis(200));
1126
1127 if Probe.is_err() {
1128 dev_log!("cocoon", "[CocoonSweep] Port {} is clean (no prior listener).", Port);
1129
1130 return;
1131 }
1132
1133 dev_log!(
1134 "cocoon",
1135 "[CocoonSweep] Port {} has a listener - attempting to resolve owner via lsof.",
1136 Port
1137 );
1138
1139 let LsofOutput = std::process::Command::new("lsof")
1141 .args(["-nP", &format!("-iTCP:{}", Port), "-sTCP:LISTEN", "-t"])
1142 .output();
1143
1144 let Output = match LsofOutput {
1145 Ok(O) => O,
1146
1147 Err(Error) => {
1148 dev_log!(
1149 "cocoon",
1150 "warn: [CocoonSweep] lsof unavailable ({}). Skipping sweep; Cocoon spawn may fail with EADDRINUSE.",
1151 Error
1152 );
1153
1154 return;
1155 },
1156 };
1157
1158 if !Output.status.success() {
1159 dev_log!("cocoon", "warn: [CocoonSweep] lsof exited non-zero. Skipping sweep.");
1160
1161 return;
1162 }
1163
1164 let Stdout = String::from_utf8_lossy(&Output.stdout);
1165
1166 let Pids:Vec<i32> = Stdout.lines().filter_map(|L| L.trim().parse::<i32>().ok()).collect();
1167
1168 if Pids.is_empty() {
1169 dev_log!(
1170 "cocoon",
1171 "warn: [CocoonSweep] Port {} answered but lsof found no LISTEN PID - giving up.",
1172 Port
1173 );
1174
1175 return;
1176 }
1177
1178 let SelfPid = std::process::id() as i32;
1181
1182 for Pid in Pids {
1183 if Pid == SelfPid {
1184 dev_log!(
1185 "cocoon",
1186 "warn: [CocoonSweep] Port {} owned by Mountain itself (PID {}); refusing to kill.",
1187 Port,
1188 Pid
1189 );
1190
1191 continue;
1192 }
1193
1194 dev_log!("cocoon", "[CocoonSweep] Killing stale PID {} (SIGTERM).", Pid);
1195
1196 let _ = std::process::Command::new("kill").arg(Pid.to_string()).status();
1197
1198 std::thread::sleep(Duration::from_millis(500));
1199
1200 let StillAlive = std::process::Command::new("kill")
1202 .args(["-0", &Pid.to_string()])
1203 .status()
1204 .map(|S| S.success())
1205 .unwrap_or(false);
1206
1207 if StillAlive {
1208 dev_log!("cocoon", "warn: [CocoonSweep] PID {} survived SIGTERM; sending SIGKILL.", Pid);
1209
1210 let _ = std::process::Command::new("kill").args(["-9", &Pid.to_string()]).status();
1211
1212 std::thread::sleep(Duration::from_millis(200));
1213 }
1214
1215 dev_log!("cocoon", "[CocoonSweep] PID {} reaped.", Pid);
1216 }
1217}
1218
1219fn FindMatchingWorkspaceContainsPatterns(Folders:&[std::path::PathBuf], Patterns:&[String]) -> Vec<String> {
1236 use std::collections::HashSet;
1237
1238 const MAX_DEPTH:usize = 3;
1239
1240 const MAX_ENTRIES_PER_ROOT:usize = 4096;
1241
1242 let mut Matched:HashSet<String> = HashSet::new();
1243
1244 for Folder in Folders {
1245 if !Folder.is_dir() {
1246 continue;
1247 }
1248
1249 let mut Entries:Vec<String> = Vec::new();
1251
1252 let mut Stack:Vec<(std::path::PathBuf, usize)> = vec![(Folder.clone(), 0)];
1253
1254 while let Some((Current, Depth)) = Stack.pop() {
1255 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1256 break;
1257 }
1258
1259 let ReadDirResult = std::fs::read_dir(&Current);
1260
1261 let ReadDir = match ReadDirResult {
1262 Ok(R) => R,
1263
1264 Err(_) => continue,
1265 };
1266
1267 for Entry in ReadDir.flatten() {
1268 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1269 break;
1270 }
1271
1272 let Path = Entry.path();
1273
1274 let Relative = match Path.strip_prefix(Folder) {
1275 Ok(R) => R.to_string_lossy().replace('\\', "/"),
1276
1277 Err(_) => continue,
1278 };
1279
1280 let IsDir = Entry.file_type().map(|T| T.is_dir()).unwrap_or(false);
1281
1282 Entries.push(Relative.clone());
1283
1284 if IsDir && Depth + 1 < MAX_DEPTH {
1285 Stack.push((Path, Depth + 1));
1286 }
1287 }
1288 }
1289
1290 for Pattern in Patterns {
1291 if Matched.contains(Pattern) {
1292 continue;
1293 }
1294
1295 if PatternMatchesAnyEntry(Pattern, &Entries) {
1296 Matched.insert(Pattern.clone());
1297 }
1298 }
1299 }
1300
1301 Matched.into_iter().collect()
1302}
1303
1304fn PatternMatchesAnyEntry(Pattern:&str, Entries:&[String]) -> bool {
1308 let HasWildcard = Pattern.contains('*') || Pattern.contains('?');
1309
1310 if !HasWildcard {
1311 return Entries.iter().any(|E| E == Pattern);
1312 }
1313
1314 let PatternSegments:Vec<&str> = Pattern.split('/').collect();
1315
1316 Entries
1317 .iter()
1318 .any(|E| SegmentMatch(&PatternSegments, &E.split('/').collect::<Vec<_>>()))
1319}
1320
1321fn SegmentMatch(Pattern:&[&str], Entry:&[&str]) -> bool {
1322 if Pattern.is_empty() {
1323 return Entry.is_empty();
1324 }
1325
1326 let Head = Pattern[0];
1327
1328 if Head == "**" {
1329 for Consumed in 0..=Entry.len() {
1331 if SegmentMatch(&Pattern[1..], &Entry[Consumed..]) {
1332 return true;
1333 }
1334 }
1335
1336 return false;
1337 }
1338
1339 if Entry.is_empty() {
1340 return false;
1341 }
1342
1343 if SingleSegmentMatch(Head, Entry[0]) {
1344 return SegmentMatch(&Pattern[1..], &Entry[1..]);
1345 }
1346
1347 false
1348}
1349
1350fn SingleSegmentMatch(Pattern:&str, Segment:&str) -> bool {
1351 if Pattern == "*" {
1352 return true;
1353 }
1354
1355 if !Pattern.contains('*') && !Pattern.contains('?') {
1356 return Pattern == Segment;
1357 }
1358
1359 let Fragments:Vec<&str> = Pattern.split('*').collect();
1364
1365 let mut Cursor = 0usize;
1366
1367 for (Index, Fragment) in Fragments.iter().enumerate() {
1368 if Fragment.is_empty() {
1369 continue;
1370 }
1371
1372 if Index == 0 {
1373 if !Segment[Cursor..].starts_with(Fragment) {
1374 return false;
1375 }
1376
1377 Cursor += Fragment.len();
1378
1379 continue;
1380 }
1381
1382 match Segment[Cursor..].find(Fragment) {
1383 Some(Offset) => Cursor += Offset + Fragment.len(),
1384
1385 None => return false,
1386 }
1387 }
1388
1389 if let Some(Last) = Fragments.last()
1390 && !Last.is_empty()
1391 {
1392 return Segment.ends_with(Last);
1393 }
1394
1395 true
1396}