Skip to main content

Mountain/Environment/
DebugProvider.rs

1//! # DebugProvider (Environment)
2//!
3//! RESPONSIBILITIES:
4//! - Implements [`DebugService`](CommonLibrary::Debug::DebugService) for
5//!   [`MountainEnvironment`]
6//! - Manages complete debugging session lifecycle from configuration to
7//!   termination
8//! - Orchestrates between extension host (Cocoon), debug adapter, and UI
9//! - Handles DAP (Debug Adapter Protocol) message mediation
10//!
11//! ARCHITECTURAL ROLE:
12//! - Core provider for debugging functionality, analogous to VSCode's debug
13//!   service
14//! - Uses two-stage registration: configuration providers and adapter
15//!   descriptor factories
16//! - Each debug type (node, java, rust) can have its own configuration and
17//!   adapter
18//! - Integrates with [`IPCProvider`](CommonLibrary::IPC::IPCProvider) for RPC
19//!   to Cocoon
20//!
21//! DEBUG SESSION FLOW:
22//! 1. UI calls `StartDebugging` with folder URI and configuration
23//! 2. Mountain RPCs to Cocoon to resolve debug configuration (variable
24//!    substitution)
25//! 3. Mountain RPCs to Cocoon to create debug adapter descriptor
26//! 4. Mountain spawns debug adapter process or connects to TCP server
27//! 5. Mountain mediates DAP messages between UI and debug adapter
28//! 6. UI sends DAP commands via `SendCommand` which forwards to adapter
29//! 7. Debug adapter sends DAP events/notifications back through Mountain to UI
30//! 8. Session ends on stop request or adapter process exit
31//!
32//! ERROR HANDLING:
33//! - Uses [`CommonError`](CommonLibrary::Error::CommonError) for all operations
34//! - Validates debug type is non-empty (InvalidArgument error)
35//! - TODO: Implement proper session lookup, timeout handling, and error
36//!   recovery
37//!
38//! PERFORMANCE:
39//! - Debug adapter spawning should be async with timeout protection (5000ms in
40//!   current RPC)
41//! - DAP message routing needs efficient session lookup (TODO: O(1) hash map)
42//! - Multiple simultaneous debug sessions require careful resource management
43//!
44//! VS CODE REFERENCE:
45//! - `vs/workbench/contrib/debug/browser/debugService.ts` - debug service main
46//!   logic
47//! - `vs/workbench/contrib/debug/common/debug.ts` - debug interfaces and models
48//! - `vs/workbench/contrib/debug/browser/adapter/descriptorFactory.ts` -
49//!   adapter descriptor factories
50//! - `vs/debugAdapter/common/debugProtocol.ts` - DAP protocol specification
51//!
52//! IMPLEMENTED:
53//! - Provider/factory registrations stored in ApplicationState (DebugState)
54//! - Active session tracking via DebugState::DebugSessions map
55//! - Executable adapter spawning with stdin/stdout/stderr pipes
56//! - DAP frame parsing (Content-Length header + JSON body) on adapter stdout
57//! - Stdout messages emitted on `sky://debug/dap-message` for renderer pickup
58//! - SendCommand serialises DAP request → writes framed bytes to stdin
59//! - StopDebugging sends DAP `disconnect` request then unregisters session
60//!
61//! FOLLOWUP:
62//! - `server` / `pipeServer` adapter connection (TCP / named-pipe)
63//! - Reverse-RPC `$sendDAPRequest` Cocoon handler for inline-impl adapters
64//! - Per-session request_seq allocation (currently caller-supplied)
65//! - Adapter crash detection: when stdout EOFs unexpectedly, emit
66//!   `$onDidTerminateDebugSession` so the workbench tears down its session view
67//!   (today the user sees a stale session row)
68//! - Debug console / variable inspection integration
69//! - Telemetry for adapter spawn duration, session length, exit codes
70//!
71//! MODULE CONTENTS:
72//! - [`DebugService`](CommonLibrary::Debug::DebugService) implementation:
73//! - `RegisterDebugConfigurationProvider` - register config resolver
74//! - `RegisterDebugAdapterDescriptorFactory` - register adapter factory
75//! - `StartDebugging` - start debug session (partial)
76//! - `SendCommand` - send DAP command to adapter (stub)
77
78use std::sync::Arc;
79
80use CommonLibrary::{
81	Debug::DebugService::DebugService,
82	Environment::Requires::Requires,
83	Error::CommonError::CommonError,
84	IPC::{DTO::ProxyTarget::ProxyTarget, IPCProvider::IPCProvider},
85};
86use async_trait::async_trait;
87use serde_json::{Value, json};
88use tauri::Emitter;
89use url::Url;
90
91use super::MountainEnvironment::MountainEnvironment;
92use crate::dev_log;
93
94#[async_trait]
95impl DebugService for MountainEnvironment {
96	async fn RegisterDebugConfigurationProvider(
97		&self,
98
99		DebugType:String,
100
101		ProviderHandle:u32,
102
103		SideCarIdentifier:String,
104	) -> Result<(), CommonError> {
105		// Validate debug type is non-empty
106		if DebugType.is_empty() {
107			return Err(CommonError::InvalidArgument {
108				ArgumentName:"DebugType".to_string(),
109				Reason:"DebugType cannot be empty".to_string(),
110			});
111		}
112
113		dev_log!(
114			"exthost",
115			"[DebugProvider] Registering DebugConfigurationProvider for type '{}' (handle: {}, sidecar: {})",
116			DebugType,
117			ProviderHandle,
118			SideCarIdentifier
119		);
120
121		// Store debug configuration provider registration in ApplicationState
122		self.ApplicationState
123			.Feature
124			.Debug
125			.RegisterDebugConfigurationProvider(DebugType, ProviderHandle, SideCarIdentifier)
126			.map_err(|e| CommonError::Unknown { Description:e })?;
127
128		Ok(())
129	}
130
131	async fn RegisterDebugAdapterDescriptorFactory(
132		&self,
133
134		DebugType:String,
135
136		FactoryHandle:u32,
137
138		SideCarIdentifier:String,
139	) -> Result<(), CommonError> {
140		// Validate debug type is non-empty
141		if DebugType.is_empty() {
142			return Err(CommonError::InvalidArgument {
143				ArgumentName:"DebugType".to_string(),
144				Reason:"DebugType cannot be empty".to_string(),
145			});
146		}
147
148		dev_log!(
149			"exthost",
150			"[DebugProvider] Registering DebugAdapterDescriptorFactory for type '{}' (handle: {}, sidecar: {})",
151			DebugType,
152			FactoryHandle,
153			SideCarIdentifier
154		);
155
156		// Store debug adapter descriptor factory registration in ApplicationState
157		self.ApplicationState
158			.Feature
159			.Debug
160			.RegisterDebugAdapterDescriptorFactory(DebugType, FactoryHandle, SideCarIdentifier)
161			.map_err(|e| CommonError::Unknown { Description:e })?;
162
163		Ok(())
164	}
165
166	async fn StartDebugging(&self, _FolderURI:Option<Url>, Configuration:Value) -> Result<String, CommonError> {
167		let SessionID = uuid::Uuid::new_v4().to_string();
168		dev_log!(
169			"exthost",
170			"[DebugProvider] Starting debug session '{}' with config: {:?}",
171			SessionID,
172			Configuration
173		);
174
175		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
176		let DebugType = Configuration
177			.get("type")
178			.and_then(Value::as_str)
179			.ok_or_else(|| {
180				CommonError::InvalidArgument {
181					ArgumentName:"Configuration".into(),
182
183					Reason:"Missing 'type' field in debug configuration.".into(),
184				}
185			})?
186			.to_string();
187
188		// TODO: Look up which sidecar (extension) handles this debug type using
189		// the registration stored in ApplicationState. The mapping should be based
190		// on previous RegisterDebugConfigurationProvider calls. Initial stub uses
191		// hardcoded "cocoon-main" until proper registration tracking is implemented.
192		let TargetSideCar = "cocoon-main".to_string();
193
194		// 1. Resolve configuration (Reverse-RPC to Cocoon)
195		dev_log!(
196			"exthost",
197			"[DebugProvider] Resolving debug configuration for type '{}'",
198			DebugType
199		);
200		dev_log!("exthost", "[DebugProvider] Resolving debug configuration...");
201		let ResolveConfigMethod = format!("{}$resolveDebugConfiguration", ProxyTarget::ExtHostDebug.GetTargetPrefix());
202		let ResolvedConfig = IPCProvider
203			.SendRequestToSideCar(
204				TargetSideCar.clone(),
205				ResolveConfigMethod,
206				json!([DebugType.clone(), Configuration]),
207				5000,
208			)
209			.await?;
210
211		// 2. Get the Debug Adapter Descriptor (Reverse-RPC to Cocoon)
212		dev_log!("exthost", "[DebugProvider] Creating debug adapter descriptor...");
213		let CreateDescriptorMethod =
214			format!("{}$createDebugAdapterDescriptor", ProxyTarget::ExtHostDebug.GetTargetPrefix());
215		let Descriptor = IPCProvider
216			.SendRequestToSideCar(
217				TargetSideCar.clone(),
218				CreateDescriptorMethod,
219				json!([DebugType, &ResolvedConfig]),
220				5000,
221			)
222			.await?;
223
224		// 3. Spawn the Debug Adapter process based on the descriptor.
225		dev_log!(
226			"exthost",
227			"[DebugProvider] Spawning Debug Adapter based on descriptor: {:?}",
228			Descriptor
229		);
230
231		// Adapter-descriptor DTO shapes mirror VS Code's
232		// `vs/workbench/api/common/extHostDebugService.ts::convert*ToDto`:
233		//   executable  → { type: "executable", command, args, options: { env?, cwd? }
234		// }   server      → { type: "server", port, host? }
235		//   pipeServer  → { type: "pipeServer", path }
236		//   implementation → { type: "implementation" }   (handled in-process by
237		// Cocoon)
238		//
239		// Phase 1 of DAP spawning supports `executable` only - that covers
240		// every JS/TS debug adapter (vscode-js-debug, node) and most
241		// language-server-driven adapters that ship as a CLI binary.
242		// Server / pipeServer connections are stubbed with a warn-log + a
243		// session-registry entry without an `StdinSender`, so SendCommand
244		// can still resolve the session and surface "adapter type
245		// unsupported" instead of a silent no-op. Inline implementations
246		// are handled entirely in the extension host - Cocoon's
247		// `$createDebugAdapterDescriptor` returns `{type:"implementation"}`
248		// and Cocoon dispatches DAP frames internally; we still record
249		// the session so `vscode.debug.onDidStartDebugSession` listeners
250		// receive the activation event.
251		let DescriptorType = Descriptor.get("type").and_then(Value::as_str).unwrap_or("").to_string();
252		let AdapterStdinSender:Option<tokio::sync::mpsc::UnboundedSender<Vec<u8>>>;
253		let AdapterChildPid:Option<u32>;
254		match DescriptorType.as_str() {
255			"executable" => {
256				let Command = Descriptor
257					.get("command")
258					.and_then(Value::as_str)
259					.ok_or_else(|| {
260						CommonError::InvalidArgument {
261							ArgumentName:"Descriptor.command".into(),
262							Reason:"executable adapter descriptor missing 'command'".into(),
263						}
264					})?
265					.to_string();
266				let Args:Vec<String> = Descriptor
267					.get("args")
268					.and_then(Value::as_array)
269					.map(|A| A.iter().filter_map(|V| V.as_str().map(str::to_string)).collect())
270					.unwrap_or_default();
271				let OptionsValue = Descriptor.get("options").cloned().unwrap_or(Value::Null);
272				let Cwd = OptionsValue.get("cwd").and_then(Value::as_str).map(str::to_string);
273				let EnvOverrides:Vec<(String, String)> = OptionsValue
274					.get("env")
275					.and_then(Value::as_object)
276					.map(|O| {
277						O.iter()
278							.filter_map(|(K, V)| V.as_str().map(|S| (K.clone(), S.to_string())))
279							.collect()
280					})
281					.unwrap_or_default();
282
283				let mut Builder = tokio::process::Command::new(&Command);
284				Builder
285					.args(&Args)
286					.stdin(std::process::Stdio::piped())
287					.stdout(std::process::Stdio::piped())
288					.stderr(std::process::Stdio::piped());
289				if let Some(CwdPath) = &Cwd {
290					Builder.current_dir(CwdPath);
291				}
292				for (Key, Value) in &EnvOverrides {
293					Builder.env(Key, Value);
294				}
295
296				let mut Child = Builder.spawn().map_err(|Error| {
297					CommonError::IPCError {
298						Description:format!(
299							"Failed to spawn debug adapter '{}' for session {}: {}",
300							Command, SessionID, Error
301						),
302					}
303				})?;
304
305				let Pid = Child.id();
306				let Stdin = Child.stdin.take().ok_or_else(|| {
307					CommonError::IPCError { Description:format!("Adapter for session {} had no stdin pipe", SessionID) }
308				})?;
309				let Stdout = Child.stdout.take().ok_or_else(|| {
310					CommonError::IPCError {
311						Description:format!("Adapter for session {} had no stdout pipe", SessionID),
312					}
313				})?;
314				let Stderr = Child.stderr.take().ok_or_else(|| {
315					CommonError::IPCError {
316						Description:format!("Adapter for session {} had no stderr pipe", SessionID),
317					}
318				})?;
319
320				let (Sender, mut Receiver) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
321
322				// Stdin writer task: drains the mpsc channel into the
323				// adapter's stdin. Closes when the channel's sender is
324				// dropped (UnregisterDebugSession) which propagates EOF
325				// to the adapter and triggers its shutdown.
326				let StdinSessionId = SessionID.clone();
327				tokio::spawn(async move {
328					use tokio::io::AsyncWriteExt;
329					let mut Pipe = Stdin;
330					while let Some(Frame) = Receiver.recv().await {
331						if let Err(Error) = Pipe.write_all(&Frame).await {
332							crate::dev_log!(
333								"exthost",
334								"warn: [DebugAdapter] stdin write failed for session {}: {}",
335								StdinSessionId,
336								Error
337							);
338							break;
339						}
340						if let Err(Error) = Pipe.flush().await {
341							crate::dev_log!(
342								"exthost",
343								"warn: [DebugAdapter] stdin flush failed for session {}: {}",
344								StdinSessionId,
345								Error
346							);
347							break;
348						}
349					}
350					let _ = Pipe.shutdown().await;
351				});
352
353				// Stdout reader task: parses DAP frames
354				// (`Content-Length: <n>\r\n\r\n<json>`) and re-emits each
355				// JSON message on `sky://debug/dap-message` so the
356				// renderer / Cocoon-side reverse-RPC can route it to the
357				// originating session listener. Errors break the read
358				// loop and trigger session cleanup.
359				let StdoutSessionId = SessionID.clone();
360				let StdoutHandle = self.ApplicationHandle.clone();
361				let StdoutSidecar = TargetSideCar.clone();
362				tokio::spawn(async move {
363					use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
364					let mut Reader = BufReader::new(Stdout);
365					let mut Header = String::new();
366					loop {
367						Header.clear();
368						let mut ContentLength:usize = 0;
369						loop {
370							Header.clear();
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									return;
382								},
383							}
384							let Trimmed = Header.trim_end_matches("\r\n").trim_end_matches('\n');
385							if Trimmed.is_empty() {
386								break;
387							}
388							if let Some(Rest) = Trimmed.strip_prefix("Content-Length:") {
389								if let Ok(N) = Rest.trim().parse::<usize>() {
390									ContentLength = N;
391								}
392							}
393						}
394						if ContentLength == 0 {
395							continue;
396						}
397						let mut Body = vec![0u8; ContentLength];
398						if let Err(Error) = Reader.read_exact(&mut Body).await {
399							crate::dev_log!(
400								"exthost",
401								"warn: [DebugAdapter] stdout body read failed for session {}: {}",
402								StdoutSessionId,
403								Error
404							);
405							return;
406						}
407						let Parsed:Value = serde_json::from_slice(&Body).unwrap_or(Value::Null);
408						let _ = StdoutHandle.emit(
409							"sky://debug/dap-message",
410							json!({
411								"sessionId": StdoutSessionId,
412								"sidecarId": StdoutSidecar,
413								"message": Parsed,
414							}),
415						);
416					}
417				});
418
419				// Stderr drain: emit each line as a `[DebugAdapter] stderr`
420				// dev_log line so adapter crash reasons surface alongside
421				// other Mountain logs.
422				let StderrSessionId = SessionID.clone();
423				tokio::spawn(async move {
424					use tokio::io::{AsyncBufReadExt, BufReader};
425					let mut Lines = BufReader::new(Stderr).lines();
426					while let Ok(Some(Line)) = Lines.next_line().await {
427						crate::dev_log!("exthost", "[DebugAdapter] stderr session={}: {}", StderrSessionId, Line);
428					}
429				});
430
431				AdapterStdinSender = Some(Sender);
432				AdapterChildPid = Pid;
433				dev_log!(
434					"exthost",
435					"[DebugProvider] Spawned executable adapter for session '{}' pid={:?} command={:?}",
436					SessionID,
437					Pid,
438					Command
439				);
440			},
441			"server" | "pipeServer" => {
442				dev_log!(
443					"exthost",
444					"warn: [DebugProvider] Adapter type '{}' not yet wired (session '{}'). Reverse-RPC dispatch only.",
445					DescriptorType,
446					SessionID
447				);
448				AdapterStdinSender = None;
449				AdapterChildPid = None;
450			},
451			"implementation" => {
452				dev_log!(
453					"exthost",
454					"[DebugProvider] Inline implementation adapter for session '{}' - DAP frames travel via Cocoon \
455					 reverse-RPC.",
456					SessionID
457				);
458				AdapterStdinSender = None;
459				AdapterChildPid = None;
460			},
461			_ => {
462				dev_log!(
463					"exthost",
464					"warn: [DebugProvider] Unknown adapter descriptor type '{}' for session '{}' - registering \
465					 session without spawn.",
466					DescriptorType,
467					SessionID
468				);
469				AdapterStdinSender = None;
470				AdapterChildPid = None;
471			},
472		}
473
474		// Persist the session in ApplicationState so SendCommand can
475		// resolve it. Without this, every subsequent DAP command from the
476		// workbench would land on the "session not found" path even though
477		// the adapter is alive and listening.
478		if let Err(RegError) = self.ApplicationState.Feature.Debug.RegisterDebugSession(
479			crate::ApplicationState::State::FeatureState::Debug::DebugState::DebugSessionEntry {
480				SessionId:SessionID.clone(),
481				DebugType:DebugType.clone(),
482				SideCarIdentifier:TargetSideCar.clone(),
483				StdinSender:AdapterStdinSender,
484				ChildPid:AdapterChildPid,
485			},
486		) {
487			dev_log!(
488				"exthost",
489				"warn: [DebugProvider] Failed to register session '{}' in DebugState: {}",
490				SessionID,
491				RegError
492			);
493		}
494
495		// Notify Cocoon that the session has started so any
496		// `vscode.debug.onDidStartDebugSession` listeners (registered
497		// from extensions through `DebugNamespace.ts:124`) fire. The
498		// payload mirrors VS Code's wire shape - extensions read
499		// `id`, `type`, `name`, and `configuration` off the session
500		// object passed to the listener. Until full session tracking
501		// lands in ApplicationState we synthesise from the resolved
502		// configuration so extensions can observe activation even
503		// while the adapter spawn path is still a stub.
504		let StartedMethod = format!("{}$onDidStartDebugSession", ProxyTarget::ExtHostDebug.GetTargetPrefix());
505		let StartedSession = json!({
506			"id": SessionID.clone(),
507			"type": DebugType.clone(),
508			"name": ResolvedConfig.get("name").and_then(Value::as_str).unwrap_or(&DebugType),
509			"configuration": ResolvedConfig.clone(),
510		});
511		if let Err(error) = IPCProvider
512			.SendNotificationToSideCar(TargetSideCar.clone(), StartedMethod, json!([StartedSession]))
513			.await
514		{
515			dev_log!(
516				"exthost",
517				"warn: [DebugProvider] StartDebugging notification failed for '{}': {:?}",
518				SessionID,
519				error
520			);
521		}
522
523		// Sky-side debug viewlet observers consume this stream so the
524		// debug toolbar / call stack panel light up without waiting on
525		// the typed `DebugService::ActiveSessions` snapshot. Mirrors
526		// `WebviewLifecycle.rs`'s pattern of dual-emitting to Cocoon
527		// (typed RPC) and Sky (renderer event).
528		let _ = self.ApplicationHandle.emit(
529			"sky://debug/sessionStart",
530			json!({
531				"sessionId": SessionID.clone(),
532				"type": DebugType.clone(),
533				"configuration": ResolvedConfig.clone(),
534			}),
535		);
536
537		dev_log!("exthost", "[DebugProvider] Debug session '{}' started (simulation).", SessionID);
538		Ok(SessionID)
539	}
540
541	async fn SendCommand(&self, SessionID:String, Command:String, Arguments:Value) -> Result<Value, CommonError> {
542		dev_log!(
543			"exthost",
544			"[DebugProvider] SendCommand for session '{}' (command: '{}', args: {:?})",
545			SessionID,
546			Command,
547			Arguments
548		);
549
550		// Resolve the active session. Missing entries fall through to the
551		// reverse-RPC path below so commands targeting an inline-impl
552		// adapter (DebugAdapterInlineImplementation - JS-only adapters
553		// running inside Cocoon) still reach their handler.
554		let SessionEntry = self.ApplicationState.Feature.Debug.GetDebugSession(&SessionID);
555
556		// DAP framing: producer must wrap the JSON message in a
557		// `Content-Length: <n>\r\n\r\n<body>` header. Sequence numbers
558		// are caller-allocated (the workbench's `RawDebugSession` keeps
559		// its own `_currentReqId`); we don't reorder. Wire the request
560		// shape that VS Code's `mainThreadDebugService.ts` produces:
561		// `{ seq, type: "request", command, arguments }`. Mountain
562		// doesn't currently track per-session seq numbers - upstream
563		// VS Code increments request_seq on the WORKBENCH side and we
564		// just forward verbatim - so we emit `0` here as a placeholder
565		// when the caller hasn't supplied one in `Arguments.seq`.
566		let RequestSeq = Arguments.get("seq").and_then(Value::as_u64).unwrap_or(0);
567		let RequestArguments = Arguments.get("arguments").cloned().unwrap_or(Arguments.clone());
568		let DapRequest = json!({
569			"seq": RequestSeq,
570			"type": "request",
571			"command": Command,
572			"arguments": RequestArguments,
573		});
574
575		if let Some(Entry) = SessionEntry.as_ref() {
576			if let Some(Sender) = Entry.StdinSender.as_ref() {
577				let Body = serde_json::to_vec(&DapRequest).map_err(|Error| {
578					CommonError::IPCError {
579						Description:format!("Failed to serialize DAP request for session {}: {}", SessionID, Error),
580					}
581				})?;
582				let Header = format!("Content-Length: {}\r\n\r\n", Body.len());
583				let mut Frame = Vec::with_capacity(Header.len() + Body.len());
584				Frame.extend_from_slice(Header.as_bytes());
585				Frame.extend_from_slice(&Body);
586				Sender.send(Frame).map_err(|Error| {
587					CommonError::IPCError {
588						Description:format!("Adapter stdin channel for session {} closed: {}", SessionID, Error),
589					}
590				})?;
591				// stdio adapters reply asynchronously through the
592				// stdout reader task, which fans the response out via
593				// `sky://debug/dap-message`. Returning an ack now lets
594				// the workbench's request sequencer continue; the actual
595				// response is correlated by `request_seq` on the
596				// renderer side.
597				return Ok(json!({
598					"success": true,
599					"sessionId": SessionID,
600					"command": Command,
601					"transport": "stdio",
602				}));
603			}
604		}
605
606		// No live stdin pipe: route via reverse-RPC into the owning
607		// sidecar. This covers (1) sessions created with
608		// `DebugAdapterInlineImplementation` where the adapter runs
609		// inside the extension host, (2) `server` / `pipeServer`
610		// descriptors awaiting their connection wiring, and (3)
611		// commands fired before `RegisterDebugSession` has landed
612		// (rare race during spawn). The Cocoon-side handler dispatches
613		// based on session-id stored in `extHostDebug.ts`'s session map.
614		let TargetSidecar = SessionEntry
615			.as_ref()
616			.map(|E| E.SideCarIdentifier.clone())
617			.unwrap_or_else(|| "cocoon-main".to_string());
618		let SendDapMethod = format!("{}$sendDAPRequest", ProxyTarget::ExtHostDebug.GetTargetPrefix());
619		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
620		match IPCProvider
621			.SendRequestToSideCar(
622				TargetSidecar,
623				SendDapMethod,
624				json!([{ "sessionId": SessionID, "request": DapRequest }]),
625				15000,
626			)
627			.await
628		{
629			Ok(Response) => Ok(Response),
630			Err(Error) => {
631				dev_log!(
632					"exthost",
633					"warn: [DebugProvider] reverse-RPC SendCommand failed for session {}: {:?}",
634					SessionID,
635					Error
636				);
637				Err(Error)
638			},
639		}
640	}
641
642	async fn StopDebugging(&self, SessionID:String) -> Result<(), CommonError> {
643		dev_log!("exthost", "[DebugProvider] StopDebugging request for session '{}'", SessionID);
644
645		// Try a graceful DAP `disconnect` first so the adapter can flush
646		// pending state and let the debuggee detach cleanly. Failures
647		// are logged-and-tolerated; the unregister below force-closes
648		// the stdin pipe regardless.
649		if let Some(Entry) = self.ApplicationState.Feature.Debug.GetDebugSession(&SessionID) {
650			if let Some(Sender) = Entry.StdinSender.as_ref() {
651				let DisconnectRequest = json!({
652					"seq": 0,
653					"type": "request",
654					"command": "disconnect",
655					"arguments": { "restart": false, "terminateDebuggee": true },
656				});
657				if let Ok(Body) = serde_json::to_vec(&DisconnectRequest) {
658					let Header = format!("Content-Length: {}\r\n\r\n", Body.len());
659					let mut Frame = Vec::with_capacity(Header.len() + Body.len());
660					Frame.extend_from_slice(Header.as_bytes());
661					Frame.extend_from_slice(&Body);
662					let _ = Sender.send(Frame);
663				}
664			}
665		}
666		// Drop the entry. The drained `Sender` clone in the in-flight
667		// stdin writer task will see the channel close on its next `recv`
668		// and shut the adapter's stdin, which most adapters interpret
669		// as a graceful disconnect.
670		let _ = self.ApplicationState.Feature.Debug.UnregisterDebugSession(&SessionID);
671
672		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
673		let TerminateMethod = format!("{}$onDidTerminateDebugSession", ProxyTarget::ExtHostDebug.GetTargetPrefix());
674		if let Err(error) = IPCProvider
675			.SendNotificationToSideCar("cocoon-main".to_string(), TerminateMethod, json!([{ "id": SessionID.clone() }]))
676			.await
677		{
678			dev_log!(
679				"exthost",
680				"warn: [DebugProvider] StopDebugging notification failed for '{}': {:?}",
681				SessionID,
682				error
683			);
684		}
685		let _ = self
686			.ApplicationHandle
687			.emit("sky://debug/sessionEnd", json!({ "sessionId": SessionID.clone() }));
688		Ok(())
689	}
690}