Skip to main content

DevelopmentNodeEnvironment_MicrosoftVSCodeDependency_22NodeVersion_Bundle_Clean_Debug_ElectronProfile_EsbuildCompiler_Mountain/ProcessManagement/
CocoonManagement.rs

1//! # Cocoon Management
2//!
3//! This module provides comprehensive lifecycle management for the Cocoon
4//! sidecar process, which serves as the VS Code extension host within the
5//! Mountain editor.
6//!
7//! ## Overview
8//!
9//! Cocoon is a Node.js-based process that provides compatibility with VS Code
10//! extensions. This module handles:
11//!
12//! - **Process Spawning**: Launching Node.js with the Cocoon bootstrap script
13//! - **Environment Configuration**: Setting up environment variables for IPC
14//!   and logging
15//! - **Communication Setup**: Establishing gRPC/Vine connections on port 50052
16//! - **Health Monitoring**: Tracking process state and handling failures
17//! - **Lifecycle Management**: Graceful shutdown and restart capabilities
18//! - **IO Redirection**: Capturing stdout/stderr for logging and debugging
19//!
20//! ## Process Communication
21//!
22//! The Cocoon process communicates via:
23//! - gRPC on port 50052 (configured via MOUNTAIN_GRPC_PORT/COCOON_GRPC_PORT)
24//! - Vine protocol for cross-process messaging
25//! - Standard streams for logging (VSCODE_PIPE_LOGGING)
26//!
27//! ## Dependencies
28//!
29//! - `scripts/cocoon/bootstrap-fork.js`: Bootstrap script for launching Cocoon
30//! - Node.js runtime: Required for executing Cocoon
31//! - Vine gRPC server: Must be running on port 50051 for handshake
32//!
33//! ## Error Handling
34//!
35//! The module provides graceful degradation:
36//! - If the bootstrap script is missing, returns `FileSystemNotFound` error
37//! - If Node.js cannot be spawned, returns `IPCError`
38//! - If gRPC connection fails, returns `IPCError` with context
39//!
40//! # Module Contents
41//!
42//! - [`InitializeCocoon`]: Main entry point for Cocoon initialization
43//! - `LaunchAndManageCocoonSideCar`: Process spawning and lifecycle
44//! management
45//!
46//! ## Example
47//!
48//! ```rust,no_run
49//! use crate::Source::ProcessManagement::CocoonManagement::InitializeCocoon;
50//!
51//! // Initialize Cocoon with application handle and environment
52//! match InitializeCocoon(&app_handle, &environment).await {
53//! 	Ok(()) => println!("Cocoon initialized successfully"),
54//! 	Err(e) => eprintln!("Cocoon initialization failed: {:?}", e),
55//! }
56//! ```
57
58use 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
83/// Configuration constants for Cocoon process management
84const 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
92/// Exponential-backoff retry parameters for the Mountain → Cocoon gRPC
93/// handshake. After the Bootstrap.ts stage-reorder fix, Cocoon's RPCServer
94/// (port 50052) starts as Stage 3 (before MountainConnection), so the port
95/// is available within 2-5 seconds of spawn. Budget raised to 30 s as a
96/// defensive buffer for slow hardware or contended startup.
97///
98/// Policy: start at 50 ms, double each attempt up to a 2 s ceiling,
99/// with a hard 30 s total-budget. Under healthy spawn timing (Cocoon
100/// binds 50052 within 2-3s) this converges on attempts 5-8 in <~3s total;
101/// under a genuinely dead Cocoon the loop abandons at the budget.
102const 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
108/// Relative path from the resolved Cocoon package root to the bundled
109/// entry module. Used by the pre-flight guard below to fail fast with
110/// an actionable error when the bundle is missing (esbuild failure,
111/// partial rm -rf, freshly cloned checkout without `pnpm run
112/// prepublishOnly`, etc.) instead of spawning Node into a dying
113/// require() chain.
114const 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
124/// Global state for tracking Cocoon process lifecycle
125struct 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
153// Global state for Cocoon process management
154lazy_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
163/// Last-known PID of the Cocoon child process. Mirrored here so callers can
164/// read it without taking the async `COCOON_STATE` mutex (e.g. from IPC
165/// handlers such as `extensionHostStarter:start`). Set after spawn and
166/// cleared on shutdown. `0` means "not running".
167static COCOON_PID:std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
168
169/// Return the Cocoon child process's OS PID, or `None` if Cocoon has not
170/// been spawned (or has exited).
171pub 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
179/// The main entry point for initializing the Cocoon sidecar process manager.
180///
181/// This orchestrates the complete initialization sequence including:
182/// - Validating feature flags and dependencies
183/// - Launching the Cocoon process with proper configuration
184/// - Establishing gRPC communication
185/// - Performing the initialization handshake
186/// - Setting up process health monitoring
187///
188/// # Arguments
189///
190/// * `ApplicationHandle` - Tauri application handle for path resolution
191/// * `Environment` - Mountain environment containing application state and
192///   services
193///
194/// # Returns
195///
196/// * `Ok(())` - Cocoon initialized successfully and ready to accept extension
197///   requests
198/// * `Err(CommonError)` - Initialization failed with detailed error context
199///
200/// # Errors
201///
202/// - `FileSystemNotFound`: Bootstrap script not found
203/// - `IPCError`: Failed to spawn process or establish gRPC connection
204///
205/// # Example
206///
207/// ```rust,no_run
208/// use crate::Source::ProcessManagement::CocoonManagement::InitializeCocoon;
209///
210/// InitializeCocoon(&app_handle, &environment).await?;
211/// ```
212pub 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	// Atom N1: `debug-mountain-only` / `release-mountain-only` profiles set
220	// Spawn=false so Mountain boots without the extension host.
221	// Extension-related IPC returns the empty-state envelope; the workbench
222	// loads but no extension activates. Useful for integration tests that
223	// exercise Mountain in isolation and for the smallest shippable surface.
224	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
247/// Spawns the Cocoon process, manages its communication channels, and performs
248/// the complete initialization handshake sequence.
249///
250/// This function implements the complete Cocoon lifecycle:
251/// 1. Validates bootstrap script availability
252/// 2. Constructs environment variables for IPC and logging
253/// 3. Spawns Node.js process with proper IO redirection
254/// 4. Captures stdout/stderr for logging
255/// 5. Waits for gRPC server to be ready
256/// 6. Establishes Vine connection
257/// 7. Sends initialization payload and validates response
258///
259/// # Arguments
260///
261/// * `ApplicationHandle` - Tauri application handle for resolving resource
262///   paths
263/// * `Environment` - Mountain environment containing application state
264///
265/// # Returns
266///
267/// * `Ok(())` - Cocoon process spawned, connected, and initialized successfully
268/// * `Err(CommonError)` - Any failure during the initialization sequence
269///
270/// # Errors
271///
272/// - `FileSystemNotFound`: Bootstrap script not found in resources
273/// - `IPCError`: Failed to spawn process, connect gRPC, or complete handshake
274///
275/// # Lifecycle
276///
277/// The process runs as a background task with IO redirection for logging.
278/// Process failures are logged but not automatically restarted (callers should
279/// implement restart strategies based on their requirements).
280async 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	// Resolve bootstrap script path.
290	// 1) Try Tauri bundled resources (production builds).
291	// 2) Fallback: resolve relative to the executable (dev builds). Dev layout:
292	//    Target/debug/binary → ../../scripts/cocoon/bootstrap-fork.js
293	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	// Pre-flight: Cocoon's bundle must exist or the spawned Node will
323	// die silently on the first `import()` and we'll sit through 20+
324	// seconds of `attempt N/M` retries with no diagnostic.
325	//
326	// Two layouts:
327	//
328	// 1. Bundle (.app): tauri.conf.json maps
329	//    `Element/Cocoon/Target/Bootstrap/Implementation/Cocoon` →
330	//    `Contents/Resources/Cocoon/Target/Bootstrap/Implementation/Cocoon`. The
331	//    Tauri resource resolver finds it directly.
332	//
333	// 2. Repo (dev binary): bootstrap is at
334	//    `Element/Mountain/scripts/cocoon/bootstrap-fork.js`, so walking `../../..`
335	//    from the bootstrap dir reaches `Element/` and `COCOON_BUNDLE_PROBE`
336	//    (`../Cocoon/Target/...`) descends into `Element/Cocoon/Target/...`.
337	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	// Atom I6: zombie-Cocoon sweep. If a prior Mountain exited without
371	// killing its child (segfault, SIGKILL, debugger detach, …), the stale
372	// node process keeps port COCOON_GRPC_PORT bound. The new Mountain's
373	// VineClient then "successfully connects" to the zombie while the
374	// freshly-spawned Cocoon fails to bind with EADDRINUSE, and the whole
375	// extension host enters degraded mode with zero extensions visible.
376	//
377	// Probe the port. If it answers, find the owning PID via `lsof -t -i
378	// :<port>` and SIGTERM → 500ms wait → SIGKILL. Then proceed as normal.
379	SweepStaleCocoon(COCOON_GRPC_PORT);
380
381	// Atom N1: resolve Node binary via NodeResolver (shipped → version
382	// managers → homebrew → PATH). Logs the pick + source for forensics.
383	// Overridable via `Pick=/absolute/path/to/node`.
384	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	// Spawn the process with error handling
397	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	// Establish Vine connection to Cocoon with exponential-backoff
420	// retry + child-exit detection.
421	//
422	// Prior policy was 20 × 1000 ms fixed poll. Under healthy timing
423	// (Cocoon binds at 150-600 ms) that wasted ~400 ms of idle time
424	// every boot; under a genuinely dead Cocoon (import error, killed
425	// process, stale bundle) it burned 20 full seconds before giving
426	// up with a generic "is Cocoon running?" hint.
427	//
428	// New policy:
429	//   - Initial 50 ms sleep, doubled per attempt up to a 2 s ceiling.
430	//   - Hard 20 s total-budget (unchanged) so the overall failure ceiling doesn't
431	//     regress for pathological slow-boot hardware.
432	//   - Before each sleep, poll `ChildProcess.try_wait()`: if Node has exited,
433	//     abandon the loop immediately with the exit status embedded in the error -
434	//     no point retrying against a dead process, and the exit code usually
435	//     reveals the import failure (1 = unhandled exception, 13 = invalid
436	//     module).
437	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				// Check if the Node child has already died. If yes,
477				// there is no point waiting any longer - report the
478				// real exit status so the dev log points at the real
479				// failure (import error, crash, oom kill) instead of
480				// the abstract "connect refused" message.
481				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) => { /* still running, keep trying */ },
506
507					Err(WaitErr) => {
508						// try_wait() itself failed; this is rare
509						// (would imply a kernel-level issue). Surface
510						// it but keep trying - the dial may still
511						// succeed on the next attempt.
512						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				// Exponential ramp with a 2 s ceiling. Doubling keeps
548				// the common case fast (4 attempts cover the first
549				// 750 ms) and the cold-boot case bounded.
550				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	// Brief delay to ensure Cocoon's gRPC service handlers are fully registered
561	// after bindAsync resolves (race condition on fast connections like attempt 1)
562	sleep(Duration::from_millis(200)).await;
563
564	// Construct initialization payload
565	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	// Send initialization request with timeout
572	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	// Validate handshake response
586	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	// Trigger startup extension activation. Cocoon is fully reactive -
608	// it won't activate any extensions until Mountain tells it to.
609	// Fire-and-forget: don't block on activation, and don't fail init if it errors.
610	//
611	// Stock VS Code fires a cascade of activation events at boot:
612	//   1. `*` - unconditional "activate anything that contributes *"
613	//   2. `onStartupFinished` - queued extensions whose start may be deferred
614	//      until after the first frame renders
615	//   3. `workspaceContains:<pattern>` for each pattern any extension
616	//      contributes, fired per matching workspace folder
617	//
618	// Previously only `*` fired, which meant a large class of extensions
619	// that gate on `workspaceContains:package.json`, `onStartupFinished`,
620	// or similar events never activated without user interaction. The
621	// added bursts below bring startup coverage in line with stock.
622	let SideCarId = SideCarIdentifier.clone();
623
624	let EnvironmentForActivation = Environment.clone();
625
626	tokio::spawn(async move {
627		// Small delay to let Cocoon finish processing the init response
628		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		// Webview panel restore: any panels persisted before the previous
646		// reload landed in global storage under `__webview_panel_state__`.
647		// Now that extensions are activated and their serializers are
648		// re-registered, ask Cocoon to deserialize each entry. Failures are
649		// per-panel - one broken serializer doesn't block the others.
650		{
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				// Clear the cache so panels aren't re-restored on the NEXT
695				// reload if the user didn't have them open this session.
696				let _ = EnvironmentForActivation
697					.UpdateStorageValue(true, PANEL_STATE_KEY.to_string(), None)
698					.await;
699			}
700		}
701
702		// Seed Cocoon's `__textDocuments` with any files already open in the
703		// workbench. Extensions that read `workspace.textDocuments` synchronously
704		// in their `activate()` function (rust-analyzer, ESLint, TypeScript) must
705		// see already-open editors rather than an empty array.
706		{
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		// Phase 2: workspaceContains: events. Iterate the scanned
730		// extension registry, collect every pattern contributed via the
731		// `workspaceContains:<pattern>` activation event, and fire the
732		// event if at least one workspace folder contains a path
733		// matching the pattern. Patterns are treated as filename globs
734		// relative to any workspace folder root; matching is done with
735		// a lightweight walk bounded by depth 3 and 2048 total visited
736		// entries per folder to cap worst-case cost on huge repos.
737		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		// Phase 3: onStartupFinished. Fire after the `*` burst has had a
806		// moment to complete so late-binding extensions layered on top
807		// of startup contributions resolve in the expected order.
808		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	// Store process handle for health monitoring and management
828	{
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	// Reset health monitor on successful initialization
841	{
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	// Start background health monitoring
850	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
859/// Background task that monitors Cocoon process health and logs crashes.
860///
861/// Once the child process has exited (or never existed), the monitor no
862/// longer has anything useful to say - it exits quietly instead of
863/// flooding the log with "No Cocoon process to monitor" every 5s, which
864/// was rendering the dev log unreadable after any Cocoon crash.
865async 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		// Check if we have a child process to monitor
872		if state_guard.ChildProcess.is_some() {
873			// Get process ID before checking status
874			let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
875
876			// Check if process is still running
877			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					// Process has exited (crashed or terminated)
886					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					// Update state
899					state_guard.IsRunning = false;
900
901					state_guard.ChildProcess = None;
902
903					COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
904
905					// Report health issue
906					{
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					// Log that automatic restart would be needed
915					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					// Process is still running
924					dev_log!(
925						"cocoon",
926						"[CocoonHealth] Cocoon process is healthy [PID: {}]",
927						process_id.unwrap_or(0)
928					);
929				},
930
931				Err(e) => {
932					// Error checking process status
933					dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
934
935					// Report health issue
936					{
937						let mut health = COCOON_HEALTH.lock().await;
938
939						health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
940					}
941				},
942			}
943		} else {
944			// No child process exists - log exactly once, then exit the
945			// monitor loop. Prior behaviour: flood the log with
946			// "No Cocoon process to monitor" every 5s forever after a
947			// crash, making the dev log unreadable. A future respawn will
948			// spawn a fresh monitor via `StartCocoon`.
949			dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
950
951			drop(state_guard);
952
953			return;
954		}
955	}
956}
957
958/// Atom I6: post-shutdown hard-kill. Called by RuntimeShutdown after the
959/// `$shutdown` gRPC notification has been sent (and either succeeded or
960/// timed out). Grabs the stored `Child` handle and force-terminates it if
961/// still alive, then resets COCOON_STATE. This plugs the "Mountain exits
962/// cleanly but child stays running" leak that leads to zombie-Cocoon
963/// zombies holding the gRPC port.
964///
965/// Call AFTER the graceful $shutdown attempt - we don't want to race the
966/// child's own cleanup. Safe to call with no stored child (no-op).
967pub 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				// Best-effort wait so the OS reaps and frees the port.
990				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
1002/// Build the complete environment variable map for the Cocoon subprocess.
1003///
1004/// Includes: VS Code pipe-logging vars, gRPC ports, PATH/HOME passthrough,
1005/// every `Product*`/`Tier*`/`Network*` var, the PascalCase Land allow-list
1006/// (PostHog, Extensions, kernel flags), and NODE_ENV / TAURI_ENV_DEBUG.
1007fn 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
1090/// Spawn background tasks that forward Cocoon's stdout and stderr into
1091/// Mountain's dev-log. Tagged lines (`[DEV:<TAG>] …`) are re-emitted under
1092/// the matching tag; plain lines stay under `cocoon`.
1093///
1094/// Uses `tauri::async_runtime::spawn` (not bare `tokio::spawn`) so the tasks
1095/// live on the same runtime handle that Tauri owns, ensuring they are polled
1096/// even while the calling async task is awaiting elsewhere.
1097fn 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
1187/// Atom I6: pre-boot sweep. TCP-probe the Cocoon gRPC port and kill any
1188/// stale process still bound to it. Prevents the EADDRINUSE cascade that
1189/// leaves the extension host in degraded mode when a prior Mountain exited
1190/// without cleaning up its child.
1191///
1192/// Behaviour:
1193/// - If the port answers a TCP connect, assume an owner is listening.
1194/// - Use `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` (macOS/Linux) to resolve the
1195///   PID. `lsof` is ubiquitous on macOS/Linux and doesn't require root for
1196///   local user-owned processes.
1197/// - SIGTERM first, 500ms grace window, then SIGKILL if still alive.
1198/// - Logs every step via `dev_log!("cocoon", …)` so the sweep is visible in
1199///   Mountain.dev.log without parsing stderr.
1200/// - Best-effort: failures don't abort Mountain boot. A real EADDRINUSE later
1201///   will surface via Cocoon's own bootstrap error.
1202fn SweepStaleCocoon(Port:u16) {
1203	use std::{net::TcpStream, time::Duration};
1204
1205	let Addr = format!("127.0.0.1:{}", Port);
1206
1207	// Cheap liveness probe. Timeout is aggressive - zombie ports answer
1208	// immediately; a clean port is ECONNREFUSED and returns instantly.
1209	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	// `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` → one PID per line.
1225	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	// Guard against self-kill. Mountain currently binds 50051, not Cocoon's
1264	// 50052, but belt-and-braces for future refactors.
1265	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		// Recheck - if still alive, escalate.
1286		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
1304/// Return the subset of `Patterns` for which at least one workspace folder
1305/// contains a matching file or directory. Patterns are interpreted the same
1306/// way VS Code does for `workspaceContains:<pattern>` activation events:
1307///
1308/// - A bare filename (no slash, no wildcards) matches an entry with that name
1309///   at the workspace root (e.g. `package.json`).
1310/// - A path with slashes but no wildcards matches a direct descendant relative
1311///   to the root (e.g. `.vscode/launch.json`).
1312/// - A glob with `**/` prefix matches any descendant up to a bounded depth.
1313/// - Any other wildcard form is matched via a simple segment-by-segment walk
1314///   honouring `*` (single segment) and `**` (any number of segments).
1315///
1316/// Matching is bounded to depth 3 and 4096 total directory entries per
1317/// workspace root to keep the cost sub-100 ms on large monorepos. Anything
1318/// deeper is rare for activation-event triggers; the trade-off is
1319/// documented in VS Code's own `ExtensionService.scanExtensions`.
1320fn 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		// Collect up to MAX_ENTRIES_PER_ROOT paths relative to the folder.
1335		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
1389/// Very small glob-matcher scoped to VS Code `workspaceContains:` syntax.
1390/// Supports literal paths, `*` (one path segment), and `**` (zero or more
1391/// segments). Case-sensitive per the VS Code spec.
1392fn 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		// `**` matches zero or more segments. Try consuming 0..=entry.len().
1415		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	// Minimal star-glob on a single segment: split by '*' and check each
1445	// fragment appears in order. Doesn't support `?` (rare in
1446	// workspaceContains patterns); unsupported glob chars fall through to
1447	// literal equality.
1448	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}