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/Cocoon/Main.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 "Capture",
392 "OTLPEndpoint",
393 "OTLPEnabled",
394 "Pick",
395 "Require",
396 "Lodge",
397 "Extend",
398 "Probe",
399 "Ship",
400 "Wire",
401 "Install",
402 "Mute",
403 "Skip",
404 "Spawn",
405 "Render",
406 "Walk",
407 "Trace",
408 "Record",
409 "Profile",
410 "Diagnose",
411 "Resolve",
412 "Open",
413 "Warn",
414 "Catch",
415 "Source",
416 "Track",
417 "Defer",
418 "Boot",
419 "Pack",
420 ];
421 for (Key, Value) in std::env::vars() {
422 if Key.starts_with("Product")
423 || Key.starts_with("Tier")
424 || Key.starts_with("Network")
425 || LandEnvAllowList.contains(&Key.as_str())
426 {
427 EnvironmentVariables.insert(Key, Value);
428 }
429 }
430
431 for Key in ["NODE_ENV", "TAURI_ENV_DEBUG"] {
437 if let Ok(Value) = std::env::var(Key) {
438 EnvironmentVariables.insert(Key.to_string(), Value);
439 }
440 }
441
442 NodeCommand
443 .arg(&ScriptPath)
444 .env_clear()
445 .envs(EnvironmentVariables)
446 .stdin(Stdio::piped())
447 .stdout(Stdio::piped())
448 .stderr(Stdio::piped());
449
450 let mut ChildProcess = NodeCommand.spawn().map_err(|Error| {
452 CommonError::IPCError {
453 Description:format!(
454 "Failed to spawn Cocoon with node={} (source={}): {}. Override with Pick=/absolute/path or install \
455 Node.js.",
456 ResolvedNodeBinary.Path.display(),
457 ResolvedNodeBinary.Source.AsLabel(),
458 Error
459 ),
460 }
461 })?;
462
463 let ProcessId = ChildProcess.id().unwrap_or(0);
464 COCOON_PID.store(ProcessId, std::sync::atomic::Ordering::Relaxed);
465 dev_log!("cocoon", "[CocoonManagement] Cocoon process spawned [PID: {}]", ProcessId);
466 crate::dev_log!("cocoon", "spawned PID={}", ProcessId);
467
468 if let Some(stdout) = ChildProcess.stdout.take() {
480 tokio::spawn(async move {
481 let Reader = BufReader::new(stdout);
482 let mut Lines = Reader.lines();
483
484 while let Ok(Some(Line)) = Lines.next_line().await {
485 if let Some(ForwardedTag) = ExtractDevTag(&Line) {
486 match ForwardedTag.as_str() {
491 "bootstrap-stage" => dev_log!("bootstrap-stage", "[Cocoon stdout] {}", Line),
492 "ext-activate" => dev_log!("ext-activate", "[Cocoon stdout] {}", Line),
493 "config-prime" => dev_log!("config-prime", "[Cocoon stdout] {}", Line),
494 "breaker" => dev_log!("breaker", "[Cocoon stdout] {}", Line),
495 _ => dev_log!("cocoon", "[Cocoon stdout] {}", Line),
496 }
497 } else {
498 dev_log!("cocoon", "[Cocoon stdout] {}", Line);
499 }
500 }
501 });
502 }
503
504 if let Some(stderr) = ChildProcess.stderr.take() {
527 tokio::spawn(async move {
528 let Reader = BufReader::new(stderr);
529 let mut Lines = Reader.lines();
530 let mut SuppressStackFrames = false;
531
532 while let Ok(Some(Line)) = Lines.next_line().await {
533 let Trimmed = Line.trim_start();
534 let IsStackFrame = Trimmed.starts_with("at ")
535 || Trimmed.starts_with("code: '")
536 || Trimmed == "}"
537 || Trimmed.is_empty();
538 if SuppressStackFrames && IsStackFrame {
539 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
540 continue;
541 }
542 SuppressStackFrames = false;
545
546 let IsBenignSingleLine = Line.contains(": is already signed")
547 || Line.contains(": replacing existing signature")
548 || Line.contains("DeprecationWarning:")
549 || Line.contains("--trace-deprecation")
550 || Line.contains("--trace-warnings");
551 let IsBenignStackHead = Line.contains("EntryNotFound (FileSystemError):")
552 || Line.contains("FileNotFound (FileSystemError):")
553 || Line.contains("[LandFix:UnhandledRejection]")
554 || Line.starts_with("[Patcher] unhandledRejection:")
555 || Line.starts_with("[Patcher] uncaughtException:");
556 if IsBenignStackHead {
557 SuppressStackFrames = true;
558 }
559 if IsBenignSingleLine || IsBenignStackHead {
560 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
561 } else {
562 dev_log!("cocoon", "warn: [Cocoon stderr] {}", Line);
563 }
564 }
565 });
566 }
567
568 let GRPCAddress = format!("127.0.0.1:{}", COCOON_GRPC_PORT);
587 dev_log!(
588 "cocoon",
589 "[CocoonManagement] Connecting to Cocoon gRPC at {} (exponential backoff, budget={}ms)...",
590 GRPCAddress,
591 GRPC_CONNECT_BUDGET_MS
592 );
593
594 let ConnectStart = tokio::time::Instant::now();
595 let mut CurrentDelayMs:u64 = GRPC_CONNECT_INITIAL_MS;
596 let mut ConnectAttempt = 0u32;
597
598 loop {
599 ConnectAttempt += 1;
600 crate::dev_log!(
601 "grpc",
602 "connecting to Cocoon at {} (attempt {}, elapsed={}ms)",
603 GRPCAddress,
604 ConnectAttempt,
605 ConnectStart.elapsed().as_millis()
606 );
607
608 match Vine::Client::ConnectToSideCar::Fn(SideCarIdentifier.clone(), GRPCAddress.clone()).await {
609 Ok(()) => {
610 crate::dev_log!(
611 "grpc",
612 "connected to Cocoon on attempt {} (elapsed={}ms)",
613 ConnectAttempt,
614 ConnectStart.elapsed().as_millis()
615 );
616 break;
617 },
618 Err(Error) => {
619 match ChildProcess.try_wait() {
625 Ok(Some(ExitStatus)) => {
626 let ExitCode = ExitStatus.code().unwrap_or(-1);
627 crate::dev_log!(
628 "grpc",
629 "attempt {} aborted: Cocoon Node process exited with code={} after {}ms - stderr above \
630 (if any) explains why",
631 ConnectAttempt,
632 ExitCode,
633 ConnectStart.elapsed().as_millis()
634 );
635 return Err(CommonError::IPCError {
636 Description:format!(
637 "Cocoon spawned but exited with code {} before Mountain could connect. See \
638 `[DEV:COCOON] warn: [Cocoon stderr] …` lines above for the Node-side error - \
639 typically a missing bundle (\"Cannot find module …\") or an ESM/CJS import drift \
640 after a partial build.",
641 ExitCode
642 ),
643 });
644 },
645 Ok(None) => { },
646 Err(WaitErr) => {
647 crate::dev_log!("grpc", "warn: try_wait on Cocoon child failed: {} (continuing)", WaitErr);
652 },
653 }
654
655 let Elapsed = ConnectStart.elapsed().as_millis() as u64;
656 if Elapsed >= GRPC_CONNECT_BUDGET_MS {
657 crate::dev_log!(
658 "grpc",
659 "attempt {} timed out (budget {}ms exhausted): {}",
660 ConnectAttempt,
661 GRPC_CONNECT_BUDGET_MS,
662 Error
663 );
664 return Err(CommonError::IPCError {
665 Description:format!(
666 "Failed to connect to Cocoon gRPC at {} after {} attempts over {}ms: {} (is Cocoon \
667 running? check `[DEV:COCOON]` log lines for stderr, or re-run with the debug-electron \
668 build profile if the bundle is stale)",
669 GRPCAddress, ConnectAttempt, GRPC_CONNECT_BUDGET_MS, Error
670 ),
671 });
672 }
673
674 crate::dev_log!(
675 "grpc",
676 "attempt {} pending (Cocoon still booting): {}, backing off {}ms",
677 ConnectAttempt,
678 Error,
679 CurrentDelayMs
680 );
681
682 sleep(Duration::from_millis(CurrentDelayMs)).await;
683 CurrentDelayMs = (CurrentDelayMs * 2).min(GRPC_CONNECT_MAX_DELAY_MS);
687 },
688 }
689 }
690
691 dev_log!(
692 "cocoon",
693 "[CocoonManagement] Connected to Cocoon. Sending initialization data..."
694 );
695
696 sleep(Duration::from_millis(200)).await;
699
700 let MainInitializationData = InitializationData::ConstructExtensionHostInitializationData(&Environment)
702 .await
703 .map_err(|Error| {
704 CommonError::IPCError { Description:format!("Failed to construct initialization data: {}", Error) }
705 })?;
706
707 let Response = Vine::Client::SendRequest::Fn(
709 &SideCarIdentifier,
710 "InitializeExtensionHost".to_string(),
711 MainInitializationData,
712 HANDSHAKE_TIMEOUT_MS,
713 )
714 .await
715 .map_err(|Error| {
716 CommonError::IPCError {
717 Description:format!("Failed to send initialization request to Cocoon: {}", Error),
718 }
719 })?;
720
721 match Response.as_str() {
723 Some("initialized") => {
724 dev_log!(
725 "cocoon",
726 "[CocoonManagement] Cocoon handshake complete. Extension host is ready."
727 );
728 },
729 Some(other) => {
730 return Err(CommonError::IPCError {
731 Description:format!("Cocoon initialization failed with unexpected response: {}", other),
732 });
733 },
734 None => {
735 return Err(CommonError::IPCError {
736 Description:"Cocoon initialization failed: no response received".to_string(),
737 });
738 },
739 }
740
741 let SideCarId = SideCarIdentifier.clone();
757 let EnvironmentForActivation = Environment.clone();
758 tokio::spawn(async move {
759 sleep(Duration::from_millis(500)).await;
761
762 crate::dev_log!("exthost", "Sending $activateByEvent(\"*\") to Cocoon");
763
764 if let Err(Error) = Vine::Client::SendRequest::Fn(
765 &SideCarId,
766 "$activateByEvent".to_string(),
767 serde_json::json!({ "activationEvent": "*" }),
768 30_000,
769 )
770 .await
771 {
772 dev_log!("cocoon", "warn: [CocoonManagement] $activateByEvent(\"*\") failed: {}", Error);
773 return;
774 }
775 dev_log!("cocoon", "[CocoonManagement] Startup extensions activation (*) triggered");
776
777 let WorkspacePatterns = {
786 let AppState = &EnvironmentForActivation.ApplicationState;
787 let Folders:Vec<std::path::PathBuf> = AppState
788 .Workspace
789 .WorkspaceFolders
790 .lock()
791 .ok()
792 .map(|Guard| {
793 Guard
794 .iter()
795 .filter_map(|Folder| Folder.URI.to_file_path().ok())
796 .collect::<Vec<_>>()
797 })
798 .unwrap_or_default();
799
800 let Patterns:Vec<String> = AppState
801 .Extension
802 .ScannedExtensions
803 .ScannedExtensions
804 .lock()
805 .ok()
806 .map(|Guard| {
807 let mut Set:std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
808 for Description in Guard.values() {
809 if let Some(Events) = &Description.ActivationEvents {
810 for Event in Events {
811 if let Some(Pattern) = Event.strip_prefix("workspaceContains:") {
812 Set.insert(Pattern.to_string());
813 }
814 }
815 }
816 }
817 Set.into_iter().collect()
818 })
819 .unwrap_or_default();
820
821 (Folders, Patterns)
822 };
823
824 let (WorkspaceFolders, Patterns):(Vec<std::path::PathBuf>, Vec<String>) = WorkspacePatterns;
825 if !WorkspaceFolders.is_empty() && !Patterns.is_empty() {
826 let Matched = FindMatchingWorkspaceContainsPatterns(&WorkspaceFolders, &Patterns);
827 dev_log!(
828 "exthost",
829 "[CocoonManagement] workspaceContains scan: {} pattern(s) matched across {} folder(s)",
830 Matched.len(),
831 WorkspaceFolders.len()
832 );
833 for Pattern in Matched {
834 let Event = format!("workspaceContains:{}", Pattern);
835 if let Err(Error) = Vine::Client::SendRequest::Fn(
836 &SideCarId,
837 "$activateByEvent".to_string(),
838 serde_json::json!({ "activationEvent": Event }),
839 30_000,
840 )
841 .await
842 {
843 dev_log!(
844 "cocoon",
845 "warn: [CocoonManagement] $activateByEvent({}) failed: {}",
846 Event,
847 Error
848 );
849 }
850 }
851 }
852
853 sleep(Duration::from_millis(2_000)).await;
857 if let Err(Error) = Vine::Client::SendRequest::Fn(
858 &SideCarId,
859 "$activateByEvent".to_string(),
860 serde_json::json!({ "activationEvent": "onStartupFinished" }),
861 30_000,
862 )
863 .await
864 {
865 dev_log!(
866 "cocoon",
867 "warn: [CocoonManagement] $activateByEvent(onStartupFinished) failed: {}",
868 Error
869 );
870 } else {
871 dev_log!("cocoon", "[CocoonManagement] onStartupFinished activation triggered");
872 }
873 });
874
875 {
877 let mut state = COCOON_STATE.lock().await;
878 state.ChildProcess = Some(ChildProcess);
879 state.IsRunning = true;
880 state.StartTime = Some(tokio::time::Instant::now());
881 dev_log!("cocoon", "[CocoonManagement] Process state updated: Running");
882 }
883
884 {
886 let mut health = COCOON_HEALTH.lock().await;
887 health.ClearIssues();
888 dev_log!("cocoon", "[CocoonManagement] Health monitor reset to active state");
889 }
890
891 let state_clone = Arc::clone(&COCOON_STATE);
893 tokio::spawn(monitor_cocoon_health_task(state_clone));
894 dev_log!("cocoon", "[CocoonManagement] Background health monitoring started");
895
896 Ok(())
897}
898
899async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
906 loop {
907 tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
908
909 let mut state_guard = state.lock().await;
910
911 if state_guard.ChildProcess.is_some() {
913 let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
915
916 let exit_status = {
918 let child = state_guard.ChildProcess.as_mut().unwrap();
919 child.try_wait()
920 };
921
922 match exit_status {
923 Ok(Some(exit_code)) => {
924 let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
926 let exit_code_num = exit_code.code().unwrap_or(-1);
927 dev_log!(
928 "cocoon",
929 "warn: [CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
930 process_id.unwrap_or(0),
931 exit_code_num,
932 uptime
933 );
934
935 state_guard.IsRunning = false;
937 state_guard.ChildProcess = None;
938 COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
939
940 {
942 let mut health = COCOON_HEALTH.lock().await;
943 health.AddIssue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
944 dev_log!("cocoon", "warn: [CocoonHealth] Health score: {}", health.HealthScore);
945 }
946
947 dev_log!(
949 "cocoon",
950 "warn: [CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted \
951 manually or via application reinitialization"
952 );
953 },
954 Ok(None) => {
955 dev_log!(
957 "cocoon",
958 "[CocoonHealth] Cocoon process is healthy [PID: {}]",
959 process_id.unwrap_or(0)
960 );
961 },
962 Err(e) => {
963 dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
965
966 {
968 let mut health = COCOON_HEALTH.lock().await;
969 health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
970 }
971 },
972 }
973 } else {
974 dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
980 drop(state_guard);
981 return;
982 }
983 }
984}
985
986pub async fn HardKillCocoon() {
996 let mut State = COCOON_STATE.lock().await;
997 if let Some(mut Child) = State.ChildProcess.take() {
998 let Pid = Child.id().unwrap_or(0);
999 match Child.try_wait() {
1000 Ok(Some(_Status)) => {
1001 dev_log!("cocoon", "[CocoonShutdown] Child PID {} already exited; clearing handle.", Pid);
1002 },
1003 Ok(None) => {
1004 dev_log!(
1005 "cocoon",
1006 "[CocoonShutdown] Child PID {} still alive after $shutdown; sending SIGKILL.",
1007 Pid
1008 );
1009 if let Err(Error) = Child.start_kill() {
1010 dev_log!("cocoon", "warn: [CocoonShutdown] start_kill failed on PID {}: {}", Pid, Error);
1011 }
1012 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), Child.wait()).await;
1014 },
1015 Err(Error) => {
1016 dev_log!("cocoon", "warn: [CocoonShutdown] try_wait failed on PID {}: {}", Pid, Error);
1017 },
1018 }
1019 }
1020 State.IsRunning = false;
1021}
1022
1023fn SweepStaleCocoon(Port:u16) {
1039 use std::{net::TcpStream, time::Duration};
1040
1041 let Addr = format!("127.0.0.1:{}", Port);
1042
1043 let Probe =
1046 TcpStream::connect_timeout(&Addr.parse().expect("valid socket addr literal"), Duration::from_millis(200));
1047 if Probe.is_err() {
1048 dev_log!("cocoon", "[CocoonSweep] Port {} is clean (no prior listener).", Port);
1049 return;
1050 }
1051
1052 dev_log!(
1053 "cocoon",
1054 "[CocoonSweep] Port {} has a listener - attempting to resolve owner via lsof.",
1055 Port
1056 );
1057
1058 let LsofOutput = std::process::Command::new("lsof")
1060 .args(["-nP", &format!("-iTCP:{}", Port), "-sTCP:LISTEN", "-t"])
1061 .output();
1062
1063 let Output = match LsofOutput {
1064 Ok(O) => O,
1065 Err(Error) => {
1066 dev_log!(
1067 "cocoon",
1068 "warn: [CocoonSweep] lsof unavailable ({}). Skipping sweep; Cocoon spawn may fail with EADDRINUSE.",
1069 Error
1070 );
1071 return;
1072 },
1073 };
1074
1075 if !Output.status.success() {
1076 dev_log!("cocoon", "warn: [CocoonSweep] lsof exited non-zero. Skipping sweep.");
1077 return;
1078 }
1079
1080 let Stdout = String::from_utf8_lossy(&Output.stdout);
1081 let Pids:Vec<i32> = Stdout.lines().filter_map(|L| L.trim().parse::<i32>().ok()).collect();
1082
1083 if Pids.is_empty() {
1084 dev_log!(
1085 "cocoon",
1086 "warn: [CocoonSweep] Port {} answered but lsof found no LISTEN PID - giving up.",
1087 Port
1088 );
1089 return;
1090 }
1091
1092 let SelfPid = std::process::id() as i32;
1095 for Pid in Pids {
1096 if Pid == SelfPid {
1097 dev_log!(
1098 "cocoon",
1099 "warn: [CocoonSweep] Port {} owned by Mountain itself (PID {}); refusing to kill.",
1100 Port,
1101 Pid
1102 );
1103 continue;
1104 }
1105 dev_log!("cocoon", "[CocoonSweep] Killing stale PID {} (SIGTERM).", Pid);
1106 let _ = std::process::Command::new("kill").arg(Pid.to_string()).status();
1107 std::thread::sleep(Duration::from_millis(500));
1108 let StillAlive = std::process::Command::new("kill")
1110 .args(["-0", &Pid.to_string()])
1111 .status()
1112 .map(|S| S.success())
1113 .unwrap_or(false);
1114 if StillAlive {
1115 dev_log!("cocoon", "warn: [CocoonSweep] PID {} survived SIGTERM; sending SIGKILL.", Pid);
1116 let _ = std::process::Command::new("kill").args(["-9", &Pid.to_string()]).status();
1117 std::thread::sleep(Duration::from_millis(200));
1118 }
1119 dev_log!("cocoon", "[CocoonSweep] PID {} reaped.", Pid);
1120 }
1121}
1122
1123fn FindMatchingWorkspaceContainsPatterns(Folders:&[std::path::PathBuf], Patterns:&[String]) -> Vec<String> {
1140 use std::collections::HashSet;
1141
1142 const MAX_DEPTH:usize = 3;
1143 const MAX_ENTRIES_PER_ROOT:usize = 4096;
1144
1145 let mut Matched:HashSet<String> = HashSet::new();
1146 for Folder in Folders {
1147 if !Folder.is_dir() {
1148 continue;
1149 }
1150 let mut Entries:Vec<String> = Vec::new();
1152 let mut Stack:Vec<(std::path::PathBuf, usize)> = vec![(Folder.clone(), 0)];
1153 while let Some((Current, Depth)) = Stack.pop() {
1154 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1155 break;
1156 }
1157 let ReadDirResult = std::fs::read_dir(&Current);
1158 let ReadDir = match ReadDirResult {
1159 Ok(R) => R,
1160 Err(_) => continue,
1161 };
1162 for Entry in ReadDir.flatten() {
1163 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1164 break;
1165 }
1166 let Path = Entry.path();
1167 let Relative = match Path.strip_prefix(Folder) {
1168 Ok(R) => R.to_string_lossy().replace('\\', "/"),
1169 Err(_) => continue,
1170 };
1171 let IsDir = Entry.file_type().map(|T| T.is_dir()).unwrap_or(false);
1172 Entries.push(Relative.clone());
1173 if IsDir && Depth + 1 < MAX_DEPTH {
1174 Stack.push((Path, Depth + 1));
1175 }
1176 }
1177 }
1178
1179 for Pattern in Patterns {
1180 if Matched.contains(Pattern) {
1181 continue;
1182 }
1183 if PatternMatchesAnyEntry(Pattern, &Entries) {
1184 Matched.insert(Pattern.clone());
1185 }
1186 }
1187 }
1188 Matched.into_iter().collect()
1189}
1190
1191fn PatternMatchesAnyEntry(Pattern:&str, Entries:&[String]) -> bool {
1195 let HasWildcard = Pattern.contains('*') || Pattern.contains('?');
1196 if !HasWildcard {
1197 return Entries.iter().any(|E| E == Pattern);
1198 }
1199 let PatternSegments:Vec<&str> = Pattern.split('/').collect();
1200 Entries
1201 .iter()
1202 .any(|E| SegmentMatch(&PatternSegments, &E.split('/').collect::<Vec<_>>()))
1203}
1204
1205fn SegmentMatch(Pattern:&[&str], Entry:&[&str]) -> bool {
1206 if Pattern.is_empty() {
1207 return Entry.is_empty();
1208 }
1209 let Head = Pattern[0];
1210 if Head == "**" {
1211 for Consumed in 0..=Entry.len() {
1213 if SegmentMatch(&Pattern[1..], &Entry[Consumed..]) {
1214 return true;
1215 }
1216 }
1217 return false;
1218 }
1219 if Entry.is_empty() {
1220 return false;
1221 }
1222 if SingleSegmentMatch(Head, Entry[0]) {
1223 return SegmentMatch(&Pattern[1..], &Entry[1..]);
1224 }
1225 false
1226}
1227
1228fn SingleSegmentMatch(Pattern:&str, Segment:&str) -> bool {
1229 if Pattern == "*" {
1230 return true;
1231 }
1232 if !Pattern.contains('*') && !Pattern.contains('?') {
1233 return Pattern == Segment;
1234 }
1235 let Fragments:Vec<&str> = Pattern.split('*').collect();
1240 let mut Cursor = 0usize;
1241 for (Index, Fragment) in Fragments.iter().enumerate() {
1242 if Fragment.is_empty() {
1243 continue;
1244 }
1245 if Index == 0 {
1246 if !Segment[Cursor..].starts_with(Fragment) {
1247 return false;
1248 }
1249 Cursor += Fragment.len();
1250 continue;
1251 }
1252 match Segment[Cursor..].find(Fragment) {
1253 Some(Offset) => Cursor += Offset + Fragment.len(),
1254 None => return false,
1255 }
1256 }
1257 if let Some(Last) = Fragments.last()
1258 && !Last.is_empty()
1259 {
1260 return Segment.ends_with(Last);
1261 }
1262 true
1263}