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
120const MAX_RESTART_ATTEMPTS:u32 = 3;
121
122const RESTART_WINDOW_SECONDS:u64 = 300;
123
124struct CocoonProcessState {
126 ChildProcess:Option<Child>,
127
128 IsRunning:bool,
129
130 StartTime:Option<tokio::time::Instant>,
131
132 RestartCount:u32,
133
134 LastRestartTime:Option<tokio::time::Instant>,
135}
136
137impl Default for CocoonProcessState {
138 fn default() -> Self {
139 Self {
140 ChildProcess:None,
141
142 IsRunning:false,
143
144 StartTime:None,
145
146 RestartCount:0,
147
148 LastRestartTime:None,
149 }
150 }
151}
152
153lazy_static::lazy_static! {
155
156 static ref COCOON_STATE: Arc<Mutex<CocoonProcessState>> =
157 Arc::new(Mutex::new(CocoonProcessState::default()));
158
159 static ref COCOON_HEALTH: Arc<Mutex<HealthMonitor>> =
160 Arc::new(Mutex::new(HealthMonitor::new()));
161}
162
163static COCOON_PID:std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
168
169pub fn GetCocoonPid() -> Option<u32> {
172 match COCOON_PID.load(std::sync::atomic::Ordering::Relaxed) {
173 0 => None,
174
175 Pid => Some(Pid),
176 }
177}
178
179pub async fn InitializeCocoon(
213 ApplicationHandle:&AppHandle,
214
215 Environment:&Arc<MountainEnvironment>,
216) -> Result<(), CommonError> {
217 dev_log!("cocoon", "[CocoonManagement] Initializing Cocoon sidecar manager...");
218
219 if matches!(std::env::var("Spawn").as_deref(), Ok("0") | Ok("false")) {
225 dev_log!("cocoon", "[CocoonManagement] Skipping spawn (Spawn=false)");
226
227 return Ok(());
228 }
229
230 #[cfg(all(feature = "ExtensionHostCocoon", not(no_node_host)))]
231 {
232 LaunchAndManageCocoonSideCar(ApplicationHandle.clone(), Environment.clone()).await
233 }
234
235 #[cfg(any(not(feature = "ExtensionHostCocoon"), no_node_host))]
236 {
237 dev_log!(
238 "cocoon",
239 "[CocoonManagement] Cocoon spawn gated off (feature=ExtensionHostCocoon disabled or \
240 TierExtensionHost=WebWorker)."
241 );
242
243 Ok(())
244 }
245}
246
247async fn LaunchAndManageCocoonSideCar(
281 ApplicationHandle:AppHandle,
282
283 Environment:Arc<MountainEnvironment>,
284) -> Result<(), CommonError> {
285 let SideCarIdentifier = COCOON_SIDE_CAR_IDENTIFIER.to_string();
286
287 let path_resolver:PathResolver<Wry> = ApplicationHandle.path().clone();
288
289 let ScriptPath = path_resolver
294 .resolve(BOOTSTRAP_SCRIPT_PATH, BaseDirectory::Resource)
295 .ok()
296 .filter(|P| P.exists())
297 .or_else(|| {
298 std::env::current_exe().ok().and_then(|Exe| {
299 let MountainRoot = Exe.parent()?.parent()?.parent()?;
300
301 let Candidate = MountainRoot.join(BOOTSTRAP_SCRIPT_PATH);
302
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
644 return;
645 }
646
647 dev_log!("cocoon", "[CocoonManagement] Startup extensions activation (*) triggered");
648
649 {
655 use CommonLibrary::Storage::StorageProvider::StorageProvider;
656
657 const PANEL_STATE_KEY:&str = "__webview_panel_state__";
658
659 if let Ok(Some(Stored)) = EnvironmentForActivation.GetStorageValue(true, PANEL_STATE_KEY).await {
660 if let Some(Entries) = Stored.as_array() {
661 if !Entries.is_empty() {
662 dev_log!(
663 "cocoon",
664 "[CocoonManagement] Restoring {} webview panel(s) from previous reload",
665 Entries.len()
666 );
667 }
668
669 for Entry in Entries {
670 let ViewType = Entry.get("viewType").and_then(|V| V.as_str()).unwrap_or("");
671
672 if ViewType.is_empty() {
673 continue;
674 }
675
676 let State = Entry.get("state").cloned().unwrap_or(serde_json::Value::Null);
677
678 let DeserializeMethod = "ExtHostWebviewPanels$deserializeWebviewPanel".to_string();
679
680 if let Err(Error) = ::Vine::Client::SendRequest::Fn(
681 &SideCarId,
682 DeserializeMethod,
683 serde_json::json!([ViewType, serde_json::Value::Null, State]),
684 5_000,
685 )
686 .await
687 {
688 dev_log!(
689 "cocoon",
690 "warn: [CocoonManagement] deserializeWebviewPanel({}) failed: {:?}",
691 ViewType,
692 Error
693 );
694 }
695 }
696 }
697
698 let _ = EnvironmentForActivation
701 .UpdateStorageValue(true, PANEL_STATE_KEY.to_string(), None)
702 .await;
703 }
704 }
705
706 {
711 let OpenDocs = EnvironmentForActivation.ApplicationState.Feature.Documents.GetAll();
712
713 if !OpenDocs.is_empty() {
714 dev_log!(
715 "exthost",
716 "[CocoonManagement] Seeding {} open document(s) to Cocoon",
717 OpenDocs.len()
718 );
719
720 for Doc in OpenDocs.values() {
721 let Payload = serde_json::json!({
722 "uri": Doc.URI.to_string(),
723 "languageId": Doc.LanguageIdentifier,
724 "version": Doc.Version,
725 "lines": Doc.Lines,
726 });
727
728 let _ = ::Vine::Client::SendNotification::Fn(
729 SideCarId.clone(),
730 "$acceptModelAdded".to_string(),
731 Payload,
732 )
733 .await;
734 }
735 }
736 }
737
738 let WorkspacePatterns = {
747 let AppState = &EnvironmentForActivation.ApplicationState;
748
749 let Folders:Vec<std::path::PathBuf> = AppState
750 .Workspace
751 .WorkspaceFolders
752 .lock()
753 .ok()
754 .map(|Guard| {
755 Guard
756 .iter()
757 .filter_map(|Folder| Folder.URI.to_file_path().ok())
758 .collect::<Vec<_>>()
759 })
760 .unwrap_or_default();
761
762 let Patterns:Vec<String> = AppState
763 .Extension
764 .ScannedExtensions
765 .ScannedExtensions
766 .lock()
767 .ok()
768 .map(|Guard| {
769 let mut Set:std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
770
771 for Description in Guard.values() {
772 if let Some(Events) = &Description.ActivationEvents {
773 for Event in Events {
774 if let Some(Pattern) = Event.strip_prefix("workspaceContains:") {
775 Set.insert(Pattern.to_string());
776 }
777 }
778 }
779 }
780
781 Set.into_iter().collect()
782 })
783 .unwrap_or_default();
784
785 (Folders, Patterns)
786 };
787
788 let (WorkspaceFolders, Patterns):(Vec<std::path::PathBuf>, Vec<String>) = WorkspacePatterns;
789
790 if !WorkspaceFolders.is_empty() && !Patterns.is_empty() {
791 let Matched = FindMatchingWorkspaceContainsPatterns(&WorkspaceFolders, &Patterns);
792
793 dev_log!(
794 "exthost",
795 "[CocoonManagement] workspaceContains scan: {} pattern(s) matched across {} folder(s)",
796 Matched.len(),
797 WorkspaceFolders.len()
798 );
799
800 for Pattern in Matched {
801 let Event = format!("workspaceContains:{}", Pattern);
802
803 if let Err(Error) = ::Vine::Client::SendRequest::Fn(
804 &SideCarId,
805 "$activateByEvent".to_string(),
806 serde_json::json!({ "activationEvent": Event }),
807 30_000,
808 )
809 .await
810 {
811 dev_log!(
812 "cocoon",
813 "warn: [CocoonManagement] $activateByEvent({}) failed: {}",
814 Event,
815 Error
816 );
817 }
818 }
819 }
820
821 sleep(Duration::from_millis(2_000)).await;
825
826 if let Err(Error) = ::Vine::Client::SendRequest::Fn(
827 &SideCarId,
828 "$activateByEvent".to_string(),
829 serde_json::json!({ "activationEvent": "onStartupFinished" }),
830 30_000,
831 )
832 .await
833 {
834 dev_log!(
835 "cocoon",
836 "warn: [CocoonManagement] $activateByEvent(onStartupFinished) failed: {}",
837 Error
838 );
839 } else {
840 dev_log!("cocoon", "[CocoonManagement] onStartupFinished activation triggered");
841 }
842 });
843
844 {
846 let mut state = COCOON_STATE.lock().await;
847
848 state.ChildProcess = Some(ChildProcess);
849
850 state.IsRunning = true;
851
852 state.StartTime = Some(tokio::time::Instant::now());
853
854 dev_log!("cocoon", "[CocoonManagement] Process state updated: Running");
855 }
856
857 {
859 let mut health = COCOON_HEALTH.lock().await;
860
861 health.ClearIssues();
862
863 dev_log!("cocoon", "[CocoonManagement] Health monitor reset to active state");
864 }
865
866 let state_clone = Arc::clone(&COCOON_STATE);
868
869 tokio::spawn(monitor_cocoon_health_task(state_clone));
870
871 dev_log!("cocoon", "[CocoonManagement] Background health monitoring started");
872
873 Ok(())
874}
875
876async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
883 loop {
884 tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
885
886 let mut state_guard = state.lock().await;
887
888 if state_guard.ChildProcess.is_some() {
890 let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
892
893 let exit_status = {
895 let child = state_guard.ChildProcess.as_mut().unwrap();
896
897 child.try_wait()
898 };
899
900 match exit_status {
901 Ok(Some(exit_code)) => {
902 let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
904
905 let exit_code_num = exit_code.code().unwrap_or(-1);
906
907 dev_log!(
908 "cocoon",
909 "warn: [CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
910 process_id.unwrap_or(0),
911 exit_code_num,
912 uptime
913 );
914
915 state_guard.IsRunning = false;
917
918 state_guard.ChildProcess = None;
919
920 COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
921
922 {
924 let mut health = COCOON_HEALTH.lock().await;
925
926 health.AddIssue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
927
928 dev_log!("cocoon", "warn: [CocoonHealth] Health score: {}", health.HealthScore);
929 }
930
931 dev_log!(
933 "cocoon",
934 "warn: [CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted \
935 manually or via application reinitialization"
936 );
937 },
938
939 Ok(None) => {
940 dev_log!(
942 "cocoon",
943 "[CocoonHealth] Cocoon process is healthy [PID: {}]",
944 process_id.unwrap_or(0)
945 );
946 },
947
948 Err(e) => {
949 dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
951
952 {
954 let mut health = COCOON_HEALTH.lock().await;
955
956 health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
957 }
958 },
959 }
960 } else {
961 dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
967
968 drop(state_guard);
969
970 return;
971 }
972 }
973}
974
975pub async fn HardKillCocoon() {
985 let mut State = COCOON_STATE.lock().await;
986
987 if let Some(mut Child) = State.ChildProcess.take() {
988 let Pid = Child.id().unwrap_or(0);
989
990 match Child.try_wait() {
991 Ok(Some(_Status)) => {
992 dev_log!("cocoon", "[CocoonShutdown] Child PID {} already exited; clearing handle.", Pid);
993 },
994
995 Ok(None) => {
996 dev_log!(
997 "cocoon",
998 "[CocoonShutdown] Child PID {} still alive after $shutdown; sending SIGKILL.",
999 Pid
1000 );
1001
1002 if let Err(Error) = Child.start_kill() {
1003 dev_log!("cocoon", "warn: [CocoonShutdown] start_kill failed on PID {}: {}", Pid, Error);
1004 }
1005
1006 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), Child.wait()).await;
1008 },
1009
1010 Err(Error) => {
1011 dev_log!("cocoon", "warn: [CocoonShutdown] try_wait failed on PID {}: {}", Pid, Error);
1012 },
1013 }
1014 }
1015
1016 State.IsRunning = false;
1017}
1018
1019fn BuildCocoonEnvironment() -> HashMap<String, String> {
1025 const LAND_ENV_ALLOW_LIST:&[&str] = &[
1026 "Authorize",
1027 "Beam",
1028 "Report",
1029 "Brand",
1030 "Replay",
1031 "Ask",
1032 "Throttle",
1033 "Buffer",
1034 "Batch",
1035 "Cap",
1036 "Capture",
1037 "Pipe",
1038 "Emit",
1039 "Pick",
1040 "Require",
1041 "Lodge",
1042 "Extend",
1043 "Probe",
1044 "Ship",
1045 "Wire",
1046 "Install",
1047 "Mute",
1048 "Skip",
1049 "Spawn",
1050 "Render",
1051 "Walk",
1052 "Trace",
1053 "Record",
1054 "Profile",
1055 "Diagnose",
1056 "Resolve",
1057 "Open",
1058 "Warn",
1059 "Catch",
1060 "Source",
1061 "Track",
1062 "Defer",
1063 "Boot",
1064 "Pack",
1065 "DebugServer",
1066 "DebugServerPortMountain",
1067 "DebugServerPortCocoon",
1068 ];
1069
1070 let mut Env = HashMap::new();
1071
1072 Env.insert("VSCODE_PIPE_LOGGING".into(), "true".into());
1073
1074 Env.insert("VSCODE_VERBOSE_LOGGING".into(), "true".into());
1075
1076 Env.insert("VSCODE_PARENT_PID".into(), std::process::id().to_string());
1077
1078 Env.insert("MOUNTAIN_GRPC_PORT".into(), MOUNTAIN_GRPC_PORT.to_string());
1079
1080 Env.insert("COCOON_GRPC_PORT".into(), COCOON_GRPC_PORT.to_string());
1081
1082 for Key in ["PATH", "HOME"] {
1083 if let Ok(V) = std::env::var(Key) {
1084 Env.insert(Key.into(), V);
1085 }
1086 }
1087
1088 for (Key, Value) in std::env::vars() {
1089 if Key.starts_with("Product")
1090 || Key.starts_with("Tier")
1091 || Key.starts_with("Network")
1092 || LAND_ENV_ALLOW_LIST.contains(&Key.as_str())
1093 {
1094 Env.insert(Key, Value);
1095 }
1096 }
1097
1098 for Key in ["NODE_ENV", "TAURI_ENV_DEBUG"] {
1099 if let Ok(V) = std::env::var(Key) {
1100 Env.insert(Key.into(), V);
1101 }
1102 }
1103
1104 Env
1105}
1106
1107fn SpawnCocoonIoForwarders(Process:&mut tokio::process::Child) {
1115 dev_log!(
1116 "cocoon",
1117 "[CocoonIO] Spawning IO forwarder tasks (stdout={}, stderr={})",
1118 Process.stdout.is_some(),
1119 Process.stderr.is_some()
1120 );
1121
1122 if let Some(Stdout) = Process.stdout.take() {
1123 tauri::async_runtime::spawn(async move {
1124 let mut Lines = BufReader::new(Stdout).lines();
1125
1126 loop {
1127 match Lines.next_line().await {
1128 Ok(Some(Line)) => {
1129 if let Some(Tag) = ExtractDevTag(&Line) {
1130 match Tag.as_str() {
1131 "bootstrap-stage" => dev_log!("bootstrap-stage", "[Cocoon stdout] {}", Line),
1132 "ext-activate" => dev_log!("ext-activate", "[Cocoon stdout] {}", Line),
1133 "config-prime" => dev_log!("config-prime", "[Cocoon stdout] {}", Line),
1134 "breaker" => dev_log!("breaker", "[Cocoon stdout] {}", Line),
1135 _ => dev_log!("cocoon", "[Cocoon stdout] {}", Line),
1136 }
1137 } else {
1138 dev_log!("cocoon", "[Cocoon stdout] {}", Line);
1139 }
1140 },
1141 Ok(None) => {
1142 dev_log!("cocoon", "[CocoonIO] stdout pipe closed (EOF)");
1143
1144 break;
1145 },
1146 Err(Error) => {
1147 dev_log!("cocoon", "warn: [CocoonIO] stdout read error: {}", Error);
1148
1149 break;
1150 },
1151 }
1152 }
1153 });
1154 } else {
1155 dev_log!("cocoon", "warn: [CocoonIO] stdout pipe not available (Stdio::piped() not set?)");
1156 }
1157
1158 if let Some(Stderr) = Process.stderr.take() {
1159 tauri::async_runtime::spawn(async move {
1160 let mut Lines = BufReader::new(Stderr).lines();
1161
1162 let mut SuppressStack = false;
1163
1164 loop {
1165 match Lines.next_line().await {
1166 Ok(Some(Line)) => {
1167 let T = Line.trim_start();
1168
1169 let IsFrame = T.starts_with("at ") || T.starts_with("code: '") || T == "}" || T.is_empty();
1170
1171 if SuppressStack && IsFrame {
1172 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
1173
1174 continue;
1175 }
1176
1177 SuppressStack = false;
1178
1179 let Benign = Line.contains(": is already signed")
1180 || Line.contains(": replacing existing signature")
1181 || Line.contains("DeprecationWarning:")
1182 || Line.contains("--trace-deprecation")
1183 || Line.contains("--trace-warnings");
1184
1185 let BenignHead = Line.contains("EntryNotFound (FileSystemError):")
1186 || Line.contains("FileNotFound (FileSystemError):")
1187 || Line.contains("[LandFix:UnhandledRejection]")
1188 || Line.starts_with("[Patcher] unhandledRejection:")
1189 || Line.starts_with("[Patcher] uncaughtException:");
1190
1191 if BenignHead {
1192 SuppressStack = true;
1193 }
1194
1195 if Benign || BenignHead {
1196 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
1197 } else {
1198 dev_log!("cocoon", "warn: [Cocoon stderr] {}", Line);
1199 }
1200 },
1201 Ok(None) => {
1202 dev_log!("cocoon", "[CocoonIO] stderr pipe closed (EOF)");
1203
1204 break;
1205 },
1206 Err(Error) => {
1207 dev_log!("cocoon", "warn: [CocoonIO] stderr read error: {}", Error);
1208
1209 break;
1210 },
1211 }
1212 }
1213 });
1214 } else {
1215 dev_log!("cocoon", "warn: [CocoonIO] stderr pipe not available");
1216 }
1217}
1218
1219fn SweepStaleCocoon(Port:u16) {
1235 use std::{net::TcpStream, time::Duration};
1236
1237 let Addr = format!("127.0.0.1:{}", Port);
1238
1239 let Probe =
1242 TcpStream::connect_timeout(&Addr.parse().expect("valid socket addr literal"), Duration::from_millis(200));
1243
1244 if Probe.is_err() {
1245 dev_log!("cocoon", "[CocoonSweep] Port {} is clean (no prior listener).", Port);
1246
1247 return;
1248 }
1249
1250 dev_log!(
1251 "cocoon",
1252 "[CocoonSweep] Port {} has a listener - attempting to resolve owner via lsof.",
1253 Port
1254 );
1255
1256 let LsofOutput = std::process::Command::new("lsof")
1258 .args(["-nP", &format!("-iTCP:{}", Port), "-sTCP:LISTEN", "-t"])
1259 .output();
1260
1261 let Output = match LsofOutput {
1262 Ok(O) => O,
1263
1264 Err(Error) => {
1265 dev_log!(
1266 "cocoon",
1267 "warn: [CocoonSweep] lsof unavailable ({}). Skipping sweep; Cocoon spawn may fail with EADDRINUSE.",
1268 Error
1269 );
1270
1271 return;
1272 },
1273 };
1274
1275 if !Output.status.success() {
1276 dev_log!("cocoon", "warn: [CocoonSweep] lsof exited non-zero. Skipping sweep.");
1277
1278 return;
1279 }
1280
1281 let Stdout = String::from_utf8_lossy(&Output.stdout);
1282
1283 let Pids:Vec<i32> = Stdout.lines().filter_map(|L| L.trim().parse::<i32>().ok()).collect();
1284
1285 if Pids.is_empty() {
1286 dev_log!(
1287 "cocoon",
1288 "warn: [CocoonSweep] Port {} answered but lsof found no LISTEN PID - giving up.",
1289 Port
1290 );
1291
1292 return;
1293 }
1294
1295 let SelfPid = std::process::id() as i32;
1298
1299 for Pid in Pids {
1300 if Pid == SelfPid {
1301 dev_log!(
1302 "cocoon",
1303 "warn: [CocoonSweep] Port {} owned by Mountain itself (PID {}); refusing to kill.",
1304 Port,
1305 Pid
1306 );
1307
1308 continue;
1309 }
1310
1311 dev_log!("cocoon", "[CocoonSweep] Killing stale PID {} (SIGTERM).", Pid);
1312
1313 let _ = std::process::Command::new("kill").arg(Pid.to_string()).status();
1314
1315 std::thread::sleep(Duration::from_millis(500));
1316
1317 let StillAlive = std::process::Command::new("kill")
1319 .args(["-0", &Pid.to_string()])
1320 .status()
1321 .map(|S| S.success())
1322 .unwrap_or(false);
1323
1324 if StillAlive {
1325 dev_log!("cocoon", "warn: [CocoonSweep] PID {} survived SIGTERM; sending SIGKILL.", Pid);
1326
1327 let _ = std::process::Command::new("kill").args(["-9", &Pid.to_string()]).status();
1328
1329 std::thread::sleep(Duration::from_millis(200));
1330 }
1331
1332 dev_log!("cocoon", "[CocoonSweep] PID {} reaped.", Pid);
1333 }
1334}
1335
1336fn FindMatchingWorkspaceContainsPatterns(Folders:&[std::path::PathBuf], Patterns:&[String]) -> Vec<String> {
1353 use std::collections::HashSet;
1354
1355 const MAX_DEPTH:usize = 3;
1356
1357 const MAX_ENTRIES_PER_ROOT:usize = 4096;
1358
1359 let mut Matched:HashSet<String> = HashSet::new();
1360
1361 for Folder in Folders {
1362 if !Folder.is_dir() {
1363 continue;
1364 }
1365
1366 let mut Entries:Vec<String> = Vec::new();
1368
1369 let mut Stack:Vec<(std::path::PathBuf, usize)> = vec![(Folder.clone(), 0)];
1370
1371 while let Some((Current, Depth)) = Stack.pop() {
1372 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1373 break;
1374 }
1375
1376 let ReadDirResult = std::fs::read_dir(&Current);
1377
1378 let ReadDir = match ReadDirResult {
1379 Ok(R) => R,
1380
1381 Err(_) => continue,
1382 };
1383
1384 for Entry in ReadDir.flatten() {
1385 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1386 break;
1387 }
1388
1389 let Path = Entry.path();
1390
1391 let Relative = match Path.strip_prefix(Folder) {
1392 Ok(R) => R.to_string_lossy().replace('\\', "/"),
1393
1394 Err(_) => continue,
1395 };
1396
1397 let IsDir = Entry.file_type().map(|T| T.is_dir()).unwrap_or(false);
1398
1399 Entries.push(Relative.clone());
1400
1401 if IsDir && Depth + 1 < MAX_DEPTH {
1402 Stack.push((Path, Depth + 1));
1403 }
1404 }
1405 }
1406
1407 for Pattern in Patterns {
1408 if Matched.contains(Pattern) {
1409 continue;
1410 }
1411
1412 if PatternMatchesAnyEntry(Pattern, &Entries) {
1413 Matched.insert(Pattern.clone());
1414 }
1415 }
1416 }
1417
1418 Matched.into_iter().collect()
1419}
1420
1421fn PatternMatchesAnyEntry(Pattern:&str, Entries:&[String]) -> bool {
1425 let HasWildcard = Pattern.contains('*') || Pattern.contains('?');
1426
1427 if !HasWildcard {
1428 return Entries.iter().any(|E| E == Pattern);
1429 }
1430
1431 let PatternSegments:Vec<&str> = Pattern.split('/').collect();
1432
1433 Entries
1434 .iter()
1435 .any(|E| SegmentMatch(&PatternSegments, &E.split('/').collect::<Vec<_>>()))
1436}
1437
1438fn SegmentMatch(Pattern:&[&str], Entry:&[&str]) -> bool {
1439 if Pattern.is_empty() {
1440 return Entry.is_empty();
1441 }
1442
1443 let Head = Pattern[0];
1444
1445 if Head == "**" {
1446 for Consumed in 0..=Entry.len() {
1448 if SegmentMatch(&Pattern[1..], &Entry[Consumed..]) {
1449 return true;
1450 }
1451 }
1452
1453 return false;
1454 }
1455
1456 if Entry.is_empty() {
1457 return false;
1458 }
1459
1460 if SingleSegmentMatch(Head, Entry[0]) {
1461 return SegmentMatch(&Pattern[1..], &Entry[1..]);
1462 }
1463
1464 false
1465}
1466
1467fn SingleSegmentMatch(Pattern:&str, Segment:&str) -> bool {
1468 if Pattern == "*" {
1469 return true;
1470 }
1471
1472 if !Pattern.contains('*') && !Pattern.contains('?') {
1473 return Pattern == Segment;
1474 }
1475
1476 let Fragments:Vec<&str> = Pattern.split('*').collect();
1481
1482 let mut Cursor = 0usize;
1483
1484 for (Index, Fragment) in Fragments.iter().enumerate() {
1485 if Fragment.is_empty() {
1486 continue;
1487 }
1488
1489 if Index == 0 {
1490 if !Segment[Cursor..].starts_with(Fragment) {
1491 return false;
1492 }
1493
1494 Cursor += Fragment.len();
1495
1496 continue;
1497 }
1498
1499 match Segment[Cursor..].find(Fragment) {
1500 Some(Offset) => Cursor += Offset + Fragment.len(),
1501
1502 None => return false,
1503 }
1504 }
1505
1506 if let Some(Last) = Fragments.last()
1507 && !Last.is_empty()
1508 {
1509 return Segment.ends_with(Last);
1510 }
1511
1512 true
1513}