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