Skip to main content

Mountain/Environment/
DebugProvider.rs

1//! # DebugProvider (Environment)
2//!
3//! Implements [`DebugService`](CommonLibrary::Debug::DebugService) for
4//! `MountainEnvironment`, managing the complete debugging session lifecycle
5//! from configuration to termination. Orchestrates between the extension host
6//! (Cocoon), the debug adapter, and the UI, including DAP (Debug Adapter
7//! Protocol) message mediation.
8//!
9//! Uses two-stage registration: configuration providers and adapter descriptor
10//! factories. Each debug type (node, java, rust) can have its own configuration
11//! and adapter. Integrates with
12//! [`IPCProvider`](CommonLibrary::IPC::IPCProvider) for RPC to Cocoon.
13//!
14//! ## Debug session flow
15//!
16//! 1. UI calls `StartDebugging` with folder URI and configuration.
17//! 2. Mountain RPCs to Cocoon to resolve debug configuration (variable
18//!    substitution).
19//! 3. Mountain RPCs to Cocoon to create debug adapter descriptor.
20//! 4. Mountain spawns debug adapter process or connects to TCP server.
21//! 5. Mountain mediates DAP messages between UI and debug adapter.
22//! 6. UI sends DAP commands via `SendCommand` which forwards to adapter.
23//! 7. Debug adapter sends DAP events/notifications back through Mountain to UI.
24//! 8. Session ends on stop request or adapter process exit.
25//!
26//! ## Methods
27//!
28//! - `RegisterDebugConfigurationProvider` - register config resolver
29//! - `RegisterDebugAdapterDescriptorFactory` - register adapter factory
30//! - `StartDebugging` - start debug session
31//! - `SendCommand` - send DAP command to adapter
32//! - `StopDebugging` - graceful DAP disconnect then session unregister
33//!
34//! ## VS Code reference
35//!
36//! - `vs/workbench/contrib/debug/browser/debugService.ts`
37//! - `vs/workbench/contrib/debug/common/debug.ts`
38//! - `vs/workbench/contrib/debug/browser/adapter/descriptorFactory.ts`
39//! - `vs/debugAdapter/common/debugProtocol.ts`
40
41use std::sync::Arc;
42
43use CommonLibrary::{
44	Debug::DebugService::DebugService,
45	Environment::Requires::Requires,
46	Error::CommonError::CommonError,
47	IPC::{DTO::ProxyTarget::ProxyTarget, IPCProvider::IPCProvider},
48};
49use async_trait::async_trait;
50use serde_json::{Value, json};
51use tauri::Emitter;
52use url::Url;
53
54use super::MountainEnvironment::MountainEnvironment;
55use crate::dev_log;
56
57#[async_trait]
58impl DebugService for MountainEnvironment {
59	async fn RegisterDebugConfigurationProvider(
60		&self,
61
62		DebugType:String,
63
64		ProviderHandle:u32,
65
66		SideCarIdentifier:String,
67	) -> Result<(), CommonError> {
68		// Validate debug type is non-empty
69		if DebugType.is_empty() {
70			return Err(CommonError::InvalidArgument {
71				ArgumentName:"DebugType".to_string(),
72				Reason:"DebugType cannot be empty".to_string(),
73			});
74		}
75
76		dev_log!(
77			"exthost",
78			"[DebugProvider] Registering DebugConfigurationProvider for type '{}' (handle: {}, sidecar: {})",
79			DebugType,
80			ProviderHandle,
81			SideCarIdentifier
82		);
83
84		// Store debug configuration provider registration in ApplicationState
85		self.ApplicationState
86			.Feature
87			.Debug
88			.RegisterDebugConfigurationProvider(DebugType, ProviderHandle, SideCarIdentifier)
89			.map_err(|e| CommonError::Unknown { Description:e })?;
90
91		Ok(())
92	}
93
94	async fn RegisterDebugAdapterDescriptorFactory(
95		&self,
96
97		DebugType:String,
98
99		FactoryHandle:u32,
100
101		SideCarIdentifier:String,
102	) -> Result<(), CommonError> {
103		// Validate debug type is non-empty
104		if DebugType.is_empty() {
105			return Err(CommonError::InvalidArgument {
106				ArgumentName:"DebugType".to_string(),
107				Reason:"DebugType cannot be empty".to_string(),
108			});
109		}
110
111		dev_log!(
112			"exthost",
113			"[DebugProvider] Registering DebugAdapterDescriptorFactory for type '{}' (handle: {}, sidecar: {})",
114			DebugType,
115			FactoryHandle,
116			SideCarIdentifier
117		);
118
119		// Store debug adapter descriptor factory registration in ApplicationState
120		self.ApplicationState
121			.Feature
122			.Debug
123			.RegisterDebugAdapterDescriptorFactory(DebugType, FactoryHandle, SideCarIdentifier)
124			.map_err(|e| CommonError::Unknown { Description:e })?;
125
126		Ok(())
127	}
128
129	async fn StartDebugging(&self, _FolderURI:Option<Url>, Configuration:Value) -> Result<String, CommonError> {
130		let SessionID = uuid::Uuid::new_v4().to_string();
131
132		dev_log!(
133			"exthost",
134			"[DebugProvider] Starting debug session '{}' with config: {:?}",
135			SessionID,
136			Configuration
137		);
138
139		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
140
141		let DebugType = Configuration
142			.get("type")
143			.and_then(Value::as_str)
144			.ok_or_else(|| {
145				CommonError::InvalidArgument {
146					ArgumentName:"Configuration".into(),
147
148					Reason:"Missing 'type' field in debug configuration.".into(),
149				}
150			})?
151			.to_string();
152
153		// Look up the registered debug configuration provider to get the
154		// sidecar that handles this debug type. Falls back to "cocoon-main"
155		// (the only extension host today; Grove multi-host will need routing).
156		let TargetSideCar = self
157			.ApplicationState
158			.Feature
159			.Debug
160			.GetDebugConfigurationProvider(&DebugType)
161			.map(|R| R.SideCarIdentifier.clone())
162			.unwrap_or_else(|| "cocoon-main".to_string());
163
164		// 1. Resolve configuration (Reverse-RPC to Cocoon)
165		dev_log!(
166			"exthost",
167			"[DebugProvider] Resolving debug configuration for type '{}'",
168			DebugType
169		);
170
171		dev_log!("exthost", "[DebugProvider] Resolving debug configuration...");
172
173		let ResolveConfigMethod = format!("{}$resolveDebugConfiguration", ProxyTarget::ExtHostDebug.GetTargetPrefix());
174
175		let ResolvedConfig = IPCProvider
176			.SendRequestToSideCar(
177				TargetSideCar.clone(),
178				ResolveConfigMethod,
179				json!([DebugType.clone(), Configuration]),
180				5000,
181			)
182			.await?;
183
184		// 2. Get the Debug Adapter Descriptor (Reverse-RPC to Cocoon)
185		dev_log!("exthost", "[DebugProvider] Creating debug adapter descriptor...");
186
187		let CreateDescriptorMethod =
188			format!("{}$createDebugAdapterDescriptor", ProxyTarget::ExtHostDebug.GetTargetPrefix());
189
190		let Descriptor = IPCProvider
191			.SendRequestToSideCar(
192				TargetSideCar.clone(),
193				CreateDescriptorMethod,
194				json!([DebugType, &ResolvedConfig]),
195				5000,
196			)
197			.await?;
198
199		// 3. Spawn the Debug Adapter process based on the descriptor.
200		dev_log!(
201			"exthost",
202			"[DebugProvider] Spawning Debug Adapter based on descriptor: {:?}",
203			Descriptor
204		);
205
206		// Adapter-descriptor DTO shapes mirror VS Code's
207		// `vs/workbench/api/common/extHostDebugService.ts::convert*ToDto`:
208		//   executable  → { type: "executable", command, args, options: { env?, cwd? }
209		// }   server      → { type: "server", port, host? }
210		//   pipeServer  → { type: "pipeServer", path }
211		//   implementation → { type: "implementation" }   (handled in-process by
212		// Cocoon)
213		//
214		// Phase 1 supports `executable` only - covers every JS/TS debug adapter
215		// (vscode-js-debug, node) and most language-server-driven adapters that
216		// ship as a CLI binary. Server/pipeServer connections are stubbed with a
217		// warn-log + a session-registry entry without a StdinSender, so SendCommand
218		// can surface "adapter type unsupported" instead of a silent no-op.
219		// TODO: Wire server / pipeServer adapter connection (TCP / named-pipe).
220		// TODO: Wire reverse-RPC `$sendDAPRequest` Cocoon handler for inline-impl
221		// adapters.
222		let DescriptorType = Descriptor.get("type").and_then(Value::as_str).unwrap_or("").to_string();
223
224		let AdapterStdinSender:Option<tokio::sync::mpsc::UnboundedSender<Vec<u8>>>;
225
226		let AdapterChildPid:Option<u32>;
227
228		match DescriptorType.as_str() {
229			"executable" => {
230				let Command = Descriptor
231					.get("command")
232					.and_then(Value::as_str)
233					.ok_or_else(|| {
234						CommonError::InvalidArgument {
235							ArgumentName:"Descriptor.command".into(),
236							Reason:"executable adapter descriptor missing 'command'".into(),
237						}
238					})?
239					.to_string();
240
241				let Args:Vec<String> = Descriptor
242					.get("args")
243					.and_then(Value::as_array)
244					.map(|A| A.iter().filter_map(|V| V.as_str().map(str::to_string)).collect())
245					.unwrap_or_default();
246
247				let OptionsValue = Descriptor.get("options").cloned().unwrap_or(Value::Null);
248
249				let Cwd = OptionsValue.get("cwd").and_then(Value::as_str).map(str::to_string);
250
251				let EnvOverrides:Vec<(String, String)> = OptionsValue
252					.get("env")
253					.and_then(Value::as_object)
254					.map(|O| {
255						O.iter()
256							.filter_map(|(K, V)| V.as_str().map(|S| (K.clone(), S.to_string())))
257							.collect()
258					})
259					.unwrap_or_default();
260
261				let mut Builder = tokio::process::Command::new(&Command);
262
263				Builder
264					.args(&Args)
265					.stdin(std::process::Stdio::piped())
266					.stdout(std::process::Stdio::piped())
267					.stderr(std::process::Stdio::piped());
268
269				if let Some(CwdPath) = &Cwd {
270					Builder.current_dir(CwdPath);
271				}
272
273				for (Key, Value) in &EnvOverrides {
274					Builder.env(Key, Value);
275				}
276
277				let mut Child = Builder.spawn().map_err(|Error| {
278					CommonError::IPCError {
279						Description:format!(
280							"Failed to spawn debug adapter '{}' for session {}: {}",
281							Command, SessionID, Error
282						),
283					}
284				})?;
285
286				let Pid = Child.id();
287
288				let Stdin = Child.stdin.take().ok_or_else(|| {
289					CommonError::IPCError { Description:format!("Adapter for session {} had no stdin pipe", SessionID) }
290				})?;
291
292				let Stdout = Child.stdout.take().ok_or_else(|| {
293					CommonError::IPCError {
294						Description:format!("Adapter for session {} had no stdout pipe", SessionID),
295					}
296				})?;
297
298				let Stderr = Child.stderr.take().ok_or_else(|| {
299					CommonError::IPCError {
300						Description:format!("Adapter for session {} had no stderr pipe", SessionID),
301					}
302				})?;
303
304				let (Sender, mut Receiver) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
305
306				// Stdin writer task: drains the mpsc channel into the
307				// adapter's stdin. Closes when the channel's sender is
308				// dropped (UnregisterDebugSession) which propagates EOF
309				// to the adapter and triggers its shutdown.
310				let StdinSessionId = SessionID.clone();
311
312				tokio::spawn(async move {
313					use tokio::io::AsyncWriteExt;
314
315					let mut Pipe = Stdin;
316
317					while let Some(Frame) = Receiver.recv().await {
318						if let Err(Error) = Pipe.write_all(&Frame).await {
319							crate::dev_log!(
320								"exthost",
321								"warn: [DebugAdapter] stdin write failed for session {}: {}",
322								StdinSessionId,
323								Error
324							);
325
326							break;
327						}
328
329						if let Err(Error) = Pipe.flush().await {
330							crate::dev_log!(
331								"exthost",
332								"warn: [DebugAdapter] stdin flush failed for session {}: {}",
333								StdinSessionId,
334								Error
335							);
336
337							break;
338						}
339					}
340
341					let _ = Pipe.shutdown().await;
342				});
343
344				// Stdout reader task: parses DAP frames
345				// (`Content-Length: <n>\r\n\r\n<json>`) and re-emits each
346				// JSON message on `sky://debug/dap-message` so the
347				// renderer / Cocoon-side reverse-RPC can route it to the
348				// originating session listener. Errors break the read
349				// loop and trigger session cleanup.
350				let StdoutSessionId = SessionID.clone();
351
352				let StdoutHandle = self.ApplicationHandle.clone();
353
354				let StdoutSidecar = TargetSideCar.clone();
355
356				tokio::spawn(async move {
357					use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
358
359					let mut Reader = BufReader::new(Stdout);
360
361					let mut Header = String::new();
362
363					loop {
364						Header.clear();
365
366						let mut ContentLength:usize = 0;
367
368						loop {
369							Header.clear();
370
371							match Reader.read_line(&mut Header).await {
372								Ok(0) => return, // EOF
373								Ok(_) => {},
374								Err(Error) => {
375									crate::dev_log!(
376										"exthost",
377										"warn: [DebugAdapter] stdout read failed for session {}: {}",
378										StdoutSessionId,
379										Error
380									);
381
382									return;
383								},
384							}
385
386							let Trimmed = Header.trim_end_matches("\r\n").trim_end_matches('\n');
387
388							if Trimmed.is_empty() {
389								break;
390							}
391
392							if let Some(Rest) = Trimmed.strip_prefix("Content-Length:") {
393								if let Ok(N) = Rest.trim().parse::<usize>() {
394									ContentLength = N;
395								}
396							}
397						}
398
399						if ContentLength == 0 {
400							continue;
401						}
402
403						let mut Body = vec![0u8; ContentLength];
404
405						if let Err(Error) = Reader.read_exact(&mut Body).await {
406							crate::dev_log!(
407								"exthost",
408								"warn: [DebugAdapter] stdout body read failed for session {}: {}",
409								StdoutSessionId,
410								Error
411							);
412
413							return;
414						}
415
416						let Parsed:Value = serde_json::from_slice(&Body).unwrap_or(Value::Null);
417
418						let _ = StdoutHandle.emit(
419							"sky://debug/dap-message",
420							json!({
421								"sessionId": StdoutSessionId,
422								"sidecarId": StdoutSidecar,
423								"message": Parsed,
424							}),
425						);
426					}
427				});
428
429				// Stderr drain: emit each line as a dev_log line so adapter
430				// crash reasons surface alongside other Mountain logs.
431				let StderrSessionId = SessionID.clone();
432
433				tokio::spawn(async move {
434					use tokio::io::{AsyncBufReadExt, BufReader};
435
436					let mut Lines = BufReader::new(Stderr).lines();
437
438					while let Ok(Some(Line)) = Lines.next_line().await {
439						crate::dev_log!("exthost", "[DebugAdapter] stderr session={}: {}", StderrSessionId, Line);
440					}
441				});
442
443				AdapterStdinSender = Some(Sender);
444
445				AdapterChildPid = Pid;
446
447				dev_log!(
448					"exthost",
449					"[DebugProvider] Spawned executable adapter for session '{}' pid={:?} command={:?}",
450					SessionID,
451					Pid,
452					Command
453				);
454			},
455
456			"server" => {
457				// Connect to an already-running debug adapter over TCP. The
458				// descriptor shape is `{ type: "server", port, host? }` where
459				// `host` defaults to 127.0.0.1. Python debugpy, Go dlv, and
460				// most language-server-driven adapters ship in this mode.
461				let Port = Descriptor.get("port").and_then(Value::as_u64).ok_or_else(|| {
462					CommonError::InvalidArgument {
463						ArgumentName:"Descriptor.port".into(),
464						Reason:"server adapter descriptor missing 'port'".into(),
465					}
466				})? as u16;
467
468				let Host = Descriptor
469					.get("host")
470					.and_then(Value::as_str)
471					.unwrap_or("127.0.0.1")
472					.to_string();
473
474				let Addr = format!("{}:{}", Host, Port);
475
476				dev_log!(
477					"exthost",
478					"[DebugProvider] Connecting to debug adapter server at {} (session '{}')",
479					Addr,
480					SessionID
481				);
482
483				let TcpStream = tokio::net::TcpStream::connect(&Addr).await.map_err(|Error| {
484					CommonError::IPCError {
485						Description:format!(
486							"Failed to connect to debug adapter server at {} for session {}: {}",
487							Addr, SessionID, Error
488						),
489					}
490				})?;
491
492				let (ReadHalf, WriteHalf) = tokio::io::split(TcpStream);
493
494				let (Sender, mut Receiver) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
495
496				// Writer task: drain mpsc into the TCP write half.
497				let WriterSessionId = SessionID.clone();
498
499				tokio::spawn(async move {
500					use tokio::io::AsyncWriteExt;
501
502					let mut Pipe = WriteHalf;
503
504					while let Some(Frame) = Receiver.recv().await {
505						if let Err(Error) = Pipe.write_all(&Frame).await {
506							crate::dev_log!(
507								"exthost",
508								"warn: [DebugAdapter/server] write failed for session {}: {}",
509								WriterSessionId,
510								Error
511							);
512
513							break;
514						}
515
516						let _ = Pipe.flush().await;
517					}
518				});
519
520				// Reader task: parse DAP frames from the TCP read half and
521				// re-emit each JSON message to Sky.
522				let ReaderSessionId = SessionID.clone();
523
524				let ReaderHandle = self.ApplicationHandle.clone();
525
526				let ReaderSidecar = TargetSideCar.clone();
527
528				tokio::spawn(async move {
529					use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
530
531					let mut Reader = BufReader::new(ReadHalf);
532
533					let mut Header = String::new();
534
535					loop {
536						Header.clear();
537
538						let mut ContentLength:usize = 0;
539
540						loop {
541							Header.clear();
542
543							match Reader.read_line(&mut Header).await {
544								Ok(0) => return,
545								Ok(_) => {},
546								Err(Error) => {
547									crate::dev_log!(
548										"exthost",
549										"warn: [DebugAdapter/server] read failed for session {}: {}",
550										ReaderSessionId,
551										Error
552									);
553
554									return;
555								},
556							}
557
558							let Trimmed = Header.trim_end_matches("\r\n").trim_end_matches('\n');
559
560							if Trimmed.is_empty() {
561								break;
562							}
563
564							if let Some(Rest) = Trimmed.strip_prefix("Content-Length:") {
565								if let Ok(N) = Rest.trim().parse::<usize>() {
566									ContentLength = N;
567								}
568							}
569						}
570
571						if ContentLength == 0 {
572							continue;
573						}
574
575						let mut Body = vec![0u8; ContentLength];
576
577						if let Err(Error) = Reader.read_exact(&mut Body).await {
578							crate::dev_log!(
579								"exthost",
580								"warn: [DebugAdapter/server] body read failed for session {}: {}",
581								ReaderSessionId,
582								Error
583							);
584
585							return;
586						}
587
588						let Parsed:Value = serde_json::from_slice(&Body).unwrap_or(Value::Null);
589
590						let _ = ReaderHandle.emit(
591							"sky://debug/dap-message",
592							json!({
593								"sessionId": ReaderSessionId,
594								"sidecarId": ReaderSidecar,
595								"message": Parsed,
596							}),
597						);
598					}
599				});
600
601				AdapterStdinSender = Some(Sender);
602
603				AdapterChildPid = None;
604
605				dev_log!(
606					"exthost",
607					"[DebugProvider] Connected to server adapter at {} for session '{}'",
608					Addr,
609					SessionID
610				);
611			},
612
613			"pipeServer" => {
614				// Connect to an already-running debug adapter over a Unix
615				// domain socket (macOS/Linux) or named pipe (Windows). The
616				// descriptor shape is `{ type: "pipeServer", path }`.
617				let PipePath = Descriptor
618					.get("path")
619					.and_then(Value::as_str)
620					.ok_or_else(|| {
621						CommonError::InvalidArgument {
622							ArgumentName:"Descriptor.path".into(),
623							Reason:"pipeServer adapter descriptor missing 'path'".into(),
624						}
625					})?
626					.to_string();
627
628				dev_log!(
629					"exthost",
630					"[DebugProvider] Connecting to debug adapter pipe at '{}' (session '{}')",
631					PipePath,
632					SessionID
633				);
634
635				#[cfg(unix)]
636				let (ReadHalf, WriteHalf) = {
637					let Stream = tokio::net::UnixStream::connect(&PipePath).await.map_err(|Error| {
638						CommonError::IPCError {
639							Description:format!(
640								"Failed to connect to debug adapter pipe '{}' for session {}: {}",
641								PipePath, SessionID, Error
642							),
643						}
644					})?;
645
646					tokio::io::split(Stream)
647				};
648
649				#[cfg(windows)]
650				let (ReadHalf, WriteHalf) = {
651					// On Windows, named pipes use \\.\pipe\<name>. Tokio's
652					// NamedPipeClient handles both directions.
653					let Stream =
654						tokio::net::windows::named_pipe::ClientOptions::new()
655							.open(&PipePath)
656							.map_err(|Error| {
657								CommonError::IPCError {
658									Description:format!(
659										"Failed to open named pipe '{}' for session {}: {}",
660										PipePath, SessionID, Error
661									),
662								}
663							})?;
664
665					tokio::io::split(Stream)
666				};
667
668				let (Sender, mut Receiver) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
669
670				let PipeWriterSessionId = SessionID.clone();
671
672				tokio::spawn(async move {
673					use tokio::io::AsyncWriteExt;
674
675					let mut Pipe = WriteHalf;
676
677					while let Some(Frame) = Receiver.recv().await {
678						if let Err(Error) = Pipe.write_all(&Frame).await {
679							crate::dev_log!(
680								"exthost",
681								"warn: [DebugAdapter/pipe] write failed for session {}: {}",
682								PipeWriterSessionId,
683								Error
684							);
685
686							break;
687						}
688
689						let _ = Pipe.flush().await;
690					}
691				});
692
693				let PipeReaderSessionId = SessionID.clone();
694
695				let PipeReaderHandle = self.ApplicationHandle.clone();
696
697				let PipeReaderSidecar = TargetSideCar.clone();
698
699				tokio::spawn(async move {
700					use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
701
702					let mut Reader = BufReader::new(ReadHalf);
703
704					let mut Header = String::new();
705
706					loop {
707						Header.clear();
708
709						let mut ContentLength:usize = 0;
710
711						loop {
712							Header.clear();
713
714							match Reader.read_line(&mut Header).await {
715								Ok(0) => return,
716								Ok(_) => {},
717								Err(Error) => {
718									crate::dev_log!(
719										"exthost",
720										"warn: [DebugAdapter/pipe] read failed for session {}: {}",
721										PipeReaderSessionId,
722										Error
723									);
724
725									return;
726								},
727							}
728
729							let Trimmed = Header.trim_end_matches("\r\n").trim_end_matches('\n');
730
731							if Trimmed.is_empty() {
732								break;
733							}
734
735							if let Some(Rest) = Trimmed.strip_prefix("Content-Length:") {
736								if let Ok(N) = Rest.trim().parse::<usize>() {
737									ContentLength = N;
738								}
739							}
740						}
741
742						if ContentLength == 0 {
743							continue;
744						}
745
746						let mut Body = vec![0u8; ContentLength];
747
748						if let Err(Error) = Reader.read_exact(&mut Body).await {
749							crate::dev_log!(
750								"exthost",
751								"warn: [DebugAdapter/pipe] body read failed for session {}: {}",
752								PipeReaderSessionId,
753								Error
754							);
755
756							return;
757						}
758
759						let Parsed:Value = serde_json::from_slice(&Body).unwrap_or(Value::Null);
760
761						let _ = PipeReaderHandle.emit(
762							"sky://debug/dap-message",
763							json!({
764								"sessionId": PipeReaderSessionId,
765								"sidecarId": PipeReaderSidecar,
766								"message": Parsed,
767							}),
768						);
769					}
770				});
771
772				AdapterStdinSender = Some(Sender);
773
774				AdapterChildPid = None;
775
776				dev_log!(
777					"exthost",
778					"[DebugProvider] Connected to pipe adapter at '{}' for session '{}'",
779					PipePath,
780					SessionID
781				);
782			},
783
784			"implementation" => {
785				dev_log!(
786					"exthost",
787					"[DebugProvider] Inline implementation adapter for session '{}' - DAP frames travel via Cocoon \
788					 reverse-RPC.",
789					SessionID
790				);
791
792				AdapterStdinSender = None;
793
794				AdapterChildPid = None;
795			},
796
797			_ => {
798				dev_log!(
799					"exthost",
800					"warn: [DebugProvider] Unknown adapter descriptor type '{}' for session '{}' - registering \
801					 session without spawn.",
802					DescriptorType,
803					SessionID
804				);
805
806				AdapterStdinSender = None;
807
808				AdapterChildPid = None;
809			},
810		}
811
812		// Persist the session in ApplicationState so SendCommand can
813		// resolve it. Without this, every subsequent DAP command from the
814		// workbench would land on the "session not found" path even though
815		// the adapter is alive and listening.
816		if let Err(RegError) = self.ApplicationState.Feature.Debug.RegisterDebugSession(
817			crate::ApplicationState::State::FeatureState::Debug::DebugState::DebugSessionEntry {
818				SessionId:SessionID.clone(),
819				DebugType:DebugType.clone(),
820				SideCarIdentifier:TargetSideCar.clone(),
821				StdinSender:AdapterStdinSender,
822				ChildPid:AdapterChildPid,
823			},
824		) {
825			dev_log!(
826				"exthost",
827				"warn: [DebugProvider] Failed to register session '{}' in DebugState: {}",
828				SessionID,
829				RegError
830			);
831		}
832
833		// Notify Cocoon that the session has started so any
834		// `vscode.debug.onDidStartDebugSession` listeners (registered
835		// from extensions through `DebugNamespace.ts:124`) fire. The
836		// payload mirrors VS Code's wire shape - extensions read
837		// `id`, `type`, `name`, and `configuration` off the session
838		// object passed to the listener. Until full session tracking
839		// lands in ApplicationState we synthesise from the resolved
840		// configuration so extensions can observe activation even
841		// while the adapter spawn path is still a stub.
842		let StartedMethod = format!("{}$onDidStartDebugSession", ProxyTarget::ExtHostDebug.GetTargetPrefix());
843
844		let StartedSession = json!({
845			"id": SessionID.clone(),
846			"type": DebugType.clone(),
847			"name": ResolvedConfig.get("name").and_then(Value::as_str).unwrap_or(&DebugType),
848			"configuration": ResolvedConfig.clone(),
849		});
850
851		if let Err(error) = IPCProvider
852			.SendNotificationToSideCar(TargetSideCar.clone(), StartedMethod, json!([StartedSession]))
853			.await
854		{
855			dev_log!(
856				"exthost",
857				"warn: [DebugProvider] StartDebugging notification failed for '{}': {:?}",
858				SessionID,
859				error
860			);
861		}
862
863		// Sky-side debug viewlet observers consume this stream so the
864		// debug toolbar / call stack panel light up without waiting on
865		// the typed `DebugService::ActiveSessions` snapshot. Mirrors
866		// `WebviewLifecycle.rs`'s pattern of dual-emitting to Cocoon
867		// (typed RPC) and Sky (renderer event).
868		let _ = self.ApplicationHandle.emit(
869			"sky://debug/sessionStart",
870			json!({
871				"sessionId": SessionID.clone(),
872				"type": DebugType.clone(),
873				"configuration": ResolvedConfig.clone(),
874			}),
875		);
876
877		dev_log!("exthost", "[DebugProvider] Debug session '{}' started (simulation).", SessionID);
878
879		Ok(SessionID)
880	}
881
882	async fn SendCommand(&self, SessionID:String, Command:String, Arguments:Value) -> Result<Value, CommonError> {
883		dev_log!(
884			"exthost",
885			"[DebugProvider] SendCommand for session '{}' (command: '{}', args: {:?})",
886			SessionID,
887			Command,
888			Arguments
889		);
890
891		// Resolve the active session. Missing entries fall through to the
892		// reverse-RPC path below so commands targeting an inline-impl
893		// adapter (DebugAdapterInlineImplementation - JS-only adapters
894		// running inside Cocoon) still reach their handler.
895		let SessionEntry = self.ApplicationState.Feature.Debug.GetDebugSession(&SessionID);
896
897		// DAP framing: producer must wrap the JSON message in a
898		// `Content-Length: <n>\r\n\r\n<body>` header. Sequence numbers
899		// are caller-allocated (the workbench's `RawDebugSession` keeps
900		// its own `_currentReqId`); we don't reorder. Wire the request
901		// shape that VS Code's `mainThreadDebugService.ts` produces:
902		// `{ seq, type: "request", command, arguments }`. Mountain
903		// doesn't currently track per-session seq numbers - upstream
904		// VS Code increments request_seq on the WORKBENCH side and we
905		// just forward verbatim - so we emit `0` here as a placeholder
906		// when the caller hasn't supplied one in `Arguments.seq`.
907		let RequestSeq = Arguments.get("seq").and_then(Value::as_u64).unwrap_or(0);
908
909		let RequestArguments = Arguments.get("arguments").cloned().unwrap_or(Arguments.clone());
910
911		let DapRequest = json!({
912			"seq": RequestSeq,
913			"type": "request",
914			"command": Command,
915			"arguments": RequestArguments,
916		});
917
918		if let Some(Entry) = SessionEntry.as_ref() {
919			if let Some(Sender) = Entry.StdinSender.as_ref() {
920				let Body = serde_json::to_vec(&DapRequest).map_err(|Error| {
921					CommonError::IPCError {
922						Description:format!("Failed to serialize DAP request for session {}: {}", SessionID, Error),
923					}
924				})?;
925
926				let Header = format!("Content-Length: {}\r\n\r\n", Body.len());
927
928				let mut Frame = Vec::with_capacity(Header.len() + Body.len());
929
930				Frame.extend_from_slice(Header.as_bytes());
931
932				Frame.extend_from_slice(&Body);
933
934				Sender.send(Frame).map_err(|Error| {
935					CommonError::IPCError {
936						Description:format!("Adapter stdin channel for session {} closed: {}", SessionID, Error),
937					}
938				})?;
939
940				// stdio adapters reply asynchronously through the
941				// stdout reader task, which fans the response out via
942				// `sky://debug/dap-message`. Returning an ack now lets
943				// the workbench's request sequencer continue; the actual
944				// response is correlated by `request_seq` on the
945				// renderer side.
946				return Ok(json!({
947					"success": true,
948					"sessionId": SessionID,
949					"command": Command,
950					"transport": "stdio",
951				}));
952			}
953		}
954
955		// No live stdin pipe: route via reverse-RPC into the owning
956		// sidecar. This covers (1) sessions created with
957		// `DebugAdapterInlineImplementation` where the adapter runs
958		// inside the extension host, (2) `server` / `pipeServer`
959		// descriptors awaiting their connection wiring, and (3)
960		// commands fired before `RegisterDebugSession` has landed
961		// (rare race during spawn). The Cocoon-side handler dispatches
962		// based on session-id stored in `extHostDebug.ts`'s session map.
963		let TargetSidecar = SessionEntry
964			.as_ref()
965			.map(|E| E.SideCarIdentifier.clone())
966			.unwrap_or_else(|| "cocoon-main".to_string());
967
968		let SendDapMethod = format!("{}$sendDAPRequest", ProxyTarget::ExtHostDebug.GetTargetPrefix());
969
970		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
971
972		match IPCProvider
973			.SendRequestToSideCar(
974				TargetSidecar,
975				SendDapMethod,
976				json!([{ "sessionId": SessionID, "request": DapRequest }]),
977				15000,
978			)
979			.await
980		{
981			Ok(Response) => Ok(Response),
982
983			Err(Error) => {
984				dev_log!(
985					"exthost",
986					"warn: [DebugProvider] reverse-RPC SendCommand failed for session {}: {:?}",
987					SessionID,
988					Error
989				);
990
991				Err(Error)
992			},
993		}
994	}
995
996	async fn StopDebugging(&self, SessionID:String) -> Result<(), CommonError> {
997		dev_log!("exthost", "[DebugProvider] StopDebugging request for session '{}'", SessionID);
998
999		// Try a graceful DAP `disconnect` first so the adapter can flush
1000		// pending state and let the debuggee detach cleanly. Failures
1001		// are logged-and-tolerated; the unregister below force-closes
1002		// the stdin pipe regardless.
1003		if let Some(Entry) = self.ApplicationState.Feature.Debug.GetDebugSession(&SessionID) {
1004			if let Some(Sender) = Entry.StdinSender.as_ref() {
1005				let DisconnectRequest = json!({
1006					"seq": 0,
1007					"type": "request",
1008					"command": "disconnect",
1009					"arguments": { "restart": false, "terminateDebuggee": true },
1010				});
1011
1012				if let Ok(Body) = serde_json::to_vec(&DisconnectRequest) {
1013					let Header = format!("Content-Length: {}\r\n\r\n", Body.len());
1014
1015					let mut Frame = Vec::with_capacity(Header.len() + Body.len());
1016
1017					Frame.extend_from_slice(Header.as_bytes());
1018
1019					Frame.extend_from_slice(&Body);
1020
1021					let _ = Sender.send(Frame);
1022				}
1023			}
1024		}
1025
1026		// Drop the entry. The drained `Sender` clone in the in-flight
1027		// stdin writer task will see the channel close on its next `recv`
1028		// and shut the adapter's stdin, which most adapters interpret
1029		// as a graceful disconnect.
1030		let _ = self.ApplicationState.Feature.Debug.UnregisterDebugSession(&SessionID);
1031
1032		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
1033
1034		let TerminateMethod = format!("{}$onDidTerminateDebugSession", ProxyTarget::ExtHostDebug.GetTargetPrefix());
1035
1036		if let Err(error) = IPCProvider
1037			.SendNotificationToSideCar("cocoon-main".to_string(), TerminateMethod, json!([{ "id": SessionID.clone() }]))
1038			.await
1039		{
1040			dev_log!(
1041				"exthost",
1042				"warn: [DebugProvider] StopDebugging notification failed for '{}': {:?}",
1043				SessionID,
1044				error
1045			);
1046		}
1047
1048		let _ = self
1049			.ApplicationHandle
1050			.emit("sky://debug/sessionEnd", json!({ "sessionId": SessionID.clone() }));
1051
1052		Ok(())
1053	}
1054}