Skip to main content

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
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	// Pre-flight: Cocoon's bundle must exist or the spawned Node will
325	// die silently on the first `import()` and we'll sit through 20+
326	// seconds of `attempt N/M` retries with no diagnostic.
327	//
328	// Two layouts:
329	//
330	// 1. Bundle (.app): tauri.conf.json maps
331	//    `Element/Cocoon/Target/Bootstrap/Implementation/Cocoon` →
332	//    `Contents/Resources/Cocoon/Target/Bootstrap/Implementation/Cocoon`. The
333	//    Tauri resource resolver finds it directly.
334	//
335	// 2. Repo (dev binary): bootstrap is at
336	//    `Element/Mountain/scripts/cocoon/bootstrap-fork.js`, so walking `../../..`
337	//    from the bootstrap dir reaches `Element/` and `COCOON_BUNDLE_PROBE`
338	//    (`../Cocoon/Target/...`) descends into `Element/Cocoon/Target/...`.
339	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	// Atom I6: zombie-Cocoon sweep. If a prior Mountain exited without
373	// killing its child (segfault, SIGKILL, debugger detach, …), the stale
374	// node process keeps port COCOON_GRPC_PORT bound. The new Mountain's
375	// VineClient then "successfully connects" to the zombie while the
376	// freshly-spawned Cocoon fails to bind with EADDRINUSE, and the whole
377	// extension host enters degraded mode with zero extensions visible.
378	//
379	// Probe the port. If it answers, find the owning PID via `lsof -t -i
380	// :<port>` and SIGTERM → 500ms wait → SIGKILL. Then proceed as normal.
381	SweepStaleCocoon(COCOON_GRPC_PORT);
382
383	// Atom N1: resolve Node binary via NodeResolver (shipped → version
384	// managers → homebrew → PATH). Logs the pick + source for forensics.
385	// Overridable via `Pick=/absolute/path/to/node`.
386	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	// Spawn the process with error handling
399	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	// Establish Vine connection to Cocoon with exponential-backoff
422	// retry + child-exit detection.
423	//
424	// Prior policy was 20 × 1000 ms fixed poll. Under healthy timing
425	// (Cocoon binds at 150-600 ms) that wasted ~400 ms of idle time
426	// every boot; under a genuinely dead Cocoon (import error, killed
427	// process, stale bundle) it burned 20 full seconds before giving
428	// up with a generic "is Cocoon running?" hint.
429	//
430	// New policy:
431	//   - Initial 50 ms sleep, doubled per attempt up to a 2 s ceiling.
432	//   - Hard 20 s total-budget (unchanged) so the overall failure ceiling doesn't
433	//     regress for pathological slow-boot hardware.
434	//   - Before each sleep, poll `ChildProcess.try_wait()`: if Node has exited,
435	//     abandon the loop immediately with the exit status embedded in the error -
436	//     no point retrying against a dead process, and the exit code usually
437	//     reveals the import failure (1 = unhandled exception, 13 = invalid
438	//     module).
439	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				// Check if the Node child has already died. If yes,
479				// there is no point waiting any longer - report the
480				// real exit status so the dev log points at the real
481				// failure (import error, crash, oom kill) instead of
482				// the abstract "connect refused" message.
483				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) => { /* still running, keep trying */ },
508
509					Err(WaitErr) => {
510						// try_wait() itself failed; this is rare
511						// (would imply a kernel-level issue). Surface
512						// it but keep trying - the dial may still
513						// succeed on the next attempt.
514						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				// Exponential ramp with a 2 s ceiling. Doubling keeps
550				// the common case fast (4 attempts cover the first
551				// 750 ms) and the cold-boot case bounded.
552				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	// Brief delay to ensure Cocoon's gRPC service handlers are fully registered
563	// after bindAsync resolves (race condition on fast connections like attempt 1)
564	sleep(Duration::from_millis(200)).await;
565
566	// Construct initialization payload
567	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	// Send initialization request with timeout
574	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	// Validate handshake response
588	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	// Trigger startup extension activation. Cocoon is fully reactive -
610	// it won't activate any extensions until Mountain tells it to.
611	// Fire-and-forget: don't block on activation, and don't fail init if it errors.
612	//
613	// Stock VS Code fires a cascade of activation events at boot:
614	//   1. `*` - unconditional "activate anything that contributes *"
615	//   2. `onStartupFinished` - queued extensions whose start may be deferred
616	//      until after the first frame renders
617	//   3. `workspaceContains:<pattern>` for each pattern any extension
618	//      contributes, fired per matching workspace folder
619	//
620	// Previously only `*` fired, which meant a large class of extensions
621	// that gate on `workspaceContains:package.json`, `onStartupFinished`,
622	// or similar events never activated without user interaction. The
623	// added bursts below bring startup coverage in line with stock.
624	let SideCarId = SideCarIdentifier.clone();
625
626	let EnvironmentForActivation = Environment.clone();
627
628	tokio::spawn(async move {
629		// Small delay to let Cocoon finish processing the init response
630		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		// Webview panel restore: any panels persisted before the previous
650		// reload landed in global storage under `__webview_panel_state__`.
651		// Now that extensions are activated and their serializers are
652		// re-registered, ask Cocoon to deserialize each entry. Failures are
653		// per-panel - one broken serializer doesn't block the others.
654		{
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				// Clear the cache so panels aren't re-restored on the NEXT
699				// reload if the user didn't have them open this session.
700				let _ = EnvironmentForActivation
701					.UpdateStorageValue(true, PANEL_STATE_KEY.to_string(), None)
702					.await;
703			}
704		}
705
706		// Seed Cocoon's `__textDocuments` with any files already open in the
707		// workbench. Extensions that read `workspace.textDocuments` synchronously
708		// in their `activate()` function (rust-analyzer, ESLint, TypeScript) must
709		// see already-open editors rather than an empty array.
710		{
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		// Phase 2: workspaceContains: events. Iterate the scanned
739		// extension registry, collect every pattern contributed via the
740		// `workspaceContains:<pattern>` activation event, and fire the
741		// event if at least one workspace folder contains a path
742		// matching the pattern. Patterns are treated as filename globs
743		// relative to any workspace folder root; matching is done with
744		// a lightweight walk bounded by depth 3 and 2048 total visited
745		// entries per folder to cap worst-case cost on huge repos.
746		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		// Phase 3: onStartupFinished. Fire after the `*` burst has had a
822		// moment to complete so late-binding extensions layered on top
823		// of startup contributions resolve in the expected order.
824		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	// Store process handle for health monitoring and management
845	{
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	// Reset health monitor on successful initialization
858	{
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	// Start background health monitoring
867	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
876/// Background task that monitors Cocoon process health and logs crashes.
877///
878/// Once the child process has exited (or never existed), the monitor no
879/// longer has anything useful to say - it exits quietly instead of
880/// flooding the log with "No Cocoon process to monitor" every 5s, which
881/// was rendering the dev log unreadable after any Cocoon crash.
882async 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		// Check if we have a child process to monitor
889		if state_guard.ChildProcess.is_some() {
890			// Get process ID before checking status
891			let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
892
893			// Check if process is still running
894			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					// Process has exited (crashed or terminated)
903					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					// Update state
916					state_guard.IsRunning = false;
917
918					state_guard.ChildProcess = None;
919
920					COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
921
922					// Report health issue
923					{
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					// Log that automatic restart would be needed
932					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					// Process is still running
941					dev_log!(
942						"cocoon",
943						"[CocoonHealth] Cocoon process is healthy [PID: {}]",
944						process_id.unwrap_or(0)
945					);
946				},
947
948				Err(e) => {
949					// Error checking process status
950					dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
951
952					// Report health issue
953					{
954						let mut health = COCOON_HEALTH.lock().await;
955
956						health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
957					}
958				},
959			}
960		} else {
961			// No child process exists - log exactly once, then exit the
962			// monitor loop. Prior behaviour: flood the log with
963			// "No Cocoon process to monitor" every 5s forever after a
964			// crash, making the dev log unreadable. A future respawn will
965			// spawn a fresh monitor via `StartCocoon`.
966			dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
967
968			drop(state_guard);
969
970			return;
971		}
972	}
973}
974
975/// Atom I6: post-shutdown hard-kill. Called by RuntimeShutdown after the
976/// `$shutdown` gRPC notification has been sent (and either succeeded or
977/// timed out). Grabs the stored `Child` handle and force-terminates it if
978/// still alive, then resets COCOON_STATE. This plugs the "Mountain exits
979/// cleanly but child stays running" leak that leads to zombie-Cocoon
980/// zombies holding the gRPC port.
981///
982/// Call AFTER the graceful $shutdown attempt - we don't want to race the
983/// child's own cleanup. Safe to call with no stored child (no-op).
984pub 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				// Best-effort wait so the OS reaps and frees the port.
1007				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
1019/// Build the complete environment variable map for the Cocoon subprocess.
1020///
1021/// Includes: VS Code pipe-logging vars, gRPC ports, PATH/HOME passthrough,
1022/// every `Product*`/`Tier*`/`Network*` var, the PascalCase Land allow-list
1023/// (PostHog, Extensions, kernel flags), and NODE_ENV / TAURI_ENV_DEBUG.
1024fn 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
1107/// Spawn background tasks that forward Cocoon's stdout and stderr into
1108/// Mountain's dev-log. Tagged lines (`[DEV:<TAG>] …`) are re-emitted under
1109/// the matching tag; plain lines stay under `cocoon`.
1110///
1111/// Uses `tauri::async_runtime::spawn` (not bare `tokio::spawn`) so the tasks
1112/// live on the same runtime handle that Tauri owns, ensuring they are polled
1113/// even while the calling async task is awaiting elsewhere.
1114fn 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
1219/// Atom I6: pre-boot sweep. TCP-probe the Cocoon gRPC port and kill any
1220/// stale process still bound to it. Prevents the EADDRINUSE cascade that
1221/// leaves the extension host in degraded mode when a prior Mountain exited
1222/// without cleaning up its child.
1223///
1224/// Behaviour:
1225/// - If the port answers a TCP connect, assume an owner is listening.
1226/// - Use `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` (macOS/Linux) to resolve the
1227///   PID. `lsof` is ubiquitous on macOS/Linux and doesn't require root for
1228///   local user-owned processes.
1229/// - SIGTERM first, 500ms grace window, then SIGKILL if still alive.
1230/// - Logs every step via `dev_log!("cocoon", …)` so the sweep is visible in
1231///   Mountain.dev.log without parsing stderr.
1232/// - Best-effort: failures don't abort Mountain boot. A real EADDRINUSE later
1233///   will surface via Cocoon's own bootstrap error.
1234fn SweepStaleCocoon(Port:u16) {
1235	use std::{net::TcpStream, time::Duration};
1236
1237	let Addr = format!("127.0.0.1:{}", Port);
1238
1239	// Cheap liveness probe. Timeout is aggressive - zombie ports answer
1240	// immediately; a clean port is ECONNREFUSED and returns instantly.
1241	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	// `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` → one PID per line.
1257	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	// Guard against self-kill. Mountain currently binds 50051, not Cocoon's
1296	// 50052, but belt-and-braces for future refactors.
1297	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		// Recheck - if still alive, escalate.
1318		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
1336/// Return the subset of `Patterns` for which at least one workspace folder
1337/// contains a matching file or directory. Patterns are interpreted the same
1338/// way VS Code does for `workspaceContains:<pattern>` activation events:
1339///
1340/// - A bare filename (no slash, no wildcards) matches an entry with that name
1341///   at the workspace root (e.g. `package.json`).
1342/// - A path with slashes but no wildcards matches a direct descendant relative
1343///   to the root (e.g. `.vscode/launch.json`).
1344/// - A glob with `**/` prefix matches any descendant up to a bounded depth.
1345/// - Any other wildcard form is matched via a simple segment-by-segment walk
1346///   honouring `*` (single segment) and `**` (any number of segments).
1347///
1348/// Matching is bounded to depth 3 and 4096 total directory entries per
1349/// workspace root to keep the cost sub-100 ms on large monorepos. Anything
1350/// deeper is rare for activation-event triggers; the trade-off is
1351/// documented in VS Code's own `ExtensionService.scanExtensions`.
1352fn 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		// Collect up to MAX_ENTRIES_PER_ROOT paths relative to the folder.
1367		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
1421/// Very small glob-matcher scoped to VS Code `workspaceContains:` syntax.
1422/// Supports literal paths, `*` (one path segment), and `**` (zero or more
1423/// segments). Case-sensitive per the VS Code spec.
1424fn 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		// `**` matches zero or more segments. Try consuming 0..=entry.len().
1447		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	// Minimal star-glob on a single segment: split by '*' and check each
1477	// fragment appears in order. Doesn't support `?` (rare in
1478	// workspaceContains patterns); unsupported glob chars fall through to
1479	// literal equality.
1480	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}