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
120#[allow(dead_code)]
121const MAX_RESTART_ATTEMPTS:u32 = 3;
122
123#[allow(dead_code)]
124const RESTART_WINDOW_SECONDS:u64 = 300;
125
126/// Global state for tracking Cocoon process lifecycle
127#[allow(dead_code)]
128struct CocoonProcessState {
129	ChildProcess:Option<Child>,
130
131	IsRunning:bool,
132
133	StartTime:Option<tokio::time::Instant>,
134
135	RestartCount:u32,
136
137	LastRestartTime:Option<tokio::time::Instant>,
138}
139
140impl Default for CocoonProcessState {
141	fn default() -> Self {
142		Self {
143			ChildProcess:None,
144
145			IsRunning:false,
146
147			StartTime:None,
148
149			RestartCount:0,
150
151			LastRestartTime:None,
152		}
153	}
154}
155
156// Global state for Cocoon process management
157lazy_static::lazy_static! {
158
159	static ref COCOON_STATE: Arc<Mutex<CocoonProcessState>> =
160		Arc::new(Mutex::new(CocoonProcessState::default()));
161
162	static ref COCOON_HEALTH: Arc<Mutex<HealthMonitor>> =
163		Arc::new(Mutex::new(HealthMonitor::new()));
164}
165
166/// Last-known PID of the Cocoon child process. Mirrored here so callers can
167/// read it without taking the async `COCOON_STATE` mutex (e.g. from IPC
168/// handlers such as `extensionHostStarter:start`). Set after spawn and
169/// cleared on shutdown. `0` means "not running".
170static COCOON_PID:std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
171
172/// Return the Cocoon child process's OS PID, or `None` if Cocoon has not
173/// been spawned (or has exited).
174pub fn GetCocoonPid() -> Option<u32> {
175	match COCOON_PID.load(std::sync::atomic::Ordering::Relaxed) {
176		0 => None,
177
178		Pid => Some(Pid),
179	}
180}
181
182/// The main entry point for initializing the Cocoon sidecar process manager.
183///
184/// This orchestrates the complete initialization sequence including:
185/// - Validating feature flags and dependencies
186/// - Launching the Cocoon process with proper configuration
187/// - Establishing gRPC communication
188/// - Performing the initialization handshake
189/// - Setting up process health monitoring
190///
191/// # Arguments
192///
193/// * `ApplicationHandle` - Tauri application handle for path resolution
194/// * `Environment` - Mountain environment containing application state and
195///   services
196///
197/// # Returns
198///
199/// * `Ok(())` - Cocoon initialized successfully and ready to accept extension
200///   requests
201/// * `Err(CommonError)` - Initialization failed with detailed error context
202///
203/// # Errors
204///
205/// - `FileSystemNotFound`: Bootstrap script not found
206/// - `IPCError`: Failed to spawn process or establish gRPC connection
207///
208/// # Example
209///
210/// ```rust,no_run
211/// use crate::Source::ProcessManagement::CocoonManagement::InitializeCocoon;
212///
213/// InitializeCocoon(&app_handle, &environment).await?;
214/// ```
215pub async fn InitializeCocoon(
216	ApplicationHandle:&AppHandle,
217
218	Environment:&Arc<MountainEnvironment>,
219) -> Result<(), CommonError> {
220	dev_log!("cocoon", "[CocoonManagement] Initializing Cocoon sidecar manager...");
221
222	// Atom N1: `debug-mountain-only` / `release-mountain-only` profiles set
223	// Spawn=false so Mountain boots without the extension host.
224	// Extension-related IPC returns the empty-state envelope; the workbench
225	// loads but no extension activates. Useful for integration tests that
226	// exercise Mountain in isolation and for the smallest shippable surface.
227	if matches!(std::env::var("Spawn").as_deref(), Ok("0") | Ok("false")) {
228		dev_log!("cocoon", "[CocoonManagement] Skipping spawn (Spawn=false)");
229
230		return Ok(());
231	}
232
233	#[cfg(feature = "ExtensionHostCocoon")]
234	{
235		LaunchAndManageCocoonSideCar(ApplicationHandle.clone(), Environment.clone()).await
236	}
237
238	#[cfg(not(feature = "ExtensionHostCocoon"))]
239	{
240		dev_log!(
241			"cocoon",
242			"[CocoonManagement] 'ExtensionHostCocoon' feature is disabled. Cocoon will not be launched."
243		);
244
245		Ok(())
246	}
247}
248
249/// Spawns the Cocoon process, manages its communication channels, and performs
250/// the complete initialization handshake sequence.
251///
252/// This function implements the complete Cocoon lifecycle:
253/// 1. Validates bootstrap script availability
254/// 2. Constructs environment variables for IPC and logging
255/// 3. Spawns Node.js process with proper IO redirection
256/// 4. Captures stdout/stderr for logging
257/// 5. Waits for gRPC server to be ready
258/// 6. Establishes Vine connection
259/// 7. Sends initialization payload and validates response
260///
261/// # Arguments
262///
263/// * `ApplicationHandle` - Tauri application handle for resolving resource
264///   paths
265/// * `Environment` - Mountain environment containing application state
266///
267/// # Returns
268///
269/// * `Ok(())` - Cocoon process spawned, connected, and initialized successfully
270/// * `Err(CommonError)` - Any failure during the initialization sequence
271///
272/// # Errors
273///
274/// - `FileSystemNotFound`: Bootstrap script not found in resources
275/// - `IPCError`: Failed to spawn process, connect gRPC, or complete handshake
276///
277/// # Lifecycle
278///
279/// The process runs as a background task with IO redirection for logging.
280/// Process failures are logged but not automatically restarted (callers should
281/// implement restart strategies based on their requirements).
282async fn LaunchAndManageCocoonSideCar(
283	ApplicationHandle:AppHandle,
284
285	Environment:Arc<MountainEnvironment>,
286) -> Result<(), CommonError> {
287	let SideCarIdentifier = COCOON_SIDE_CAR_IDENTIFIER.to_string();
288
289	let path_resolver:PathResolver<Wry> = ApplicationHandle.path().clone();
290
291	// Resolve bootstrap script path.
292	// 1) Try Tauri bundled resources (production builds).
293	// 2) Fallback: resolve relative to the executable (dev builds). Dev layout:
294	//    Target/debug/binary → ../../scripts/cocoon/bootstrap-fork.js
295	let ScriptPath = path_resolver
296		.resolve(BOOTSTRAP_SCRIPT_PATH, BaseDirectory::Resource)
297		.ok()
298		.filter(|P| P.exists())
299		.or_else(|| {
300			std::env::current_exe().ok().and_then(|Exe| {
301				let MountainRoot = Exe.parent()?.parent()?.parent()?;
302				let Candidate = MountainRoot.join(BOOTSTRAP_SCRIPT_PATH);
303				if Candidate.exists() { Some(Candidate) } else { None }
304			})
305		})
306		.ok_or_else(|| {
307			CommonError::FileSystemNotFound(
308				format!(
309					"Cocoon bootstrap script '{}' not found in resources or relative to executable",
310					BOOTSTRAP_SCRIPT_PATH
311				)
312				.into(),
313			)
314		})?;
315
316	dev_log!(
317		"cocoon",
318		"[CocoonManagement] Found bootstrap script at: {}",
319		ScriptPath.display()
320	);
321
322	crate::dev_log!("cocoon", "bootstrap script: {}", ScriptPath.display());
323
324	// 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			return;
644		}
645		dev_log!("cocoon", "[CocoonManagement] Startup extensions activation (*) triggered");
646
647		// Phase 2: workspaceContains: events. Iterate the scanned
648		// extension registry, collect every pattern contributed via the
649		// `workspaceContains:<pattern>` activation event, and fire the
650		// event if at least one workspace folder contains a path
651		// matching the pattern. Patterns are treated as filename globs
652		// relative to any workspace folder root; matching is done with
653		// a lightweight walk bounded by depth 3 and 2048 total visited
654		// entries per folder to cap worst-case cost on huge repos.
655		let WorkspacePatterns = {
656			let AppState = &EnvironmentForActivation.ApplicationState;
657			let Folders:Vec<std::path::PathBuf> = AppState
658				.Workspace
659				.WorkspaceFolders
660				.lock()
661				.ok()
662				.map(|Guard| {
663					Guard
664						.iter()
665						.filter_map(|Folder| Folder.URI.to_file_path().ok())
666						.collect::<Vec<_>>()
667				})
668				.unwrap_or_default();
669
670			let Patterns:Vec<String> = AppState
671				.Extension
672				.ScannedExtensions
673				.ScannedExtensions
674				.lock()
675				.ok()
676				.map(|Guard| {
677					let mut Set:std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
678					for Description in Guard.values() {
679						if let Some(Events) = &Description.ActivationEvents {
680							for Event in Events {
681								if let Some(Pattern) = Event.strip_prefix("workspaceContains:") {
682									Set.insert(Pattern.to_string());
683								}
684							}
685						}
686					}
687					Set.into_iter().collect()
688				})
689				.unwrap_or_default();
690
691			(Folders, Patterns)
692		};
693
694		let (WorkspaceFolders, Patterns):(Vec<std::path::PathBuf>, Vec<String>) = WorkspacePatterns;
695		if !WorkspaceFolders.is_empty() && !Patterns.is_empty() {
696			let Matched = FindMatchingWorkspaceContainsPatterns(&WorkspaceFolders, &Patterns);
697			dev_log!(
698				"exthost",
699				"[CocoonManagement] workspaceContains scan: {} pattern(s) matched across {} folder(s)",
700				Matched.len(),
701				WorkspaceFolders.len()
702			);
703			for Pattern in Matched {
704				let Event = format!("workspaceContains:{}", Pattern);
705				if let Err(Error) = Vine::Client::SendRequest::Fn(
706					&SideCarId,
707					"$activateByEvent".to_string(),
708					serde_json::json!({ "activationEvent": Event }),
709					30_000,
710				)
711				.await
712				{
713					dev_log!(
714						"cocoon",
715						"warn: [CocoonManagement] $activateByEvent({}) failed: {}",
716						Event,
717						Error
718					);
719				}
720			}
721		}
722
723		// Phase 3: onStartupFinished. Fire after the `*` burst has had a
724		// moment to complete so late-binding extensions layered on top
725		// of startup contributions resolve in the expected order.
726		sleep(Duration::from_millis(2_000)).await;
727		if let Err(Error) = Vine::Client::SendRequest::Fn(
728			&SideCarId,
729			"$activateByEvent".to_string(),
730			serde_json::json!({ "activationEvent": "onStartupFinished" }),
731			30_000,
732		)
733		.await
734		{
735			dev_log!(
736				"cocoon",
737				"warn: [CocoonManagement] $activateByEvent(onStartupFinished) failed: {}",
738				Error
739			);
740		} else {
741			dev_log!("cocoon", "[CocoonManagement] onStartupFinished activation triggered");
742		}
743	});
744
745	// Store process handle for health monitoring and management
746	{
747		let mut state = COCOON_STATE.lock().await;
748
749		state.ChildProcess = Some(ChildProcess);
750
751		state.IsRunning = true;
752
753		state.StartTime = Some(tokio::time::Instant::now());
754
755		dev_log!("cocoon", "[CocoonManagement] Process state updated: Running");
756	}
757
758	// Reset health monitor on successful initialization
759	{
760		let mut health = COCOON_HEALTH.lock().await;
761
762		health.ClearIssues();
763
764		dev_log!("cocoon", "[CocoonManagement] Health monitor reset to active state");
765	}
766
767	// Start background health monitoring
768	let state_clone = Arc::clone(&COCOON_STATE);
769
770	tokio::spawn(monitor_cocoon_health_task(state_clone));
771
772	dev_log!("cocoon", "[CocoonManagement] Background health monitoring started");
773
774	Ok(())
775}
776
777/// Background task that monitors Cocoon process health and logs crashes.
778///
779/// Once the child process has exited (or never existed), the monitor no
780/// longer has anything useful to say - it exits quietly instead of
781/// flooding the log with "No Cocoon process to monitor" every 5s, which
782/// was rendering the dev log unreadable after any Cocoon crash.
783async fn monitor_cocoon_health_task(state:Arc<Mutex<CocoonProcessState>>) {
784	loop {
785		tokio::time::sleep(Duration::from_secs(HEALTH_CHECK_INTERVAL_SECONDS)).await;
786
787		let mut state_guard = state.lock().await;
788
789		// Check if we have a child process to monitor
790		if state_guard.ChildProcess.is_some() {
791			// Get process ID before checking status
792			let process_id = state_guard.ChildProcess.as_ref().map(|c| c.id().unwrap_or(0));
793
794			// Check if process is still running
795			let exit_status = {
796				let child = state_guard.ChildProcess.as_mut().unwrap();
797
798				child.try_wait()
799			};
800
801			match exit_status {
802				Ok(Some(exit_code)) => {
803					// Process has exited (crashed or terminated)
804					let uptime = state_guard.StartTime.map(|t| t.elapsed().as_secs()).unwrap_or(0);
805
806					let exit_code_num = exit_code.code().unwrap_or(-1);
807
808					dev_log!(
809						"cocoon",
810						"warn: [CocoonHealth] Cocoon process crashed [PID: {}] [Exit Code: {}] [Uptime: {}s]",
811						process_id.unwrap_or(0),
812						exit_code_num,
813						uptime
814					);
815
816					// Update state
817					state_guard.IsRunning = false;
818
819					state_guard.ChildProcess = None;
820
821					COCOON_PID.store(0, std::sync::atomic::Ordering::Relaxed);
822
823					// Report health issue
824					{
825						let mut health = COCOON_HEALTH.lock().await;
826
827						health.AddIssue(HealthIssue::Custom(format!("ProcessCrashed (Exit code: {})", exit_code_num)));
828
829						dev_log!("cocoon", "warn: [CocoonHealth] Health score: {}", health.HealthScore);
830					}
831
832					// Log that automatic restart would be needed
833					dev_log!(
834						"cocoon",
835						"warn: [CocoonHealth] CRASH DETECTED: Cocoon process has crashed and must be restarted \
836						 manually or via application reinitialization"
837					);
838				},
839
840				Ok(None) => {
841					// Process is still running
842					dev_log!(
843						"cocoon",
844						"[CocoonHealth] Cocoon process is healthy [PID: {}]",
845						process_id.unwrap_or(0)
846					);
847				},
848
849				Err(e) => {
850					// Error checking process status
851					dev_log!("cocoon", "warn: [CocoonHealth] Error checking process status: {}", e);
852
853					// Report health issue
854					{
855						let mut health = COCOON_HEALTH.lock().await;
856
857						health.AddIssue(HealthIssue::Custom(format!("ProcessCheckError: {}", e)));
858					}
859				},
860			}
861		} else {
862			// No child process exists - log exactly once, then exit the
863			// monitor loop. Prior behaviour: flood the log with
864			// "No Cocoon process to monitor" every 5s forever after a
865			// crash, making the dev log unreadable. A future respawn will
866			// spawn a fresh monitor via `StartCocoon`.
867			dev_log!("cocoon", "[CocoonHealth] No Cocoon process to monitor - exiting monitor loop");
868
869			drop(state_guard);
870
871			return;
872		}
873	}
874}
875
876/// Atom I6: post-shutdown hard-kill. Called by RuntimeShutdown after the
877/// `$shutdown` gRPC notification has been sent (and either succeeded or
878/// timed out). Grabs the stored `Child` handle and force-terminates it if
879/// still alive, then resets COCOON_STATE. This plugs the "Mountain exits
880/// cleanly but child stays running" leak that leads to zombie-Cocoon
881/// zombies holding the gRPC port.
882///
883/// Call AFTER the graceful $shutdown attempt - we don't want to race the
884/// child's own cleanup. Safe to call with no stored child (no-op).
885pub async fn HardKillCocoon() {
886	let mut State = COCOON_STATE.lock().await;
887
888	if let Some(mut Child) = State.ChildProcess.take() {
889		let Pid = Child.id().unwrap_or(0);
890
891		match Child.try_wait() {
892			Ok(Some(_Status)) => {
893				dev_log!("cocoon", "[CocoonShutdown] Child PID {} already exited; clearing handle.", Pid);
894			},
895
896			Ok(None) => {
897				dev_log!(
898					"cocoon",
899					"[CocoonShutdown] Child PID {} still alive after $shutdown; sending SIGKILL.",
900					Pid
901				);
902
903				if let Err(Error) = Child.start_kill() {
904					dev_log!("cocoon", "warn: [CocoonShutdown] start_kill failed on PID {}: {}", Pid, Error);
905				}
906
907				// Best-effort wait so the OS reaps and frees the port.
908				let _ = tokio::time::timeout(std::time::Duration::from_secs(2), Child.wait()).await;
909			},
910
911			Err(Error) => {
912				dev_log!("cocoon", "warn: [CocoonShutdown] try_wait failed on PID {}: {}", Pid, Error);
913			},
914		}
915	}
916
917	State.IsRunning = false;
918}
919
920/// Build the complete environment variable map for the Cocoon subprocess.
921///
922/// Includes: VS Code pipe-logging vars, gRPC ports, PATH/HOME passthrough,
923/// every `Product*`/`Tier*`/`Network*` var, the PascalCase Land allow-list
924/// (PostHog, Extensions, kernel flags), and NODE_ENV / TAURI_ENV_DEBUG.
925fn BuildCocoonEnvironment() -> HashMap<String, String> {
926	const LAND_ENV_ALLOW_LIST:&[&str] = &[
927		"Authorize",
928		"Beam",
929		"Report",
930		"Brand",
931		"Replay",
932		"Ask",
933		"Throttle",
934		"Buffer",
935		"Batch",
936		"Cap",
937		"Capture",
938		"OTLPEndpoint",
939		"OTLPEnabled",
940		"Pick",
941		"Require",
942		"Lodge",
943		"Extend",
944		"Probe",
945		"Ship",
946		"Wire",
947		"Install",
948		"Mute",
949		"Skip",
950		"Spawn",
951		"Render",
952		"Walk",
953		"Trace",
954		"Record",
955		"Profile",
956		"Diagnose",
957		"Resolve",
958		"Open",
959		"Warn",
960		"Catch",
961		"Source",
962		"Track",
963		"Defer",
964		"Boot",
965		"Pack",
966	];
967
968	let mut Env = HashMap::new();
969
970	Env.insert("VSCODE_PIPE_LOGGING".into(), "true".into());
971	Env.insert("VSCODE_VERBOSE_LOGGING".into(), "true".into());
972	Env.insert("VSCODE_PARENT_PID".into(), std::process::id().to_string());
973	Env.insert("MOUNTAIN_GRPC_PORT".into(), MOUNTAIN_GRPC_PORT.to_string());
974	Env.insert("COCOON_GRPC_PORT".into(), COCOON_GRPC_PORT.to_string());
975
976	for Key in ["PATH", "HOME"] {
977		if let Ok(V) = std::env::var(Key) {
978			Env.insert(Key.into(), V);
979		}
980	}
981
982	for (Key, Value) in std::env::vars() {
983		if Key.starts_with("Product")
984			|| Key.starts_with("Tier")
985			|| Key.starts_with("Network")
986			|| LAND_ENV_ALLOW_LIST.contains(&Key.as_str())
987		{
988			Env.insert(Key, Value);
989		}
990	}
991
992	for Key in ["NODE_ENV", "TAURI_ENV_DEBUG"] {
993		if let Ok(V) = std::env::var(Key) {
994			Env.insert(Key.into(), V);
995		}
996	}
997
998	Env
999}
1000
1001/// Spawn background tasks that forward Cocoon's stdout and stderr into
1002/// Mountain's dev-log. Tagged lines (`[DEV:<TAG>] …`) are re-emitted under
1003/// the matching tag; plain lines stay under `cocoon`.
1004///
1005/// Uses `tauri::async_runtime::spawn` (not bare `tokio::spawn`) so the tasks
1006/// live on the same runtime handle that Tauri owns, ensuring they are polled
1007/// even while the calling async task is awaiting elsewhere.
1008fn SpawnCocoonIoForwarders(Process:&mut tokio::process::Child) {
1009	dev_log!(
1010		"cocoon",
1011		"[CocoonIO] Spawning IO forwarder tasks (stdout={}, stderr={})",
1012		Process.stdout.is_some(),
1013		Process.stderr.is_some()
1014	);
1015
1016	if let Some(Stdout) = Process.stdout.take() {
1017		tauri::async_runtime::spawn(async move {
1018			let mut Lines = BufReader::new(Stdout).lines();
1019			loop {
1020				match Lines.next_line().await {
1021					Ok(Some(Line)) => {
1022						if let Some(Tag) = ExtractDevTag(&Line) {
1023							match Tag.as_str() {
1024								"bootstrap-stage" => dev_log!("bootstrap-stage", "[Cocoon stdout] {}", Line),
1025								"ext-activate" => dev_log!("ext-activate", "[Cocoon stdout] {}", Line),
1026								"config-prime" => dev_log!("config-prime", "[Cocoon stdout] {}", Line),
1027								"breaker" => dev_log!("breaker", "[Cocoon stdout] {}", Line),
1028								_ => dev_log!("cocoon", "[Cocoon stdout] {}", Line),
1029							}
1030						} else {
1031							dev_log!("cocoon", "[Cocoon stdout] {}", Line);
1032						}
1033					},
1034					Ok(None) => {
1035						dev_log!("cocoon", "[CocoonIO] stdout pipe closed (EOF)");
1036						break;
1037					},
1038					Err(Error) => {
1039						dev_log!("cocoon", "warn: [CocoonIO] stdout read error: {}", Error);
1040						break;
1041					},
1042				}
1043			}
1044		});
1045	} else {
1046		dev_log!("cocoon", "warn: [CocoonIO] stdout pipe not available (Stdio::piped() not set?)");
1047	}
1048
1049	if let Some(Stderr) = Process.stderr.take() {
1050		tauri::async_runtime::spawn(async move {
1051			let mut Lines = BufReader::new(Stderr).lines();
1052			let mut SuppressStack = false;
1053			loop {
1054				match Lines.next_line().await {
1055					Ok(Some(Line)) => {
1056						let T = Line.trim_start();
1057						let IsFrame = T.starts_with("at ") || T.starts_with("code: '") || T == "}" || T.is_empty();
1058						if SuppressStack && IsFrame {
1059							dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
1060							continue;
1061						}
1062						SuppressStack = false;
1063						let Benign = Line.contains(": is already signed")
1064							|| Line.contains(": replacing existing signature")
1065							|| Line.contains("DeprecationWarning:")
1066							|| Line.contains("--trace-deprecation")
1067							|| Line.contains("--trace-warnings");
1068						let BenignHead = Line.contains("EntryNotFound (FileSystemError):")
1069							|| Line.contains("FileNotFound (FileSystemError):")
1070							|| Line.contains("[LandFix:UnhandledRejection]")
1071							|| Line.starts_with("[Patcher] unhandledRejection:")
1072							|| Line.starts_with("[Patcher] uncaughtException:");
1073						if BenignHead {
1074							SuppressStack = true;
1075						}
1076						if Benign || BenignHead {
1077							dev_log!("cocoon-stderr-verbose", "[Cocoon stderr] {}", Line);
1078						} else {
1079							dev_log!("cocoon", "warn: [Cocoon stderr] {}", Line);
1080						}
1081					},
1082					Ok(None) => {
1083						dev_log!("cocoon", "[CocoonIO] stderr pipe closed (EOF)");
1084						break;
1085					},
1086					Err(Error) => {
1087						dev_log!("cocoon", "warn: [CocoonIO] stderr read error: {}", Error);
1088						break;
1089					},
1090				}
1091			}
1092		});
1093	} else {
1094		dev_log!("cocoon", "warn: [CocoonIO] stderr pipe not available");
1095	}
1096}
1097
1098/// Atom I6: pre-boot sweep. TCP-probe the Cocoon gRPC port and kill any
1099/// stale process still bound to it. Prevents the EADDRINUSE cascade that
1100/// leaves the extension host in degraded mode when a prior Mountain exited
1101/// without cleaning up its child.
1102///
1103/// Behaviour:
1104/// - If the port answers a TCP connect, assume an owner is listening.
1105/// - Use `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` (macOS/Linux) to resolve the
1106///   PID. `lsof` is ubiquitous on macOS/Linux and doesn't require root for
1107///   local user-owned processes.
1108/// - SIGTERM first, 500ms grace window, then SIGKILL if still alive.
1109/// - Logs every step via `dev_log!("cocoon", …)` so the sweep is visible in
1110///   Mountain.dev.log without parsing stderr.
1111/// - Best-effort: failures don't abort Mountain boot. A real EADDRINUSE later
1112///   will surface via Cocoon's own bootstrap error.
1113fn SweepStaleCocoon(Port:u16) {
1114	use std::{net::TcpStream, time::Duration};
1115
1116	let Addr = format!("127.0.0.1:{}", Port);
1117
1118	// Cheap liveness probe. Timeout is aggressive - zombie ports answer
1119	// immediately; a clean port is ECONNREFUSED and returns instantly.
1120	let Probe =
1121		TcpStream::connect_timeout(&Addr.parse().expect("valid socket addr literal"), Duration::from_millis(200));
1122
1123	if Probe.is_err() {
1124		dev_log!("cocoon", "[CocoonSweep] Port {} is clean (no prior listener).", Port);
1125
1126		return;
1127	}
1128
1129	dev_log!(
1130		"cocoon",
1131		"[CocoonSweep] Port {} has a listener - attempting to resolve owner via lsof.",
1132		Port
1133	);
1134
1135	// `lsof -nP -iTCP:<port> -sTCP:LISTEN -t` → one PID per line.
1136	let LsofOutput = std::process::Command::new("lsof")
1137		.args(["-nP", &format!("-iTCP:{}", Port), "-sTCP:LISTEN", "-t"])
1138		.output();
1139
1140	let Output = match LsofOutput {
1141		Ok(O) => O,
1142
1143		Err(Error) => {
1144			dev_log!(
1145				"cocoon",
1146				"warn: [CocoonSweep] lsof unavailable ({}). Skipping sweep; Cocoon spawn may fail with EADDRINUSE.",
1147				Error
1148			);
1149
1150			return;
1151		},
1152	};
1153
1154	if !Output.status.success() {
1155		dev_log!("cocoon", "warn: [CocoonSweep] lsof exited non-zero. Skipping sweep.");
1156
1157		return;
1158	}
1159
1160	let Stdout = String::from_utf8_lossy(&Output.stdout);
1161
1162	let Pids:Vec<i32> = Stdout.lines().filter_map(|L| L.trim().parse::<i32>().ok()).collect();
1163
1164	if Pids.is_empty() {
1165		dev_log!(
1166			"cocoon",
1167			"warn: [CocoonSweep] Port {} answered but lsof found no LISTEN PID - giving up.",
1168			Port
1169		);
1170
1171		return;
1172	}
1173
1174	// Guard against self-kill. Mountain currently binds 50051, not Cocoon's
1175	// 50052, but belt-and-braces for future refactors.
1176	let SelfPid = std::process::id() as i32;
1177
1178	for Pid in Pids {
1179		if Pid == SelfPid {
1180			dev_log!(
1181				"cocoon",
1182				"warn: [CocoonSweep] Port {} owned by Mountain itself (PID {}); refusing to kill.",
1183				Port,
1184				Pid
1185			);
1186
1187			continue;
1188		}
1189
1190		dev_log!("cocoon", "[CocoonSweep] Killing stale PID {} (SIGTERM).", Pid);
1191
1192		let _ = std::process::Command::new("kill").arg(Pid.to_string()).status();
1193
1194		std::thread::sleep(Duration::from_millis(500));
1195
1196		// Recheck - if still alive, escalate.
1197		let StillAlive = std::process::Command::new("kill")
1198			.args(["-0", &Pid.to_string()])
1199			.status()
1200			.map(|S| S.success())
1201			.unwrap_or(false);
1202
1203		if StillAlive {
1204			dev_log!("cocoon", "warn: [CocoonSweep] PID {} survived SIGTERM; sending SIGKILL.", Pid);
1205
1206			let _ = std::process::Command::new("kill").args(["-9", &Pid.to_string()]).status();
1207
1208			std::thread::sleep(Duration::from_millis(200));
1209		}
1210
1211		dev_log!("cocoon", "[CocoonSweep] PID {} reaped.", Pid);
1212	}
1213}
1214
1215/// Return the subset of `Patterns` for which at least one workspace folder
1216/// contains a matching file or directory. Patterns are interpreted the same
1217/// way VS Code does for `workspaceContains:<pattern>` activation events:
1218///
1219/// - A bare filename (no slash, no wildcards) matches an entry with that name
1220///   at the workspace root (e.g. `package.json`).
1221/// - A path with slashes but no wildcards matches a direct descendant relative
1222///   to the root (e.g. `.vscode/launch.json`).
1223/// - A glob with `**/` prefix matches any descendant up to a bounded depth.
1224/// - Any other wildcard form is matched via a simple segment-by-segment walk
1225///   honouring `*` (single segment) and `**` (any number of segments).
1226///
1227/// Matching is bounded to depth 3 and 4096 total directory entries per
1228/// workspace root to keep the cost sub-100 ms on large monorepos. Anything
1229/// deeper is rare for activation-event triggers; the trade-off is
1230/// documented in VS Code's own `ExtensionService.scanExtensions`.
1231fn FindMatchingWorkspaceContainsPatterns(Folders:&[std::path::PathBuf], Patterns:&[String]) -> Vec<String> {
1232	use std::collections::HashSet;
1233
1234	const MAX_DEPTH:usize = 3;
1235
1236	const MAX_ENTRIES_PER_ROOT:usize = 4096;
1237
1238	let mut Matched:HashSet<String> = HashSet::new();
1239
1240	for Folder in Folders {
1241		if !Folder.is_dir() {
1242			continue;
1243		}
1244
1245		// Collect up to MAX_ENTRIES_PER_ROOT paths relative to the folder.
1246		let mut Entries:Vec<String> = Vec::new();
1247
1248		let mut Stack:Vec<(std::path::PathBuf, usize)> = vec![(Folder.clone(), 0)];
1249
1250		while let Some((Current, Depth)) = Stack.pop() {
1251			if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1252				break;
1253			}
1254
1255			let ReadDirResult = std::fs::read_dir(&Current);
1256
1257			let ReadDir = match ReadDirResult {
1258				Ok(R) => R,
1259
1260				Err(_) => continue,
1261			};
1262
1263			for Entry in ReadDir.flatten() {
1264				if Entries.len() >= MAX_ENTRIES_PER_ROOT {
1265					break;
1266				}
1267
1268				let Path = Entry.path();
1269
1270				let Relative = match Path.strip_prefix(Folder) {
1271					Ok(R) => R.to_string_lossy().replace('\\', "/"),
1272
1273					Err(_) => continue,
1274				};
1275
1276				let IsDir = Entry.file_type().map(|T| T.is_dir()).unwrap_or(false);
1277
1278				Entries.push(Relative.clone());
1279
1280				if IsDir && Depth + 1 < MAX_DEPTH {
1281					Stack.push((Path, Depth + 1));
1282				}
1283			}
1284		}
1285
1286		for Pattern in Patterns {
1287			if Matched.contains(Pattern) {
1288				continue;
1289			}
1290
1291			if PatternMatchesAnyEntry(Pattern, &Entries) {
1292				Matched.insert(Pattern.clone());
1293			}
1294		}
1295	}
1296
1297	Matched.into_iter().collect()
1298}
1299
1300/// Very small glob-matcher scoped to VS Code `workspaceContains:` syntax.
1301/// Supports literal paths, `*` (one path segment), and `**` (zero or more
1302/// segments). Case-sensitive per the VS Code spec.
1303fn PatternMatchesAnyEntry(Pattern:&str, Entries:&[String]) -> bool {
1304	let HasWildcard = Pattern.contains('*') || Pattern.contains('?');
1305
1306	if !HasWildcard {
1307		return Entries.iter().any(|E| E == Pattern);
1308	}
1309
1310	let PatternSegments:Vec<&str> = Pattern.split('/').collect();
1311
1312	Entries
1313		.iter()
1314		.any(|E| SegmentMatch(&PatternSegments, &E.split('/').collect::<Vec<_>>()))
1315}
1316
1317fn SegmentMatch(Pattern:&[&str], Entry:&[&str]) -> bool {
1318	if Pattern.is_empty() {
1319		return Entry.is_empty();
1320	}
1321
1322	let Head = Pattern[0];
1323
1324	if Head == "**" {
1325		// `**` matches zero or more segments. Try consuming 0..=entry.len().
1326		for Consumed in 0..=Entry.len() {
1327			if SegmentMatch(&Pattern[1..], &Entry[Consumed..]) {
1328				return true;
1329			}
1330		}
1331
1332		return false;
1333	}
1334
1335	if Entry.is_empty() {
1336		return false;
1337	}
1338
1339	if SingleSegmentMatch(Head, Entry[0]) {
1340		return SegmentMatch(&Pattern[1..], &Entry[1..]);
1341	}
1342
1343	false
1344}
1345
1346fn SingleSegmentMatch(Pattern:&str, Segment:&str) -> bool {
1347	if Pattern == "*" {
1348		return true;
1349	}
1350
1351	if !Pattern.contains('*') && !Pattern.contains('?') {
1352		return Pattern == Segment;
1353	}
1354
1355	// Minimal star-glob on a single segment: split by '*' and check each
1356	// fragment appears in order. Doesn't support `?` (rare in
1357	// workspaceContains patterns); unsupported glob chars fall through to
1358	// literal equality.
1359	let Fragments:Vec<&str> = Pattern.split('*').collect();
1360
1361	let mut Cursor = 0usize;
1362
1363	for (Index, Fragment) in Fragments.iter().enumerate() {
1364		if Fragment.is_empty() {
1365			continue;
1366		}
1367
1368		if Index == 0 {
1369			if !Segment[Cursor..].starts_with(Fragment) {
1370				return false;
1371			}
1372
1373			Cursor += Fragment.len();
1374
1375			continue;
1376		}
1377
1378		match Segment[Cursor..].find(Fragment) {
1379			Some(Offset) => Cursor += Offset + Fragment.len(),
1380
1381			None => return false,
1382		}
1383	}
1384
1385	if let Some(Last) = Fragments.last()
1386		&& !Last.is_empty()
1387	{
1388		return Segment.ends_with(Last);
1389	}
1390
1391	true
1392}