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 let Candidate = MountainRoot.join(BOOTSTRAP_SCRIPT_PATH);
301 if Candidate.exists() { Some(Candidate) } else { None }
302 })
303 })
304 .ok_or_else(|| {
305 CommonError::FileSystemNotFound(
306 format!(
307 "Cocoon bootstrap script '{}' not found in resources or relative to executable",
308 BOOTSTRAP_SCRIPT_PATH
309 )
310 .into(),
311 )
312 })?;
313
314 dev_log!(
315 "cocoon",
316 "[CocoonManagement] Found bootstrap script at: {}",
317 ScriptPath.display()
318 );
319
320 crate::dev_log!("cocoon", "bootstrap script: {}", ScriptPath.display());
321
322 let BundleProbe = path_resolver
338 .resolve("Cocoon/Target/Bootstrap/Implementation/Cocoon/Main.js", BaseDirectory::Resource)
339 .ok()
340 .filter(|P| P.exists());
341
342 if BundleProbe.is_none() {
343 if let Some(BootstrapDirectory) = ScriptPath.parent() {
344 let RepoProbePath = BootstrapDirectory.join("../..").join(COCOON_BUNDLE_PROBE);
345
346 if !RepoProbePath.exists() {
347 return Err(CommonError::IPCError {
348 Description:format!(
349 "Cocoon bundle is missing at {}. Run `pnpm run prepublishOnly \
350 --filter=@codeeditorland/cocoon` (or the full `./Maintain/Debug/Build.sh --profile \
351 debug-electron`) before launching - node will fail to import without it and Mountain will \
352 fall into degraded mode with zero extensions available. Root cause is typically an esbuild \
353 failure in an upstream Cocoon source file or a stale `rm -rf Element/Cocoon/Target` without \
354 a rebuild.",
355 RepoProbePath.display()
356 ),
357 });
358 }
359
360 dev_log!(
361 "cocoon",
362 "[CocoonManagement] pre-flight OK: bundle at {} (repo)",
363 RepoProbePath.display()
364 );
365 }
366 } else {
367 dev_log!("cocoon", "[CocoonManagement] pre-flight OK: bundle in bundle resources");
368 }
369
370 SweepStaleCocoon(COCOON_GRPC_PORT);
380
381 let ResolvedNodeBinary = NodeResolver::ResolveNodeBinary::Fn(&ApplicationHandle);
385
386 let mut NodeCommand = Command::new(&ResolvedNodeBinary.Path);
387
388 NodeCommand
389 .arg(&ScriptPath)
390 .env_clear()
391 .envs(BuildCocoonEnvironment())
392 .stdin(Stdio::piped())
393 .stdout(Stdio::piped())
394 .stderr(Stdio::piped());
395
396 let mut ChildProcess = NodeCommand.spawn().map_err(|Error| {
398 CommonError::IPCError {
399 Description:format!(
400 "Failed to spawn Cocoon with node={} (source={}): {}. Override with Pick=/absolute/path or install \
401 Node.js.",
402 ResolvedNodeBinary.Path.display(),
403 ResolvedNodeBinary.Source.AsLabel(),
404 Error
405 ),
406 }
407 })?;
408
409 let ProcessId = ChildProcess.id().unwrap_or(0);
410
411 COCOON_PID.store(ProcessId, std::sync::atomic::Ordering::Relaxed);
412
413 dev_log!("cocoon", "[CocoonManagement] Cocoon process spawned [PID: {}]", ProcessId);
414
415 crate::dev_log!("cocoon", "spawned PID={}", ProcessId);
416
417 SpawnCocoonIoForwarders(&mut ChildProcess);
418
419 let GRPCAddress = format!("127.0.0.1:{}", COCOON_GRPC_PORT);
438
439 dev_log!(
440 "cocoon",
441 "[CocoonManagement] Connecting to Cocoon gRPC at {} (exponential backoff, budget={}ms)...",
442 GRPCAddress,
443 GRPC_CONNECT_BUDGET_MS
444 );
445
446 let ConnectStart = tokio::time::Instant::now();
447
448 let mut CurrentDelayMs:u64 = GRPC_CONNECT_INITIAL_MS;
449
450 let mut ConnectAttempt = 0u32;
451
452 loop {
453 ConnectAttempt += 1;
454
455 crate::dev_log!(
456 "grpc",
457 "connecting to Cocoon at {} (attempt {}, elapsed={}ms)",
458 GRPCAddress,
459 ConnectAttempt,
460 ConnectStart.elapsed().as_millis()
461 );
462
463 match Vine::Client::ConnectToSideCar::Fn(SideCarIdentifier.clone(), GRPCAddress.clone()).await {
464 Ok(()) => {
465 crate::dev_log!(
466 "grpc",
467 "connected to Cocoon on attempt {} (elapsed={}ms)",
468 ConnectAttempt,
469 ConnectStart.elapsed().as_millis()
470 );
471
472 break;
473 },
474
475 Err(Error) => {
476 match ChildProcess.try_wait() {
482 Ok(Some(ExitStatus)) => {
483 let ExitCode = ExitStatus.code().unwrap_or(-1);
484
485 crate::dev_log!(
486 "grpc",
487 "attempt {} aborted: Cocoon Node process exited with code={} after {}ms - stderr above \
488 (if any) explains why",
489 ConnectAttempt,
490 ExitCode,
491 ConnectStart.elapsed().as_millis()
492 );
493
494 return Err(CommonError::IPCError {
495 Description:format!(
496 "Cocoon spawned but exited with code {} before Mountain could connect. See \
497 `[DEV:COCOON] warn: [Cocoon stderr] …` lines above for the Node-side error - \
498 typically a missing bundle (\"Cannot find module …\") or an ESM/CJS import drift \
499 after a partial build.",
500 ExitCode
501 ),
502 });
503 },
504
505 Ok(None) => { },
506
507 Err(WaitErr) => {
508 crate::dev_log!("grpc", "warn: try_wait on Cocoon child failed: {} (continuing)", WaitErr);
513 },
514 }
515
516 let Elapsed = ConnectStart.elapsed().as_millis() as u64;
517
518 if Elapsed >= GRPC_CONNECT_BUDGET_MS {
519 crate::dev_log!(
520 "grpc",
521 "attempt {} timed out (budget {}ms exhausted): {}",
522 ConnectAttempt,
523 GRPC_CONNECT_BUDGET_MS,
524 Error
525 );
526
527 return Err(CommonError::IPCError {
528 Description:format!(
529 "Failed to connect to Cocoon gRPC at {} after {} attempts over {}ms: {} (is Cocoon \
530 running? check `[DEV:COCOON]` log lines for stderr, or re-run with the debug-electron \
531 build profile if the bundle is stale)",
532 GRPCAddress, ConnectAttempt, GRPC_CONNECT_BUDGET_MS, Error
533 ),
534 });
535 }
536
537 crate::dev_log!(
538 "grpc",
539 "attempt {} pending (Cocoon still booting): {}, backing off {}ms",
540 ConnectAttempt,
541 Error,
542 CurrentDelayMs
543 );
544
545 sleep(Duration::from_millis(CurrentDelayMs)).await;
546
547 CurrentDelayMs = (CurrentDelayMs * 2).min(GRPC_CONNECT_MAX_DELAY_MS);
551 },
552 }
553 }
554
555 dev_log!(
556 "cocoon",
557 "[CocoonManagement] Connected to Cocoon. Sending initialization data..."
558 );
559
560 sleep(Duration::from_millis(200)).await;
563
564 let MainInitializationData = InitializationData::ConstructExtensionHostInitializationData(&Environment)
566 .await
567 .map_err(|Error| {
568 CommonError::IPCError { Description:format!("Failed to construct initialization data: {}", Error) }
569 })?;
570
571 let Response = Vine::Client::SendRequest::Fn(
573 &SideCarIdentifier,
574 "InitializeExtensionHost".to_string(),
575 MainInitializationData,
576 HANDSHAKE_TIMEOUT_MS,
577 )
578 .await
579 .map_err(|Error| {
580 CommonError::IPCError {
581 Description:format!("Failed to send initialization request to Cocoon: {}", Error),
582 }
583 })?;
584
585 match Response.as_str() {
587 Some("initialized") => {
588 dev_log!(
589 "cocoon",
590 "[CocoonManagement] Cocoon handshake complete. Extension host is ready."
591 );
592 },
593
594 Some(other) => {
595 return Err(CommonError::IPCError {
596 Description:format!("Cocoon initialization failed with unexpected response: {}", other),
597 });
598 },
599
600 None => {
601 return Err(CommonError::IPCError {
602 Description:"Cocoon initialization failed: no response received".to_string(),
603 });
604 },
605 }
606
607 let SideCarId = SideCarIdentifier.clone();
623
624 let EnvironmentForActivation = Environment.clone();
625
626 tokio::spawn(async move {
627 sleep(Duration::from_millis(500)).await;
629
630 crate::dev_log!("exthost", "Sending $activateByEvent(\"*\") to Cocoon");
631
632 if let Err(Error) = Vine::Client::SendRequest::Fn(
633 &SideCarId,
634 "$activateByEvent".to_string(),
635 serde_json::json!({ "activationEvent": "*" }),
636 30_000,
637 )
638 .await
639 {
640 dev_log!("cocoon", "warn: [CocoonManagement] $activateByEvent(\"*\") failed: {}", Error);
641 return;
642 }
643 dev_log!("cocoon", "[CocoonManagement] Startup extensions activation (*) triggered");
644
645 {
651 use CommonLibrary::Storage::StorageProvider::StorageProvider;
652
653 const PANEL_STATE_KEY:&str = "__webview_panel_state__";
654
655 if let Ok(Some(Stored)) = EnvironmentForActivation.GetStorageValue(true, PANEL_STATE_KEY).await {
656 if let Some(Entries) = Stored.as_array() {
657 if !Entries.is_empty() {
658 dev_log!(
659 "cocoon",
660 "[CocoonManagement] Restoring {} webview panel(s) from previous reload",
661 Entries.len()
662 );
663 }
664
665 for Entry in Entries {
666 let ViewType = Entry.get("viewType").and_then(|V| V.as_str()).unwrap_or("");
667
668 if ViewType.is_empty() {
669 continue;
670 }
671
672 let State = Entry.get("state").cloned().unwrap_or(serde_json::Value::Null);
673
674 let DeserializeMethod = "ExtHostWebviewPanels$deserializeWebviewPanel".to_string();
675
676 if let Err(Error) = Vine::Client::SendRequest::Fn(
677 &SideCarId,
678 DeserializeMethod,
679 serde_json::json!([ViewType, serde_json::Value::Null, State]),
680 5_000,
681 )
682 .await
683 {
684 dev_log!(
685 "cocoon",
686 "warn: [CocoonManagement] deserializeWebviewPanel({}) failed: {:?}",
687 ViewType,
688 Error
689 );
690 }
691 }
692 }
693
694 let _ = EnvironmentForActivation
697 .UpdateStorageValue(true, PANEL_STATE_KEY.to_string(), None)
698 .await;
699 }
700 }
701
702 {
707 let OpenDocs = EnvironmentForActivation.ApplicationState.Feature.Documents.GetAll();
708
709 if !OpenDocs.is_empty() {
710 dev_log!(
711 "exthost",
712 "[CocoonManagement] Seeding {} open document(s) to Cocoon",
713 OpenDocs.len()
714 );
715 for Doc in OpenDocs.values() {
716 let Payload = serde_json::json!({
717 "uri": Doc.URI.to_string(),
718 "languageId": Doc.LanguageIdentifier,
719 "version": Doc.Version,
720 "lines": Doc.Lines,
721 });
722 let _ =
723 Vine::Client::SendNotification::Fn(SideCarId.clone(), "$acceptModelAdded".to_string(), Payload)
724 .await;
725 }
726 }
727 }
728
729 let WorkspacePatterns = {
738 let AppState = &EnvironmentForActivation.ApplicationState;
739 let Folders:Vec<std::path::PathBuf> = AppState
740 .Workspace
741 .WorkspaceFolders
742 .lock()
743 .ok()
744 .map(|Guard| {
745 Guard
746 .iter()
747 .filter_map(|Folder| Folder.URI.to_file_path().ok())
748 .collect::<Vec<_>>()
749 })
750 .unwrap_or_default();
751
752 let Patterns:Vec<String> = AppState
753 .Extension
754 .ScannedExtensions
755 .ScannedExtensions
756 .lock()
757 .ok()
758 .map(|Guard| {
759 let mut Set:std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
760 for Description in Guard.values() {
761 if let Some(Events) = &Description.ActivationEvents {
762 for Event in Events {
763 if let Some(Pattern) = Event.strip_prefix("workspaceContains:") {
764 Set.insert(Pattern.to_string());
765 }
766 }
767 }
768 }
769 Set.into_iter().collect()
770 })
771 .unwrap_or_default();
772
773 (Folders, Patterns)
774 };
775
776 let (WorkspaceFolders, Patterns):(Vec<std::path::PathBuf>, Vec<String>) = WorkspacePatterns;
777 if !WorkspaceFolders.is_empty() && !Patterns.is_empty() {
778 let Matched = FindMatchingWorkspaceContainsPatterns(&WorkspaceFolders, &Patterns);
779 dev_log!(
780 "exthost",
781 "[CocoonManagement] workspaceContains scan: {} pattern(s) matched across {} folder(s)",
782 Matched.len(),
783 WorkspaceFolders.len()
784 );
785 for Pattern in Matched {
786 let Event = format!("workspaceContains:{}", Pattern);
787 if let Err(Error) = Vine::Client::SendRequest::Fn(
788 &SideCarId,
789 "$activateByEvent".to_string(),
790 serde_json::json!({ "activationEvent": Event }),
791 30_000,
792 )
793 .await
794 {
795 dev_log!(
796 "cocoon",
797 "warn: [CocoonManagement] $activateByEvent({}) failed: {}",
798 Event,
799 Error
800 );
801 }
802 }
803 }
804
805 sleep(Duration::from_millis(2_000)).await;
809 if let Err(Error) = Vine::Client::SendRequest::Fn(
810 &SideCarId,
811 "$activateByEvent".to_string(),
812 serde_json::json!({ "activationEvent": "onStartupFinished" }),
813 30_000,
814 )
815 .await
816 {
817 dev_log!(
818 "cocoon",
819 "warn: [CocoonManagement] $activateByEvent(onStartupFinished) failed: {}",
820 Error
821 );
822 } else {
823 dev_log!("cocoon", "[CocoonManagement] onStartupFinished activation triggered");
824 }
825 });
826
827 {
829 let mut state = COCOON_STATE.lock().await;
830
831 state.ChildProcess = Some(ChildProcess);
832
833 state.IsRunning = true;
834
835 state.StartTime = Some(tokio::time::Instant::now());
836
837 dev_log!("cocoon", "[CocoonManagement] Process state updated: Running");
838 }
839
840 {
842 let mut health = COCOON_HEALTH.lock().await;
843
844 health.ClearIssues();
845
846 dev_log!("cocoon", "[CocoonManagement] Health monitor reset to active state");
847 }
848
849 let state_clone = Arc::clone(&COCOON_STATE);
851
852 tokio::spawn(monitor_cocoon_health_task(state_clone));
853
854 dev_log!("cocoon", "[CocoonManagement] Background health monitoring started");
855
856 Ok(())
857}
858
859async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
866 loop {
867 tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
868
869 let mut state_guard = state.lock().await;
870
871 if state_guard.ChildProcess.is_some() {
873 let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
875
876 let exit_status = {
878 let child = state_guard.ChildProcess.as_mut().unwrap();
879
880 child.try_wait()
881 };
882
883 match exit_status {
884 Ok(Some(exit_code)) => {
885 let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
887
888 let exit_code_num = exit_code.code().unwrap_or(-1);
889
890 dev_log!(
891 "cocoon",
892 "warn: [CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
893 process_id.unwrap_or(0),
894 exit_code_num,
895 uptime
896 );
897
898 state_guard.IsRunning = false;
900
901 state_guard.ChildProcess = None;
902
903 COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
904
905 {
907 let mut health = COCOON_HEALTH.lock().await;
908
909 health.AddIssue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
910
911 dev_log!("cocoon", "warn: [CocoonHealth] Health score: {}", health.HealthScore);
912 }
913
914 dev_log!(
916 "cocoon",
917 "warn: [CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted \
918 manually or via application reinitialization"
919 );
920 },
921
922 Ok(None) => {
923 dev_log!(
925 "cocoon",
926 "[CocoonHealth] Cocoon process is healthy [PID: {}]",
927 process_id.unwrap_or(0)
928 );
929 },
930
931 Err(e) => {
932 dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
934
935 {
937 let mut health = COCOON_HEALTH.lock().await;
938
939 health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
940 }
941 },
942 }
943 } else {
944 dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
950
951 drop(state_guard);
952
953 return;
954 }
955 }
956}
957
958pub async fn HardKillCocoon() {
968 let mut State = COCOON_STATE.lock().await;
969
970 if let Some(mut Child) = State.ChildProcess.take() {
971 let Pid = Child.id().unwrap_or(0);
972
973 match Child.try_wait() {
974 Ok(Some(_Status)) => {
975 dev_log!("cocoon", "[CocoonShutdown] Child PID {} already exited; clearing handle.", Pid);
976 },
977
978 Ok(None) => {
979 dev_log!(
980 "cocoon",
981 "[CocoonShutdown] Child PID {} still alive after $shutdown; sending SIGKILL.",
982 Pid
983 );
984
985 if let Err(Error) = Child.start_kill() {
986 dev_log!("cocoon", "warn: [CocoonShutdown] start_kill failed on PID {}: {}", Pid, Error);
987 }
988
989 let _ = tokio::time::timeout(std::time::Duration::from_secs(2), Child.wait()).await;
991 },
992
993 Err(Error) => {
994 dev_log!("cocoon", "warn: [CocoonShutdown] try_wait failed on PID {}: {}", Pid, Error);
995 },
996 }
997 }
998
999 State.IsRunning = false;
1000}
1001
1002fn BuildCocoonEnvironment() -> HashMap<String, String> {
1008 const LAND_ENV_ALLOW_LIST:&[&str] = &[
1009 "Authorize",
1010 "Beam",
1011 "Report",
1012 "Brand",
1013 "Replay",
1014 "Ask",
1015 "Throttle",
1016 "Buffer",
1017 "Batch",
1018 "Cap",
1019 "Capture",
1020 "Pipe",
1021 "Emit",
1022 "Pick",
1023 "Require",
1024 "Lodge",
1025 "Extend",
1026 "Probe",
1027 "Ship",
1028 "Wire",
1029 "Install",
1030 "Mute",
1031 "Skip",
1032 "Spawn",
1033 "Render",
1034 "Walk",
1035 "Trace",
1036 "Record",
1037 "Profile",
1038 "Diagnose",
1039 "Resolve",
1040 "Open",
1041 "Warn",
1042 "Catch",
1043 "Source",
1044 "Track",
1045 "Defer",
1046 "Boot",
1047 "Pack",
1048 "DebugServer",
1049 "DebugServerPortMountain",
1050 "DebugServerPortCocoon",
1051 ];
1052
1053 let mut Env = HashMap::new();
1054
1055 Env.insert("VSCODE_PIPE_LOGGING".into(), "true".into());
1056
1057 Env.insert("VSCODE_VERBOSE_LOGGING".into(), "true".into());
1058
1059 Env.insert("VSCODE_PARENT_PID".into(), std::process::id().to_string());
1060
1061 Env.insert("MOUNTAIN_GRPC_PORT".into(), MOUNTAIN_GRPC_PORT.to_string());
1062
1063 Env.insert("COCOON_GRPC_PORT".into(), COCOON_GRPC_PORT.to_string());
1064
1065 for Key in ["PATH", "HOME"] {
1066 if let Ok(V) = std::env::var(Key) {
1067 Env.insert(Key.into(), V);
1068 }
1069 }
1070
1071 for (Key, Value) in std::env::vars() {
1072 if Key.starts_with("Product")
1073 || Key.starts_with("Tier")
1074 || Key.starts_with("Network")
1075 || LAND_ENV_ALLOW_LIST.contains(&Key.as_str())
1076 {
1077 Env.insert(Key, Value);
1078 }
1079 }
1080
1081 for Key in ["NODE_ENV", "TAURI_ENV_DEBUG"] {
1082 if let Ok(V) = std::env::var(Key) {
1083 Env.insert(Key.into(), V);
1084 }
1085 }
1086
1087 Env
1088}
1089
1090fn SpawnCocoonIoForwarders(Process:&mut tokio::process::Child) {
1098 dev_log!(
1099 "cocoon",
1100 "[CocoonIO] Spawning IO forwarder tasks (stdout={}, stderr={})",
1101 Process.stdout.is_some(),
1102 Process.stderr.is_some()
1103 );
1104
1105 if let Some(Stdout) = Process.stdout.take() {
1106 tauri::async_runtime::spawn(async move {
1107 let mut Lines = BufReader::new(Stdout).lines();
1108 loop {
1109 match Lines.next_line().await {
1110 Ok(Some(Line)) => {
1111 if let Some(Tag) = ExtractDevTag(&Line) {
1112 match Tag.as_str() {
1113 "bootstrap-stage" => dev_log!("bootstrap-stage", "[Cocoon stdout] {}", Line),
1114 "ext-activate" => dev_log!("ext-activate", "[Cocoon stdout] {}", Line),
1115 "config-prime" => dev_log!("config-prime", "[Cocoon stdout] {}", Line),
1116 "breaker" => dev_log!("breaker", "[Cocoon stdout] {}", Line),
1117 _ => dev_log!("cocoon", "[Cocoon stdout] {}", Line),
1118 }
1119 } else {
1120 dev_log!("cocoon", "[Cocoon stdout] {}", Line);
1121 }
1122 },
1123 Ok(None) => {
1124 dev_log!("cocoon", "[CocoonIO] stdout pipe closed (EOF)");
1125 break;
1126 },
1127 Err(Error) => {
1128 dev_log!("cocoon", "warn: [CocoonIO] stdout read error: {}", Error);
1129 break;
1130 },
1131 }
1132 }
1133 });
1134 } else {
1135 dev_log!("cocoon", "warn: [CocoonIO] stdout pipe not available (Stdio::piped() not set?)");
1136 }
1137
1138 if let Some(Stderr) = Process.stderr.take() {
1139 tauri::async_runtime::spawn(async move {
1140 let mut Lines = BufReader::new(Stderr).lines();
1141 let mut SuppressStack = false;
1142 loop {
1143 match Lines.next_line().await {
1144 Ok(Some(Line)) => {
1145 let T = Line.trim_start();
1146 let IsFrame = T.starts_with("at ") || T.starts_with("code: '") || T == "}" || T.is_empty();
1147 if SuppressStack && IsFrame {
1148 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
1149 continue;
1150 }
1151 SuppressStack = false;
1152 let Benign = Line.contains(": is already signed")
1153 || Line.contains(": replacing existing signature")
1154 || Line.contains("DeprecationWarning:")
1155 || Line.contains("--trace-deprecation")
1156 || Line.contains("--trace-warnings");
1157 let BenignHead = Line.contains("EntryNotFound (FileSystemError):")
1158 || Line.contains("FileNotFound (FileSystemError):")
1159 || Line.contains("[LandFix:UnhandledRejection]")
1160 || Line.starts_with("[Patcher] unhandledRejection:")
1161 || Line.starts_with("[Patcher] uncaughtException:");
1162 if BenignHead {
1163 SuppressStack = true;
1164 }
1165 if Benign || BenignHead {
1166 dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
1167 } else {
1168 dev_log!("cocoon", "warn: [Cocoon stderr] {}", Line);
1169 }
1170 },
1171 Ok(None) => {
1172 dev_log!("cocoon", "[CocoonIO] stderr pipe closed (EOF)");
1173 break;
1174 },
1175 Err(Error) => {
1176 dev_log!("cocoon", "warn: [CocoonIO] stderr read error: {}", Error);
1177 break;
1178 },
1179 }
1180 }
1181 });
1182 } else {
1183 dev_log!("cocoon", "warn: [CocoonIO] stderr pipe not available");
1184 }
1185}
1186
1187fn SweepStaleCocoon(Port:u16) {
1203 use std::{net::TcpStream, time::Duration};
1204
1205 let Addr = format!("127.0.0.1:{}", Port);
1206
1207 let Probe =
1210 TcpStream::connect_timeout(&Addr.parse().expect("valid socket addr literal"), Duration::from_millis(200));
1211
1212 if Probe.is_err() {
1213 dev_log!("cocoon", "[CocoonSweep] Port {} is clean (no prior listener).", Port);
1214
1215 return;
1216 }
1217
1218 dev_log!(
1219 "cocoon",
1220 "[CocoonSweep] Port {} has a listener - attempting to resolve owner via lsof.",
1221 Port
1222 );
1223
1224 let LsofOutput = std::process::Command::new("lsof")
1226 .args(["-nP", &format!("-iTCP:{}", Port), "-sTCP:LISTEN", "-t"])
1227 .output();
1228
1229 let Output = match LsofOutput {
1230 Ok(O) => O,
1231
1232 Err(Error) => {
1233 dev_log!(
1234 "cocoon",
1235 "warn: [CocoonSweep] lsof unavailable ({}). Skipping sweep; Cocoon spawn may fail with EADDRINUSE.",
1236 Error
1237 );
1238
1239 return;
1240 },
1241 };
1242
1243 if !Output.status.success() {
1244 dev_log!("cocoon", "warn: [CocoonSweep] lsof exited non-zero. Skipping sweep.");
1245
1246 return;
1247 }
1248
1249 let Stdout = String::from_utf8_lossy(&Output.stdout);
1250
1251 let Pids:Vec<i32> = Stdout.lines().filter_map(|L| L.trim().parse::<i32>().ok()).collect();
1252
1253 if Pids.is_empty() {
1254 dev_log!(
1255 "cocoon",
1256 "warn: [CocoonSweep] Port {} answered but lsof found no LISTEN PID - giving up.",
1257 Port
1258 );
1259
1260 return;
1261 }
1262
1263 let SelfPid = std::process::id() as i32;
1266
1267 for Pid in Pids {
1268 if Pid == SelfPid {
1269 dev_log!(
1270 "cocoon",
1271 "warn: [CocoonSweep] Port {} owned by Mountain itself (PID {}); refusing to kill.",
1272 Port,
1273 Pid
1274 );
1275
1276 continue;
1277 }
1278
1279 dev_log!("cocoon", "[CocoonSweep] Killing stale PID {} (SIGTERM).", Pid);
1280
1281 let _ = std::process::Command::new("kill").arg(Pid.to_string()).status();
1282
1283 std::thread::sleep(Duration::from_millis(500));
1284
1285 let StillAlive = std::process::Command::new("kill")
1287 .args(["-0", &Pid.to_string()])
1288 .status()
1289 .map(|S| S.success())
1290 .unwrap_or(false);
1291
1292 if StillAlive {
1293 dev_log!("cocoon", "warn: [CocoonSweep] PID {} survived SIGTERM; sending SIGKILL.", Pid);
1294
1295 let _ = std::process::Command::new("kill").args(["-9", &Pid.to_string()]).status();
1296
1297 std::thread::sleep(Duration::from_millis(200));
1298 }
1299
1300 dev_log!("cocoon", "[CocoonSweep] PID {} reaped.", Pid);
1301 }
1302}
1303
1304fn FindMatchingWorkspaceContainsPatterns(Folders:&[std::path::PathBuf], Patterns:&[String]) -> Vec<String> {
1321 use std::collections::HashSet;
1322
1323 const MAX_DEPTH:usize = 3;
1324
1325 const MAX_ENTRIES_PER_ROOT:usize = 4096;
1326
1327 let mut Matched:HashSet<String> = HashSet::new();
1328
1329 for Folder in Folders {
1330 if !Folder.is_dir() {
1331 continue;
1332 }
1333
1334 let mut Entries:Vec<String> = Vec::new();
1336
1337 let mut Stack:Vec<(std::path::PathBuf, usize)> = vec![(Folder.clone(), 0)];
1338
1339 while let Some((Current, Depth)) = Stack.pop() {
1340 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1341 break;
1342 }
1343
1344 let ReadDirResult = std::fs::read_dir(&Current);
1345
1346 let ReadDir = match ReadDirResult {
1347 Ok(R) => R,
1348
1349 Err(_) => continue,
1350 };
1351
1352 for Entry in ReadDir.flatten() {
1353 if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1354 break;
1355 }
1356
1357 let Path = Entry.path();
1358
1359 let Relative = match Path.strip_prefix(Folder) {
1360 Ok(R) => R.to_string_lossy().replace('\\', "/"),
1361
1362 Err(_) => continue,
1363 };
1364
1365 let IsDir = Entry.file_type().map(|T| T.is_dir()).unwrap_or(false);
1366
1367 Entries.push(Relative.clone());
1368
1369 if IsDir && Depth + 1 < MAX_DEPTH {
1370 Stack.push((Path, Depth + 1));
1371 }
1372 }
1373 }
1374
1375 for Pattern in Patterns {
1376 if Matched.contains(Pattern) {
1377 continue;
1378 }
1379
1380 if PatternMatchesAnyEntry(Pattern, &Entries) {
1381 Matched.insert(Pattern.clone());
1382 }
1383 }
1384 }
1385
1386 Matched.into_iter().collect()
1387}
1388
1389fn PatternMatchesAnyEntry(Pattern:&str, Entries:&[String]) -> bool {
1393 let HasWildcard = Pattern.contains('*') || Pattern.contains('?');
1394
1395 if !HasWildcard {
1396 return Entries.iter().any(|E| E == Pattern);
1397 }
1398
1399 let PatternSegments:Vec<&str> = Pattern.split('/').collect();
1400
1401 Entries
1402 .iter()
1403 .any(|E| SegmentMatch(&PatternSegments, &E.split('/').collect::<Vec<_>>()))
1404}
1405
1406fn SegmentMatch(Pattern:&[&str], Entry:&[&str]) -> bool {
1407 if Pattern.is_empty() {
1408 return Entry.is_empty();
1409 }
1410
1411 let Head = Pattern[0];
1412
1413 if Head == "**" {
1414 for Consumed in 0..=Entry.len() {
1416 if SegmentMatch(&Pattern[1..], &Entry[Consumed..]) {
1417 return true;
1418 }
1419 }
1420
1421 return false;
1422 }
1423
1424 if Entry.is_empty() {
1425 return false;
1426 }
1427
1428 if SingleSegmentMatch(Head, Entry[0]) {
1429 return SegmentMatch(&Pattern[1..], &Entry[1..]);
1430 }
1431
1432 false
1433}
1434
1435fn SingleSegmentMatch(Pattern:&str, Segment:&str) -> bool {
1436 if Pattern == "*" {
1437 return true;
1438 }
1439
1440 if !Pattern.contains('*') && !Pattern.contains('?') {
1441 return Pattern == Segment;
1442 }
1443
1444 let Fragments:Vec<&str> = Pattern.split('*').collect();
1449
1450 let mut Cursor = 0usize;
1451
1452 for (Index, Fragment) in Fragments.iter().enumerate() {
1453 if Fragment.is_empty() {
1454 continue;
1455 }
1456
1457 if Index == 0 {
1458 if !Segment[Cursor..].starts_with(Fragment) {
1459 return false;
1460 }
1461
1462 Cursor += Fragment.len();
1463
1464 continue;
1465 }
1466
1467 match Segment[Cursor..].find(Fragment) {
1468 Some(Offset) => Cursor += Offset + Fragment.len(),
1469
1470 None => return false,
1471 }
1472 }
1473
1474 if let Some(Last) = Fragments.last()
1475 && !Last.is_empty()
1476 {
1477 return Segment.ends_with(Last);
1478 }
1479
1480 true
1481}