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";
85const COCOON_GRPC_PORT:u16 = 50052;
86const MOUNTAIN_GRPC_PORT:u16 = 50051;
87const BOOTSTRAP_SCRIPT_PATH:&str = "scripts/cocoon/bootstrap-fork.js";
88
89const GRPC_CONNECT_INITIAL_MS:u64 = 50;
101const GRPC_CONNECT_MAX_DELAY_MS:u64 = 2_000;
102const GRPC_CONNECT_BUDGET_MS:u64 = 20_000;
103
104const COCOON_BUNDLE_PROBE:&str = "../Cocoon/Target/Bootstrap/Implementation/CocoonMain.js";
111const HANDSHAKE_TIMEOUT_MS:u64 = 60000;
112const HEALTH_CHECK_INTERVAL_SECONDS:u64 = 5;
113#[allow(dead_code)]
114const MAX_RESTART_ATTEMPTS:u32 = 3;
115#[allow(dead_code)]
116const RESTART_WINDOW_SECONDS:u64 = 300;
117
118#[allow(dead_code)]
120struct CocoonProcessState {
121 ChildProcess:Option<Child>,
122 IsRunning:bool,
123 StartTime:Option<tokio::time::Instant>,
124 RestartCount:u32,
125 LastRestartTime:Option<tokio::time::Instant>,
126}
127
128impl Default for CocoonProcessState {
129 fn default() -> Self {
130 Self {
131 ChildProcess:None,
132 IsRunning:false,
133 StartTime:None,
134 RestartCount:0,
135 LastRestartTime:None,
136 }
137 }
138}
139
140lazy_static::lazy_static! {
142 static ref COCOON_STATE: Arc<Mutex<CocoonProcessState>> =
143 Arc::new(Mutex::new(CocoonProcessState::default()));
144
145 static ref COCOON_HEALTH: Arc<Mutex<HealthMonitor>> =
146 Arc::new(Mutex::new(HealthMonitor::new()));
147}
148
149static COCOON_PID:std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
154
155pub fn GetCocoonPid() -> Option<u32> {
158 match COCOON_PID.load(std::sync::atomic::Ordering::Relaxed) {
159 0 => None,
160 Pid => Some(Pid),
161 }
162}
163
164pub async fn InitializeCocoon(
198 ApplicationHandle:&AppHandle,
199 Environment:&Arc<MountainEnvironment>,
200) -> Result<(), CommonError> {
201 dev_log!("cocoon", "[CocoonManagement] Initializing Cocoon sidecar manager...");
202
203 if matches!(std::env::var("Spawn").as_deref(), Ok("0") | Ok("false")) {
209 dev_log!("cocoon", "[CocoonManagement] Skipping spawn (Spawn=false)");
210 return Ok(());
211 }
212
213 #[cfg(feature = "ExtensionHostCocoon")]
214 {
215 LaunchAndManageCocoonSideCar(ApplicationHandle.clone(), Environment.clone()).await
216 }
217
218 #[cfg(not(feature = "ExtensionHostCocoon"))]
219 {
220 dev_log!(
221 "cocoon",
222 "[CocoonManagement] 'ExtensionHostCocoon' feature is disabled. Cocoon will not be launched."
223 );
224 Ok(())
225 }
226}
227
228async fn LaunchAndManageCocoonSideCar(
262 ApplicationHandle:AppHandle,
263 Environment:Arc<MountainEnvironment>,
264) -> Result<(), CommonError> {
265 let SideCarIdentifier = COCOON_SIDE_CAR_IDENTIFIER.to_string();
266 let path_resolver:PathResolver<Wry> = ApplicationHandle.path().clone();
267
268 let ScriptPath = path_resolver
273 .resolve(BOOTSTRAP_SCRIPT_PATH, BaseDirectory::Resource)
274 .ok()
275 .filter(|P| P.exists())
276 .or_else(|| {
277 std::env::current_exe().ok().and_then(|Exe| {
278 let MountainRoot = Exe.parent()?.parent()?.parent()?;
279 let Candidate = MountainRoot.join(BOOTSTRAP_SCRIPT_PATH);
280 if Candidate.exists() { Some(Candidate) } else { None }
281 })
282 })
283 .ok_or_else(|| {
284 CommonError::FileSystemNotFound(
285 format!(
286 "Cocoon bootstrap script '{}' not found in resources or relative to executable",
287 BOOTSTRAP_SCRIPT_PATH
288 )
289 .into(),
290 )
291 })?;
292
293 dev_log!(
294 "cocoon",
295 "[CocoonManagement] Found bootstrap script at: {}",
296 ScriptPath.display()
297 );
298 crate::dev_log!("cocoon", "bootstrap script: {}", ScriptPath.display());
299
300 if let Some(BootstrapDirectory) = ScriptPath.parent() {
309 let ProbePath = BootstrapDirectory.join("../..").join(COCOON_BUNDLE_PROBE);
310 if !ProbePath.exists() {
311 return Err(CommonError::IPCError {
312 Description:format!(
313 "Cocoon bundle is missing at {}. Run `pnpm run prepublishOnly --filter=@codeeditorland/cocoon` \
314 (or the full `./Maintain/Debug/Build.sh --profile debug-electron`) before launching - node will \
315 fail to import without it and Mountain will fall into degraded mode with zero extensions \
316 available. Root cause is typically an esbuild failure in an upstream Cocoon source file or a \
317 stale `rm -rf Element/Cocoon/Target` without a rebuild.",
318 ProbePath.display()
319 ),
320 });
321 }
322 dev_log!("cocoon", "[CocoonManagement] pre-flight OK: bundle at {}", ProbePath.display());
323 }
324
325 SweepStaleCocoon(COCOON_GRPC_PORT);
335
336 let ResolvedNodeBinary = NodeResolver::ResolveNodeBinary::Fn(&ApplicationHandle);
340
341 let mut NodeCommand = Command::new(&ResolvedNodeBinary.Path);
343
344 let mut EnvironmentVariables = HashMap::new();
345
346 EnvironmentVariables.insert("VSCODE_PIPE_LOGGING".to_string(), "true".to_string());
348 EnvironmentVariables.insert("VSCODE_VERBOSE_LOGGING".to_string(), "true".to_string());
349 EnvironmentVariables.insert("VSCODE_PARENT_PID".to_string(), std::process::id().to_string());
350
351 EnvironmentVariables.insert("MOUNTAIN_GRPC_PORT".to_string(), MOUNTAIN_GRPC_PORT.to_string());
353 EnvironmentVariables.insert("COCOON_GRPC_PORT".to_string(), COCOON_GRPC_PORT.to_string());
354
355 if let Ok(Path) = std::env::var("PATH") {
357 EnvironmentVariables.insert("PATH".to_string(), Path);
358 }
359 if let Ok(Home) = std::env::var("HOME") {
360 EnvironmentVariables.insert("HOME".to_string(), Home);
361 }
362
363 const LandEnvAllowList:&[&str] = &[
381 "Authorize",
382 "Beam",
383 "Report",
384 "Brand",
385 "Replay",
386 "Ask",
387 "Throttle",
388 "Buffer",
389 "Batch",
390 "Cap",
391 "Pick",
392 "Require",
393 "Lodge",
394 "Extend",
395 "Probe",
396 "Ship",
397 "Wire",
398 "Install",
399 "Mute",
400 "Skip",
401 "Spawn",
402 "Render",
403 "Walk",
404 "Trace",
405 "Record",
406 "Profile",
407 "Diagnose",
408 "Resolve",
409 "Open",
410 "Warn",
411 "Catch",
412 "Source",
413 "Track",
414 "Defer",
415 "Boot",
416 "Pack",
417 ];
418 for (Key, Value) in std::env::vars() {
419 if Key.starts_with("Product")
420 || Key.starts_with("Tier")
421 || Key.starts_with("Network")
422 || LandEnvAllowList.contains(&Key.as_str())
423 {
424 EnvironmentVariables.insert(Key, Value);
425 }
426 }
427
428 for Key in ["NODE_ENV", "TAURI_ENV_DEBUG"] {
434 if let Ok(Value) = std::env::var(Key) {
435 EnvironmentVariables.insert(Key.to_string(), Value);
436 }
437 }
438
439 NodeCommand
440 .arg(&ScriptPath)
441 .env_clear()
442 .envs(EnvironmentVariables)
443 .stdin(Stdio::piped())
444 .stdout(Stdio::piped())
445 .stderr(Stdio::piped());
446
447 let mut ChildProcess = NodeCommand.spawn().map_err(|Error| {
449 CommonError::IPCError {
450 Description:format!(
451 "Failed to spawn Cocoon with node={} (source={}): {}. Override with Pick=/absolute/path or install \
452 Node.js.",
453 ResolvedNodeBinary.Path.display(),
454 ResolvedNodeBinary.Source.AsLabel(),
455 Error
456 ),
457 }
458 })?;
459
460 let ProcessId = ChildProcess.id().unwrap_or(0);
461 COCOON_PID.store(ProcessId, std::sync::atomic::Ordering::Relaxed);
462 dev_log!("cocoon", "[CocoonManagement] Cocoon process spawned [PID: {}]", ProcessId);
463 crate::dev_log!("cocoon", "spawned PID={}", ProcessId);
464
465 if let Some(stdout) = ChildProcess.stdout.take() {
477 tokio::spawn(async move {
478 let Reader = BufReader::new(stdout);
479 let mut Lines = Reader.lines();
480
481 while let Ok(Some(Line)) = Lines.next_line().await {
482 if let Some(ForwardedTag) = ExtractDevTag(&Line) {
483 match ForwardedTag.as_str() {
488 "bootstrap-stage" => dev_log!("bootstrap-stage", "[Cocoon stdout] {}", Line),
489 "ext-activate" => dev_log!("ext-activate", "[Cocoon stdout] {}", Line),
490 "config-prime" => dev_log!("config-prime", "[Cocoon stdout] {}", Line),
491 "breaker" => dev_log!("breaker", "[Cocoon stdout] {}", Line),
492 _ => dev_log!("cocoon", "[Cocoon stdout] {}", Line),
493 }
494 } else {
495 dev_log!("cocoon", "[Cocoon stdout] {}", Line);
496 }
497 }
498 });
499 }
500
501 if let Some(stderr) = ChildProcess.stderr.take() {
524 tokio::spawn(async move {
525 let Reader = BufReader::new(stderr);
526 let mut Lines = Reader.lines();
527 let mut SuppressStackFrames = false;
528
529 while let Ok(Some(Line)) = Lines.next_line().await {
530 let Trimmed = Line.trim_start();
531 let IsStackFrame = Trimmed.starts_with("at ")
532 || Trimmed.starts_with("code: '")
533 || Trimmed == "}"
534 || Trimmed.is_empty();
535 if SuppressStackFrames && IsStackFrame {
536 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
537 continue;
538 }
539 SuppressStackFrames = false;
542
543 let IsBenignSingleLine = Line.contains(": is already signed")
544 || Line.contains(": replacing existing signature")
545 || Line.contains("DeprecationWarning:")
546 || Line.contains("--trace-deprecation")
547 || Line.contains("--trace-warnings");
548 let IsBenignStackHead = Line.contains("EntryNotFound (FileSystemError):")
549 || Line.contains("FileNotFound (FileSystemError):")
550 || Line.contains("[LandFix:UnhandledRejection]")
551 || Line.starts_with("[Patcher] unhandledRejection:")
552 || Line.starts_with("[Patcher] uncaughtException:");
553 if IsBenignStackHead {
554 SuppressStackFrames = true;
555 }
556 if IsBenignSingleLine || IsBenignStackHead {
557 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
558 } else {
559 dev_log!("cocoon", "warn: [Cocoon stderr] {}", Line);
560 }
561 }
562 });
563 }
564
565 let GRPCAddress = format!("127.0.0.1:{}", COCOON_GRPC_PORT);
584 dev_log!(
585 "cocoon",
586 "[CocoonManagement] Connecting to Cocoon gRPC at {} (exponential backoff, budget={}ms)...",
587 GRPCAddress,
588 GRPC_CONNECT_BUDGET_MS
589 );
590
591 let ConnectStart = tokio::time::Instant::now();
592 let mut CurrentDelayMs:u64 = GRPC_CONNECT_INITIAL_MS;
593 let mut ConnectAttempt = 0u32;
594
595 loop {
596 ConnectAttempt += 1;
597 crate::dev_log!(
598 "grpc",
599 "connecting to Cocoon at {} (attempt {}, elapsed={}ms)",
600 GRPCAddress,
601 ConnectAttempt,
602 ConnectStart.elapsed().as_millis()
603 );
604
605 match Vine::Client::ConnectToSideCar::Fn(SideCarIdentifier.clone(), GRPCAddress.clone()).await {
606 Ok(()) => {
607 crate::dev_log!(
608 "grpc",
609 "connected to Cocoon on attempt {} (elapsed={}ms)",
610 ConnectAttempt,
611 ConnectStart.elapsed().as_millis()
612 );
613 break;
614 },
615 Err(Error) => {
616 match ChildProcess.try_wait() {
622 Ok(Some(ExitStatus)) => {
623 let ExitCode = ExitStatus.code().unwrap_or(-1);
624 crate::dev_log!(
625 "grpc",
626 "attempt {} aborted: Cocoon Node process exited with code={} after {}ms - stderr above \
627 (if any) explains why",
628 ConnectAttempt,
629 ExitCode,
630 ConnectStart.elapsed().as_millis()
631 );
632 return Err(CommonError::IPCError {
633 Description:format!(
634 "Cocoon spawned but exited with code {} before Mountain could connect. See \
635 `[DEV:COCOON] warn: [Cocoon stderr] …` lines above for the Node-side error - \
636 typically a missing bundle (\"Cannot find module …\") or an ESM/CJS import drift \
637 after a partial build.",
638 ExitCode
639 ),
640 });
641 },
642 Ok(None) => { },
643 Err(WaitErr) => {
644 crate::dev_log!("grpc", "warn: try_wait on Cocoon child failed: {} (continuing)", WaitErr);
649 },
650 }
651
652 let Elapsed = ConnectStart.elapsed().as_millis() as u64;
653 if Elapsed >= GRPC_CONNECT_BUDGET_MS {
654 crate::dev_log!(
655 "grpc",
656 "attempt {} timed out (budget {}ms exhausted): {}",
657 ConnectAttempt,
658 GRPC_CONNECT_BUDGET_MS,
659 Error
660 );
661 return Err(CommonError::IPCError {
662 Description:format!(
663 "Failed to connect to Cocoon gRPC at {} after {} attempts over {}ms: {} (is Cocoon \
664 running? check `[DEV:COCOON]` log lines for stderr, or re-run with the debug-electron \
665 build profile if the bundle is stale)",
666 GRPCAddress, ConnectAttempt, GRPC_CONNECT_BUDGET_MS, Error
667 ),
668 });
669 }
670
671 crate::dev_log!(
672 "grpc",
673 "attempt {} pending (Cocoon still booting): {}, backing off {}ms",
674 ConnectAttempt,
675 Error,
676 CurrentDelayMs
677 );
678
679 sleep(Duration::from_millis(CurrentDelayMs)).await;
680 CurrentDelayMs = (CurrentDelayMs * 2).min(GRPC_CONNECT_MAX_DELAY_MS);
684 },
685 }
686 }
687
688 dev_log!(
689 "cocoon",
690 "[CocoonManagement] Connected to Cocoon. Sending initialization data..."
691 );
692
693 sleep(Duration::from_millis(200)).await;
696
697 let MainInitializationData = InitializationData::ConstructExtensionHostInitializationData(&Environment)
699 .await
700 .map_err(|Error| {
701 CommonError::IPCError { Description:format!("Failed to construct initialization data: {}", Error) }
702 })?;
703
704 let Response = Vine::Client::SendRequest::Fn(
706 &SideCarIdentifier,
707 "InitializeExtensionHost".to_string(),
708 MainInitializationData,
709 HANDSHAKE_TIMEOUT_MS,
710 )
711 .await
712 .map_err(|Error| {
713 CommonError::IPCError {
714 Description:format!("Failed to send initialization request to Cocoon: {}", Error),
715 }
716 })?;
717
718 match Response.as_str() {
720 Some("initialized") => {
721 dev_log!(
722 "cocoon",
723 "[CocoonManagement] Cocoon handshake complete. Extension host is ready."
724 );
725 },
726 Some(other) => {
727 return Err(CommonError::IPCError {
728 Description:format!("Cocoon initialization failed with unexpected response: {}", other),
729 });
730 },
731 None => {
732 return Err(CommonError::IPCError {
733 Description:"Cocoon initialization failed: no response received".to_string(),
734 });
735 },
736 }
737
738 let SideCarId = SideCarIdentifier.clone();
754 let EnvironmentForActivation = Environment.clone();
755 tokio::spawn(async move {
756 sleep(Duration::from_millis(500)).await;
758
759 crate::dev_log!("exthost", "Sending $activateByEvent(\"*\") to Cocoon");
760
761 if let Err(Error) = Vine::Client::SendRequest::Fn(
762 &SideCarId,
763 "$activateByEvent".to_string(),
764 serde_json::json!({ "activationEvent": "*" }),
765 30_000,
766 )
767 .await
768 {
769 dev_log!("cocoon", "warn: [CocoonManagement] $activateByEvent(\"*\") failed: {}", Error);
770 return;
771 }
772 dev_log!("cocoon", "[CocoonManagement] Startup extensions activation (*) triggered");
773
774 let WorkspacePatterns = {
783 let AppState = &EnvironmentForActivation.ApplicationState;
784 let Folders:Vec<std::path::PathBuf> = AppState
785 .Workspace
786 .WorkspaceFolders
787 .lock()
788 .ok()
789 .map(|Guard| {
790 Guard
791 .iter()
792 .filter_map(|Folder| Folder.URI.to_file_path().ok())
793 .collect::<Vec<_>>()
794 })
795 .unwrap_or_default();
796
797 let Patterns:Vec<String> = AppState
798 .Extension
799 .ScannedExtensions
800 .ScannedExtensions
801 .lock()
802 .ok()
803 .map(|Guard| {
804 let mut Set:std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
805 for Description in Guard.values() {
806 if let Some(Events) = &Description.ActivationEvents {
807 for Event in Events {
808 if let Some(Pattern) = Event.strip_prefix("workspaceContains:") {
809 Set.insert(Pattern.to_string());
810 }
811 }
812 }
813 }
814 Set.into_iter().collect()
815 })
816 .unwrap_or_default();
817
818 (Folders, Patterns)
819 };
820
821 let (WorkspaceFolders, Patterns):(Vec<std::path::PathBuf>, Vec<String>) = WorkspacePatterns;
822 if !WorkspaceFolders.is_empty() && !Patterns.is_empty() {
823 let Matched = FindMatchingWorkspaceContainsPatterns(&WorkspaceFolders, &Patterns);
824 dev_log!(
825 "exthost",
826 "[CocoonManagement] workspaceContains scan: {} pattern(s) matched across {} folder(s)",
827 Matched.len(),
828 WorkspaceFolders.len()
829 );
830 for Pattern in Matched {
831 let Event = format!("workspaceContains:{}", Pattern);
832 if let Err(Error) = Vine::Client::SendRequest::Fn(
833 &SideCarId,
834 "$activateByEvent".to_string(),
835 serde_json::json!({ "activationEvent": Event }),
836 30_000,
837 )
838 .await
839 {
840 dev_log!(
841 "cocoon",
842 "warn: [CocoonManagement] $activateByEvent({}) failed: {}",
843 Event,
844 Error
845 );
846 }
847 }
848 }
849
850 sleep(Duration::from_millis(2_000)).await;
854 if let Err(Error) = Vine::Client::SendRequest::Fn(
855 &SideCarId,
856 "$activateByEvent".to_string(),
857 serde_json::json!({ "activationEvent": "onStartupFinished" }),
858 30_000,
859 )
860 .await
861 {
862 dev_log!(
863 "cocoon",
864 "warn: [CocoonManagement] $activateByEvent(onStartupFinished) failed: {}",
865 Error
866 );
867 } else {
868 dev_log!("cocoon", "[CocoonManagement] onStartupFinished activation triggered");
869 }
870 });
871
872 {
874 let mut state = COCOON_STATE.lock().await;
875 state.ChildProcess = Some(ChildProcess);
876 state.IsRunning = true;
877 state.StartTime = Some(tokio::time::Instant::now());
878 dev_log!("cocoon", "[CocoonManagement] Process state updated: Running");
879 }
880
881 {
883 let mut health = COCOON_HEALTH.lock().await;
884 health.ClearIssues();
885 dev_log!("cocoon", "[CocoonManagement] Health monitor reset to active state");
886 }
887
888 let state_clone = Arc::clone(&COCOON_STATE);
890 tokio::spawn(monitor_cocoon_health_task(state_clone));
891 dev_log!("cocoon", "[CocoonManagement] Background health monitoring started");
892
893 Ok(())
894}
895
896async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
903 loop {
904 tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
905
906 let mut state_guard = state.lock().await;
907
908 if state_guard.ChildProcess.is_some() {
910 let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
912
913 let exit_status = {
915 let child = state_guard.ChildProcess.as_mut().unwrap();
916 child.try_wait()
917 };
918
919 match exit_status {
920 Ok(Some(exit_code)) => {
921 let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
923 let exit_code_num = exit_code.code().unwrap_or(-1);
924 dev_log!(
925 "cocoon",
926 "warn: [CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
927 process_id.unwrap_or(0),
928 exit_code_num,
929 uptime
930 );
931
932 state_guard.IsRunning = false;
934 state_guard.ChildProcess = None;
935 COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
936
937 {
939 let mut health = COCOON_HEALTH.lock().await;
940 health.AddIssue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
941 dev_log!("cocoon", "warn: [CocoonHealth] Health score: {}", health.HealthScore);
942 }
943
944 dev_log!(
946 "cocoon",
947 "warn: [CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted \
948 manually or via application reinitialization"
949 );
950 },
951 Ok(None) => {
952 dev_log!(
954 "cocoon",
955 "[CocoonHealth] Cocoon process is healthy [PID: {}]",
956 process_id.unwrap_or(0)
957 );
958 },
959 Err(e) => {
960 dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
962
963 {
965 let mut health = COCOON_HEALTH.lock().await;
966 health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
967 }
968 },
969 }
970 } else {
971 dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
977 drop(state_guard);
978 return;
979 }
980 }
981}
982
983pub async fn HardKillCocoon() {
993 let mut State = COCOON_STATE.lock().await;
994 if let Some(mut Child) = State.ChildProcess.take() {
995 let Pid = Child.id().unwrap_or(0);
996 match Child.try_wait() {
997 Ok(Some(_Status)) => {
998 dev_log!("cocoon", "[CocoonShutdown] Child PID {} already exited; clearing handle.", Pid);
999 },
1000 Ok(None) => {
1001 dev_log!(
1002 "cocoon",
1003 "[CocoonShutdown] Child PID {} still alive after $shutdown; sending SIGKILL.",
1004 Pid
1005 );
1006 if let Err(Error) = Child.start_kill() {
1007 dev_log!("cocoon", "warn: [CocoonShutdown] start_kill failed on PID {}: {}", Pid, Error);
1008 }
1009 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), Child.wait()).await;
1011 },
1012 Err(Error) => {
1013 dev_log!("cocoon", "warn: [CocoonShutdown] try_wait failed on PID {}: {}", Pid, Error);
1014 },
1015 }
1016 }
1017 State.IsRunning = false;
1018}
1019
1020fn SweepStaleCocoon(Port:u16) {
1036 use std::{net::TcpStream, time::Duration};
1037
1038 let Addr = format!("127.0.0.1:{}", Port);
1039
1040 let Probe =
1043 TcpStream::connect_timeout(&Addr.parse().expect("valid socket addr literal"), Duration::from_millis(200));
1044 if Probe.is_err() {
1045 dev_log!("cocoon", "[CocoonSweep] Port {} is clean (no prior listener).", Port);
1046 return;
1047 }
1048
1049 dev_log!(
1050 "cocoon",
1051 "[CocoonSweep] Port {} has a listener - attempting to resolve owner via lsof.",
1052 Port
1053 );
1054
1055 let LsofOutput = std::process::Command::new("lsof")
1057 .args(["-nP", &format!("-iTCP:{}", Port), "-sTCP:LISTEN", "-t"])
1058 .output();
1059
1060 let Output = match LsofOutput {
1061 Ok(O) => O,
1062 Err(Error) => {
1063 dev_log!(
1064 "cocoon",
1065 "warn: [CocoonSweep] lsof unavailable ({}). Skipping sweep; Cocoon spawn may fail with EADDRINUSE.",
1066 Error
1067 );
1068 return;
1069 },
1070 };
1071
1072 if !Output.status.success() {
1073 dev_log!("cocoon", "warn: [CocoonSweep] lsof exited non-zero. Skipping sweep.");
1074 return;
1075 }
1076
1077 let Stdout = String::from_utf8_lossy(&Output.stdout);
1078 let Pids:Vec<i32> = Stdout.lines().filter_map(|L| L.trim().parse::<i32>().ok()).collect();
1079
1080 if Pids.is_empty() {
1081 dev_log!(
1082 "cocoon",
1083 "warn: [CocoonSweep] Port {} answered but lsof found no LISTEN PID - giving up.",
1084 Port
1085 );
1086 return;
1087 }
1088
1089 let SelfPid = std::process::id() as i32;
1092 for Pid in Pids {
1093 if Pid == SelfPid {
1094 dev_log!(
1095 "cocoon",
1096 "warn: [CocoonSweep] Port {} owned by Mountain itself (PID {}); refusing to kill.",
1097 Port,
1098 Pid
1099 );
1100 continue;
1101 }
1102 dev_log!("cocoon", "[CocoonSweep] Killing stale PID {} (SIGTERM).", Pid);
1103 let _ = std::process::Command::new("kill").arg(Pid.to_string()).status();
1104 std::thread::sleep(Duration::from_millis(500));
1105 let StillAlive = std::process::Command::new("kill")
1107 .args(["-0", &Pid.to_string()])
1108 .status()
1109 .map(|S| S.success())
1110 .unwrap_or(false);
1111 if StillAlive {
1112 dev_log!("cocoon", "warn: [CocoonSweep] PID {} survived SIGTERM; sending SIGKILL.", Pid);
1113 let _ = std::process::Command::new("kill").args(["-9", &Pid.to_string()]).status();
1114 std::thread::sleep(Duration::from_millis(200));
1115 }
1116 dev_log!("cocoon", "[CocoonSweep] PID {} reaped.", Pid);
1117 }
1118}
1119
1120fn FindMatchingWorkspaceContainsPatterns(Folders:&[std::path::PathBuf], Patterns:&[String]) -> Vec<String> {
1137 use std::collections::HashSet;
1138
1139 const MAX_DEPTH:usize = 3;
1140 const MAX_ENTRIES_PER_ROOT:usize = 4096;
1141
1142 let mut Matched:HashSet<String> = HashSet::new();
1143 for Folder in Folders {
1144 if !Folder.is_dir() {
1145 continue;
1146 }
1147 let mut Entries:Vec<String> = Vec::new();
1149 let mut Stack:Vec<(std::path::PathBuf, usize)> = vec![(Folder.clone(), 0)];
1150 while let Some((Current, Depth)) = Stack.pop() {
1151 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1152 break;
1153 }
1154 let ReadDirResult = std::fs::read_dir(&Current);
1155 let ReadDir = match ReadDirResult {
1156 Ok(R) => R,
1157 Err(_) => continue,
1158 };
1159 for Entry in ReadDir.flatten() {
1160 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1161 break;
1162 }
1163 let Path = Entry.path();
1164 let Relative = match Path.strip_prefix(Folder) {
1165 Ok(R) => R.to_string_lossy().replace('\\', "/"),
1166 Err(_) => continue,
1167 };
1168 let IsDir = Entry.file_type().map(|T| T.is_dir()).unwrap_or(false);
1169 Entries.push(Relative.clone());
1170 if IsDir && Depth + 1 < MAX_DEPTH {
1171 Stack.push((Path, Depth + 1));
1172 }
1173 }
1174 }
1175
1176 for Pattern in Patterns {
1177 if Matched.contains(Pattern) {
1178 continue;
1179 }
1180 if PatternMatchesAnyEntry(Pattern, &Entries) {
1181 Matched.insert(Pattern.clone());
1182 }
1183 }
1184 }
1185 Matched.into_iter().collect()
1186}
1187
1188fn PatternMatchesAnyEntry(Pattern:&str, Entries:&[String]) -> bool {
1192 let HasWildcard = Pattern.contains('*') || Pattern.contains('?');
1193 if !HasWildcard {
1194 return Entries.iter().any(|E| E == Pattern);
1195 }
1196 let PatternSegments:Vec<&str> = Pattern.split('/').collect();
1197 Entries
1198 .iter()
1199 .any(|E| SegmentMatch(&PatternSegments, &E.split('/').collect::<Vec<_>>()))
1200}
1201
1202fn SegmentMatch(Pattern:&[&str], Entry:&[&str]) -> bool {
1203 if Pattern.is_empty() {
1204 return Entry.is_empty();
1205 }
1206 let Head = Pattern[0];
1207 if Head == "**" {
1208 for Consumed in 0..=Entry.len() {
1210 if SegmentMatch(&Pattern[1..], &Entry[Consumed..]) {
1211 return true;
1212 }
1213 }
1214 return false;
1215 }
1216 if Entry.is_empty() {
1217 return false;
1218 }
1219 if SingleSegmentMatch(Head, Entry[0]) {
1220 return SegmentMatch(&Pattern[1..], &Entry[1..]);
1221 }
1222 false
1223}
1224
1225fn SingleSegmentMatch(Pattern:&str, Segment:&str) -> bool {
1226 if Pattern == "*" {
1227 return true;
1228 }
1229 if !Pattern.contains('*') && !Pattern.contains('?') {
1230 return Pattern == Segment;
1231 }
1232 let Fragments:Vec<&str> = Pattern.split('*').collect();
1237 let mut Cursor = 0usize;
1238 for (Index, Fragment) in Fragments.iter().enumerate() {
1239 if Fragment.is_empty() {
1240 continue;
1241 }
1242 if Index == 0 {
1243 if !Segment[Cursor..].starts_with(Fragment) {
1244 return false;
1245 }
1246 Cursor += Fragment.len();
1247 continue;
1248 }
1249 match Segment[Cursor..].find(Fragment) {
1250 Some(Offset) => Cursor += Offset + Fragment.len(),
1251 None => return false,
1252 }
1253 }
1254 if let Some(Last) = Fragments.last()
1255 && !Last.is_empty()
1256 {
1257 return Segment.ends_with(Last);
1258 }
1259 true
1260}