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					let mut Pipe = Stdin;
315					while let Some(Frame) = Receiver.recv().await {
316						if let Err(Error) = Pipe.write_all(&Frame).await {
317							crate::dev_log!(
318								"exthost",
319								"warn: [DebugAdapter] stdin write failed for session {}: {}",
320								StdinSessionId,
321								Error
322							);
323							break;
324						}
325						if let Err(Error) = Pipe.flush().await {
326							crate::dev_log!(
327								"exthost",
328								"warn: [DebugAdapter] stdin flush failed for session {}: {}",
329								StdinSessionId,
330								Error
331							);
332							break;
333						}
334					}
335					let _ = Pipe.shutdown().await;
336				});
337
338				// Stdout reader task: parses DAP frames
339				// (`Content-Length: <n>\r\n\r\n<json>`) and re-emits each
340				// JSON message on `sky://debug/dap-message` so the
341				// renderer / Cocoon-side reverse-RPC can route it to the
342				// originating session listener. Errors break the read
343				// loop and trigger session cleanup.
344				let StdoutSessionId = SessionID.clone();
345
346				let StdoutHandle = self.ApplicationHandle.clone();
347
348				let StdoutSidecar = TargetSideCar.clone();
349
350				tokio::spawn(async move {
351					use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
352					let mut Reader = BufReader::new(Stdout);
353					let mut Header = String::new();
354					loop {
355						Header.clear();
356						let mut ContentLength:usize = 0;
357						loop {
358							Header.clear();
359							match Reader.read_line(&mut Header).await {
360								Ok(0) => return, // EOF
361								Ok(_) => {},
362								Err(Error) => {
363									crate::dev_log!(
364										"exthost",
365										"warn: [DebugAdapter] stdout read failed for session {}: {}",
366										StdoutSessionId,
367										Error
368									);
369									return;
370								},
371							}
372							let Trimmed = Header.trim_end_matches("\r\n").trim_end_matches('\n');
373							if Trimmed.is_empty() {
374								break;
375							}
376							if let Some(Rest) = Trimmed.strip_prefix("Content-Length:") {
377								if let Ok(N) = Rest.trim().parse::<usize>() {
378									ContentLength = N;
379								}
380							}
381						}
382						if ContentLength == 0 {
383							continue;
384						}
385						let mut Body = vec![0u8; ContentLength];
386						if let Err(Error) = Reader.read_exact(&mut Body).await {
387							crate::dev_log!(
388								"exthost",
389								"warn: [DebugAdapter] stdout body read failed for session {}: {}",
390								StdoutSessionId,
391								Error
392							);
393							return;
394						}
395						let Parsed:Value = serde_json::from_slice(&Body).unwrap_or(Value::Null);
396						let _ = StdoutHandle.emit(
397							"sky://debug/dap-message",
398							json!({
399								"sessionId": StdoutSessionId,
400								"sidecarId": StdoutSidecar,
401								"message": Parsed,
402							}),
403						);
404					}
405				});
406
407				// Stderr drain: emit each line as a dev_log line so adapter
408				// crash reasons surface alongside other Mountain logs.
409				let StderrSessionId = SessionID.clone();
410
411				tokio::spawn(async move {
412					use tokio::io::{AsyncBufReadExt, BufReader};
413					let mut Lines = BufReader::new(Stderr).lines();
414					while let Ok(Some(Line)) = Lines.next_line().await {
415						crate::dev_log!("exthost", "[DebugAdapter] stderr session={}: {}", StderrSessionId, Line);
416					}
417				});
418
419				AdapterStdinSender = Some(Sender);
420
421				AdapterChildPid = Pid;
422
423				dev_log!(
424					"exthost",
425					"[DebugProvider] Spawned executable adapter for session '{}' pid={:?} command={:?}",
426					SessionID,
427					Pid,
428					Command
429				);
430			},
431
432			"server" | "pipeServer" => {
433				dev_log!(
434					"exthost",
435					"warn: [DebugProvider] Adapter type '{}' not yet wired (session '{}'). Reverse-RPC dispatch only.",
436					DescriptorType,
437					SessionID
438				);
439
440				AdapterStdinSender = None;
441
442				AdapterChildPid = None;
443			},
444
445			"implementation" => {
446				dev_log!(
447					"exthost",
448					"[DebugProvider] Inline implementation adapter for session '{}' - DAP frames travel via Cocoon \
449					 reverse-RPC.",
450					SessionID
451				);
452
453				AdapterStdinSender = None;
454
455				AdapterChildPid = None;
456			},
457
458			_ => {
459				dev_log!(
460					"exthost",
461					"warn: [DebugProvider] Unknown adapter descriptor type '{}' for session '{}' - registering \
462					 session without spawn.",
463					DescriptorType,
464					SessionID
465				);
466
467				AdapterStdinSender = None;
468
469				AdapterChildPid = None;
470			},
471		}
472
473		// Persist the session in ApplicationState so SendCommand can
474		// resolve it. Without this, every subsequent DAP command from the
475		// workbench would land on the "session not found" path even though
476		// the adapter is alive and listening.
477		if let Err(RegError) = self.ApplicationState.Feature.Debug.RegisterDebugSession(
478			crate::ApplicationState::State::FeatureState::Debug::DebugState::DebugSessionEntry {
479				SessionId:SessionID.clone(),
480				DebugType:DebugType.clone(),
481				SideCarIdentifier:TargetSideCar.clone(),
482				StdinSender:AdapterStdinSender,
483				ChildPid:AdapterChildPid,
484			},
485		) {
486			dev_log!(
487				"exthost",
488				"warn: [DebugProvider] Failed to register session '{}' in DebugState: {}",
489				SessionID,
490				RegError
491			);
492		}
493
494		// Notify Cocoon that the session has started so any
495		// `vscode.debug.onDidStartDebugSession` listeners (registered
496		// from extensions through `DebugNamespace.ts:124`) fire. The
497		// payload mirrors VS Code's wire shape - extensions read
498		// `id`, `type`, `name`, and `configuration` off the session
499		// object passed to the listener. Until full session tracking
500		// lands in ApplicationState we synthesise from the resolved
501		// configuration so extensions can observe activation even
502		// while the adapter spawn path is still a stub.
503		let StartedMethod = format!("{}$onDidStartDebugSession", ProxyTarget::ExtHostDebug.GetTargetPrefix());
504
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
512		if let Err(error) = IPCProvider
513			.SendNotificationToSideCar(TargetSideCar.clone(), StartedMethod, json!([StartedSession]))
514			.await
515		{
516			dev_log!(
517				"exthost",
518				"warn: [DebugProvider] StartDebugging notification failed for '{}': {:?}",
519				SessionID,
520				error
521			);
522		}
523
524		// Sky-side debug viewlet observers consume this stream so the
525		// debug toolbar / call stack panel light up without waiting on
526		// the typed `DebugService::ActiveSessions` snapshot. Mirrors
527		// `WebviewLifecycle.rs`'s pattern of dual-emitting to Cocoon
528		// (typed RPC) and Sky (renderer event).
529		let _ = self.ApplicationHandle.emit(
530			"sky://debug/sessionStart",
531			json!({
532				"sessionId": SessionID.clone(),
533				"type": DebugType.clone(),
534				"configuration": ResolvedConfig.clone(),
535			}),
536		);
537
538		dev_log!("exthost", "[DebugProvider] Debug session '{}' started (simulation).", SessionID);
539
540		Ok(SessionID)
541	}
542
543	async fn SendCommand(&self, SessionID:String, Command:String, Arguments:Value) -> Result<Value, CommonError> {
544		dev_log!(
545			"exthost",
546			"[DebugProvider] SendCommand for session '{}' (command: '{}', args: {:?})",
547			SessionID,
548			Command,
549			Arguments
550		);
551
552		// Resolve the active session. Missing entries fall through to the
553		// reverse-RPC path below so commands targeting an inline-impl
554		// adapter (DebugAdapterInlineImplementation - JS-only adapters
555		// running inside Cocoon) still reach their handler.
556		let SessionEntry = self.ApplicationState.Feature.Debug.GetDebugSession(&SessionID);
557
558		// DAP framing: producer must wrap the JSON message in a
559		// `Content-Length: <n>\r\n\r\n<body>` header. Sequence numbers
560		// are caller-allocated (the workbench's `RawDebugSession` keeps
561		// its own `_currentReqId`); we don't reorder. Wire the request
562		// shape that VS Code's `mainThreadDebugService.ts` produces:
563		// `{ seq, type: "request", command, arguments }`. Mountain
564		// doesn't currently track per-session seq numbers - upstream
565		// VS Code increments request_seq on the WORKBENCH side and we
566		// just forward verbatim - so we emit `0` here as a placeholder
567		// when the caller hasn't supplied one in `Arguments.seq`.
568		let RequestSeq = Arguments.get("seq").and_then(Value::as_u64).unwrap_or(0);
569
570		let RequestArguments = Arguments.get("arguments").cloned().unwrap_or(Arguments.clone());
571
572		let DapRequest = json!({
573			"seq": RequestSeq,
574			"type": "request",
575			"command": Command,
576			"arguments": RequestArguments,
577		});
578
579		if let Some(Entry) = SessionEntry.as_ref() {
580			if let Some(Sender) = Entry.StdinSender.as_ref() {
581				let Body = serde_json::to_vec(&DapRequest).map_err(|Error| {
582					CommonError::IPCError {
583						Description:format!("Failed to serialize DAP request for session {}: {}", SessionID, Error),
584					}
585				})?;
586
587				let Header = format!("Content-Length: {}\r\n\r\n", Body.len());
588
589				let mut Frame = Vec::with_capacity(Header.len() + Body.len());
590
591				Frame.extend_from_slice(Header.as_bytes());
592
593				Frame.extend_from_slice(&Body);
594
595				Sender.send(Frame).map_err(|Error| {
596					CommonError::IPCError {
597						Description:format!("Adapter stdin channel for session {} closed: {}", SessionID, Error),
598					}
599				})?;
600
601				// stdio adapters reply asynchronously through the
602				// stdout reader task, which fans the response out via
603				// `sky://debug/dap-message`. Returning an ack now lets
604				// the workbench's request sequencer continue; the actual
605				// response is correlated by `request_seq` on the
606				// renderer side.
607				return Ok(json!({
608					"success": true,
609					"sessionId": SessionID,
610					"command": Command,
611					"transport": "stdio",
612				}));
613			}
614		}
615
616		// No live stdin pipe: route via reverse-RPC into the owning
617		// sidecar. This covers (1) sessions created with
618		// `DebugAdapterInlineImplementation` where the adapter runs
619		// inside the extension host, (2) `server` / `pipeServer`
620		// descriptors awaiting their connection wiring, and (3)
621		// commands fired before `RegisterDebugSession` has landed
622		// (rare race during spawn). The Cocoon-side handler dispatches
623		// based on session-id stored in `extHostDebug.ts`'s session map.
624		let TargetSidecar = SessionEntry
625			.as_ref()
626			.map(|E| E.SideCarIdentifier.clone())
627			.unwrap_or_else(|| "cocoon-main".to_string());
628
629		let SendDapMethod = format!("{}$sendDAPRequest", ProxyTarget::ExtHostDebug.GetTargetPrefix());
630
631		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
632
633		match IPCProvider
634			.SendRequestToSideCar(
635				TargetSidecar,
636				SendDapMethod,
637				json!([{ "sessionId": SessionID, "request": DapRequest }]),
638				15000,
639			)
640			.await
641		{
642			Ok(Response) => Ok(Response),
643
644			Err(Error) => {
645				dev_log!(
646					"exthost",
647					"warn: [DebugProvider] reverse-RPC SendCommand failed for session {}: {:?}",
648					SessionID,
649					Error
650				);
651
652				Err(Error)
653			},
654		}
655	}
656
657	async fn StopDebugging(&self, SessionID:String) -> Result<(), CommonError> {
658		dev_log!("exthost", "[DebugProvider] StopDebugging request for session '{}'", SessionID);
659
660		// Try a graceful DAP `disconnect` first so the adapter can flush
661		// pending state and let the debuggee detach cleanly. Failures
662		// are logged-and-tolerated; the unregister below force-closes
663		// the stdin pipe regardless.
664		if let Some(Entry) = self.ApplicationState.Feature.Debug.GetDebugSession(&SessionID) {
665			if let Some(Sender) = Entry.StdinSender.as_ref() {
666				let DisconnectRequest = json!({
667					"seq": 0,
668					"type": "request",
669					"command": "disconnect",
670					"arguments": { "restart": false, "terminateDebuggee": true },
671				});
672
673				if let Ok(Body) = serde_json::to_vec(&DisconnectRequest) {
674					let Header = format!("Content-Length: {}\r\n\r\n", Body.len());
675
676					let mut Frame = Vec::with_capacity(Header.len() + Body.len());
677
678					Frame.extend_from_slice(Header.as_bytes());
679
680					Frame.extend_from_slice(&Body);
681
682					let _ = Sender.send(Frame);
683				}
684			}
685		}
686
687		// Drop the entry. The drained `Sender` clone in the in-flight
688		// stdin writer task will see the channel close on its next `recv`
689		// and shut the adapter's stdin, which most adapters interpret
690		// as a graceful disconnect.
691		let _ = self.ApplicationState.Feature.Debug.UnregisterDebugSession(&SessionID);
692
693		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
694
695		let TerminateMethod = format!("{}$onDidTerminateDebugSession", ProxyTarget::ExtHostDebug.GetTargetPrefix());
696
697		if let Err(error) = IPCProvider
698			.SendNotificationToSideCar("cocoon-main".to_string(), TerminateMethod, json!([{ "id": SessionID.clone() }]))
699			.await
700		{
701			dev_log!(
702				"exthost",
703				"warn: [DebugProvider] StopDebugging notification failed for '{}': {:?}",
704				SessionID,
705				error
706			);
707		}
708
709		let _ = self
710			.ApplicationHandle
711			.emit("sky://debug/sessionEnd", json!({ "sessionId": SessionID.clone() }));
712
713		Ok(())
714	}
715}