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		// TODO: Look up which sidecar handles this debug type using
154		// RegisterDebugConfigurationProvider registrations in ApplicationState.
155		// Hardcoded "cocoon-main" until proper registration tracking is implemented.
156		let TargetSideCar = "cocoon-main".to_string();
157
158		// 1. Resolve configuration (Reverse-RPC to Cocoon)
159		dev_log!(
160			"exthost",
161			"[DebugProvider] Resolving debug configuration for type '{}'",
162			DebugType
163		);
164
165		dev_log!("exthost", "[DebugProvider] Resolving debug configuration...");
166
167		let ResolveConfigMethod = format!("{}$resolveDebugConfiguration", ProxyTarget::ExtHostDebug.GetTargetPrefix());
168
169		let ResolvedConfig = IPCProvider
170			.SendRequestToSideCar(
171				TargetSideCar.clone(),
172				ResolveConfigMethod,
173				json!([DebugType.clone(), Configuration]),
174				5000,
175			)
176			.await?;
177
178		// 2. Get the Debug Adapter Descriptor (Reverse-RPC to Cocoon)
179		dev_log!("exthost", "[DebugProvider] Creating debug adapter descriptor...");
180
181		let CreateDescriptorMethod =
182			format!("{}$createDebugAdapterDescriptor", ProxyTarget::ExtHostDebug.GetTargetPrefix());
183
184		let Descriptor = IPCProvider
185			.SendRequestToSideCar(
186				TargetSideCar.clone(),
187				CreateDescriptorMethod,
188				json!([DebugType, &ResolvedConfig]),
189				5000,
190			)
191			.await?;
192
193		// 3. Spawn the Debug Adapter process based on the descriptor.
194		dev_log!(
195			"exthost",
196			"[DebugProvider] Spawning Debug Adapter based on descriptor: {:?}",
197			Descriptor
198		);
199
200		// Adapter-descriptor DTO shapes mirror VS Code's
201		// `vs/workbench/api/common/extHostDebugService.ts::convert*ToDto`:
202		//   executable  → { type: "executable", command, args, options: { env?, cwd? }
203		// }   server      → { type: "server", port, host? }
204		//   pipeServer  → { type: "pipeServer", path }
205		//   implementation → { type: "implementation" }   (handled in-process by
206		// Cocoon)
207		//
208		// Phase 1 supports `executable` only - covers every JS/TS debug adapter
209		// (vscode-js-debug, node) and most language-server-driven adapters that
210		// ship as a CLI binary. Server/pipeServer connections are stubbed with a
211		// warn-log + a session-registry entry without a StdinSender, so SendCommand
212		// can surface "adapter type unsupported" instead of a silent no-op.
213		// TODO: Wire server / pipeServer adapter connection (TCP / named-pipe).
214		// TODO: Wire reverse-RPC `$sendDAPRequest` Cocoon handler for inline-impl
215		// adapters.
216		let DescriptorType = Descriptor.get("type").and_then(Value::as_str).unwrap_or("").to_string();
217
218		let AdapterStdinSender:Option<tokio::sync::mpsc::UnboundedSender<Vec<u8>>>;
219
220		let AdapterChildPid:Option<u32>;
221
222		match DescriptorType.as_str() {
223			"executable" => {
224				let Command = Descriptor
225					.get("command")
226					.and_then(Value::as_str)
227					.ok_or_else(|| {
228						CommonError::InvalidArgument {
229							ArgumentName:"Descriptor.command".into(),
230							Reason:"executable adapter descriptor missing 'command'".into(),
231						}
232					})?
233					.to_string();
234
235				let Args:Vec<String> = Descriptor
236					.get("args")
237					.and_then(Value::as_array)
238					.map(|A| A.iter().filter_map(|V| V.as_str().map(str::to_string)).collect())
239					.unwrap_or_default();
240
241				let OptionsValue = Descriptor.get("options").cloned().unwrap_or(Value::Null);
242
243				let Cwd = OptionsValue.get("cwd").and_then(Value::as_str).map(str::to_string);
244
245				let EnvOverrides:Vec<(String, String)> = OptionsValue
246					.get("env")
247					.and_then(Value::as_object)
248					.map(|O| {
249						O.iter()
250							.filter_map(|(K, V)| V.as_str().map(|S| (K.clone(), S.to_string())))
251							.collect()
252					})
253					.unwrap_or_default();
254
255				let mut Builder = tokio::process::Command::new(&Command);
256
257				Builder
258					.args(&Args)
259					.stdin(std::process::Stdio::piped())
260					.stdout(std::process::Stdio::piped())
261					.stderr(std::process::Stdio::piped());
262
263				if let Some(CwdPath) = &Cwd {
264					Builder.current_dir(CwdPath);
265				}
266
267				for (Key, Value) in &EnvOverrides {
268					Builder.env(Key, Value);
269				}
270
271				let mut Child = Builder.spawn().map_err(|Error| {
272					CommonError::IPCError {
273						Description:format!(
274							"Failed to spawn debug adapter '{}' for session {}: {}",
275							Command, SessionID, Error
276						),
277					}
278				})?;
279
280				let Pid = Child.id();
281
282				let Stdin = Child.stdin.take().ok_or_else(|| {
283					CommonError::IPCError { Description:format!("Adapter for session {} had no stdin pipe", SessionID) }
284				})?;
285
286				let Stdout = Child.stdout.take().ok_or_else(|| {
287					CommonError::IPCError {
288						Description:format!("Adapter for session {} had no stdout pipe", SessionID),
289					}
290				})?;
291
292				let Stderr = Child.stderr.take().ok_or_else(|| {
293					CommonError::IPCError {
294						Description:format!("Adapter for session {} had no stderr pipe", SessionID),
295					}
296				})?;
297
298				let (Sender, mut Receiver) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
299
300				// Stdin writer task: drains the mpsc channel into the
301				// adapter's stdin. Closes when the channel's sender is
302				// dropped (UnregisterDebugSession) which propagates EOF
303				// to the adapter and triggers its shutdown.
304				let StdinSessionId = SessionID.clone();
305
306				tokio::spawn(async move {
307					use tokio::io::AsyncWriteExt;
308					let mut Pipe = Stdin;
309					while let Some(Frame) = Receiver.recv().await {
310						if let Err(Error) = Pipe.write_all(&Frame).await {
311							crate::dev_log!(
312								"exthost",
313								"warn: [DebugAdapter] stdin write failed for session {}: {}",
314								StdinSessionId,
315								Error
316							);
317							break;
318						}
319						if let Err(Error) = Pipe.flush().await {
320							crate::dev_log!(
321								"exthost",
322								"warn: [DebugAdapter] stdin flush failed for session {}: {}",
323								StdinSessionId,
324								Error
325							);
326							break;
327						}
328					}
329					let _ = Pipe.shutdown().await;
330				});
331
332				// Stdout reader task: parses DAP frames
333				// (`Content-Length: <n>\r\n\r\n<json>`) and re-emits each
334				// JSON message on `sky://debug/dap-message` so the
335				// renderer / Cocoon-side reverse-RPC can route it to the
336				// originating session listener. Errors break the read
337				// loop and trigger session cleanup.
338				let StdoutSessionId = SessionID.clone();
339
340				let StdoutHandle = self.ApplicationHandle.clone();
341
342				let StdoutSidecar = TargetSideCar.clone();
343
344				tokio::spawn(async move {
345					use tokio::io::{AsyncBufReadExt, AsyncReadExt, BufReader};
346					let mut Reader = BufReader::new(Stdout);
347					let mut Header = String::new();
348					loop {
349						Header.clear();
350						let mut ContentLength:usize = 0;
351						loop {
352							Header.clear();
353							match Reader.read_line(&mut Header).await {
354								Ok(0) => return, // EOF
355								Ok(_) => {},
356								Err(Error) => {
357									crate::dev_log!(
358										"exthost",
359										"warn: [DebugAdapter] stdout read failed for session {}: {}",
360										StdoutSessionId,
361										Error
362									);
363									return;
364								},
365							}
366							let Trimmed = Header.trim_end_matches("\r\n").trim_end_matches('\n');
367							if Trimmed.is_empty() {
368								break;
369							}
370							if let Some(Rest) = Trimmed.strip_prefix("Content-Length:") {
371								if let Ok(N) = Rest.trim().parse::<usize>() {
372									ContentLength = N;
373								}
374							}
375						}
376						if ContentLength == 0 {
377							continue;
378						}
379						let mut Body = vec![0u8; ContentLength];
380						if let Err(Error) = Reader.read_exact(&mut Body).await {
381							crate::dev_log!(
382								"exthost",
383								"warn: [DebugAdapter] stdout body read failed for session {}: {}",
384								StdoutSessionId,
385								Error
386							);
387							return;
388						}
389						let Parsed:Value = serde_json::from_slice(&Body).unwrap_or(Value::Null);
390						let _ = StdoutHandle.emit(
391							"sky://debug/dap-message",
392							json!({
393								"sessionId": StdoutSessionId,
394								"sidecarId": StdoutSidecar,
395								"message": Parsed,
396							}),
397						);
398					}
399				});
400
401				// Stderr drain: emit each line as a dev_log line so adapter
402				// crash reasons surface alongside other Mountain logs.
403				let StderrSessionId = SessionID.clone();
404
405				tokio::spawn(async move {
406					use tokio::io::{AsyncBufReadExt, BufReader};
407					let mut Lines = BufReader::new(Stderr).lines();
408					while let Ok(Some(Line)) = Lines.next_line().await {
409						crate::dev_log!("exthost", "[DebugAdapter] stderr session={}: {}", StderrSessionId, Line);
410					}
411				});
412
413				AdapterStdinSender = Some(Sender);
414
415				AdapterChildPid = Pid;
416
417				dev_log!(
418					"exthost",
419					"[DebugProvider] Spawned executable adapter for session '{}' pid={:?} command={:?}",
420					SessionID,
421					Pid,
422					Command
423				);
424			},
425
426			"server" | "pipeServer" => {
427				dev_log!(
428					"exthost",
429					"warn: [DebugProvider] Adapter type '{}' not yet wired (session '{}'). Reverse-RPC dispatch only.",
430					DescriptorType,
431					SessionID
432				);
433
434				AdapterStdinSender = None;
435
436				AdapterChildPid = None;
437			},
438
439			"implementation" => {
440				dev_log!(
441					"exthost",
442					"[DebugProvider] Inline implementation adapter for session '{}' - DAP frames travel via Cocoon \
443					 reverse-RPC.",
444					SessionID
445				);
446
447				AdapterStdinSender = None;
448
449				AdapterChildPid = None;
450			},
451
452			_ => {
453				dev_log!(
454					"exthost",
455					"warn: [DebugProvider] Unknown adapter descriptor type '{}' for session '{}' - registering \
456					 session without spawn.",
457					DescriptorType,
458					SessionID
459				);
460
461				AdapterStdinSender = None;
462
463				AdapterChildPid = None;
464			},
465		}
466
467		// Persist the session in ApplicationState so SendCommand can
468		// resolve it. Without this, every subsequent DAP command from the
469		// workbench would land on the "session not found" path even though
470		// the adapter is alive and listening.
471		if let Err(RegError) = self.ApplicationState.Feature.Debug.RegisterDebugSession(
472			crate::ApplicationState::State::FeatureState::Debug::DebugState::DebugSessionEntry {
473				SessionId:SessionID.clone(),
474				DebugType:DebugType.clone(),
475				SideCarIdentifier:TargetSideCar.clone(),
476				StdinSender:AdapterStdinSender,
477				ChildPid:AdapterChildPid,
478			},
479		) {
480			dev_log!(
481				"exthost",
482				"warn: [DebugProvider] Failed to register session '{}' in DebugState: {}",
483				SessionID,
484				RegError
485			);
486		}
487
488		// Notify Cocoon that the session has started so any
489		// `vscode.debug.onDidStartDebugSession` listeners (registered
490		// from extensions through `DebugNamespace.ts:124`) fire. The
491		// payload mirrors VS Code's wire shape - extensions read
492		// `id`, `type`, `name`, and `configuration` off the session
493		// object passed to the listener. Until full session tracking
494		// lands in ApplicationState we synthesise from the resolved
495		// configuration so extensions can observe activation even
496		// while the adapter spawn path is still a stub.
497		let StartedMethod = format!("{}$onDidStartDebugSession", ProxyTarget::ExtHostDebug.GetTargetPrefix());
498
499		let StartedSession = json!({
500			"id": SessionID.clone(),
501			"type": DebugType.clone(),
502			"name": ResolvedConfig.get("name").and_then(Value::as_str).unwrap_or(&DebugType),
503			"configuration": ResolvedConfig.clone(),
504		});
505
506		if let Err(error) = IPCProvider
507			.SendNotificationToSideCar(TargetSideCar.clone(), StartedMethod, json!([StartedSession]))
508			.await
509		{
510			dev_log!(
511				"exthost",
512				"warn: [DebugProvider] StartDebugging notification failed for '{}': {:?}",
513				SessionID,
514				error
515			);
516		}
517
518		// Sky-side debug viewlet observers consume this stream so the
519		// debug toolbar / call stack panel light up without waiting on
520		// the typed `DebugService::ActiveSessions` snapshot. Mirrors
521		// `WebviewLifecycle.rs`'s pattern of dual-emitting to Cocoon
522		// (typed RPC) and Sky (renderer event).
523		let _ = self.ApplicationHandle.emit(
524			"sky://debug/sessionStart",
525			json!({
526				"sessionId": SessionID.clone(),
527				"type": DebugType.clone(),
528				"configuration": ResolvedConfig.clone(),
529			}),
530		);
531
532		dev_log!("exthost", "[DebugProvider] Debug session '{}' started (simulation).", SessionID);
533
534		Ok(SessionID)
535	}
536
537	async fn SendCommand(&self, SessionID:String, Command:String, Arguments:Value) -> Result<Value, CommonError> {
538		dev_log!(
539			"exthost",
540			"[DebugProvider] SendCommand for session '{}' (command: '{}', args: {:?})",
541			SessionID,
542			Command,
543			Arguments
544		);
545
546		// Resolve the active session. Missing entries fall through to the
547		// reverse-RPC path below so commands targeting an inline-impl
548		// adapter (DebugAdapterInlineImplementation - JS-only adapters
549		// running inside Cocoon) still reach their handler.
550		let SessionEntry = self.ApplicationState.Feature.Debug.GetDebugSession(&SessionID);
551
552		// DAP framing: producer must wrap the JSON message in a
553		// `Content-Length: <n>\r\n\r\n<body>` header. Sequence numbers
554		// are caller-allocated (the workbench's `RawDebugSession` keeps
555		// its own `_currentReqId`); we don't reorder. Wire the request
556		// shape that VS Code's `mainThreadDebugService.ts` produces:
557		// `{ seq, type: "request", command, arguments }`. Mountain
558		// doesn't currently track per-session seq numbers - upstream
559		// VS Code increments request_seq on the WORKBENCH side and we
560		// just forward verbatim - so we emit `0` here as a placeholder
561		// when the caller hasn't supplied one in `Arguments.seq`.
562		let RequestSeq = Arguments.get("seq").and_then(Value::as_u64).unwrap_or(0);
563
564		let RequestArguments = Arguments.get("arguments").cloned().unwrap_or(Arguments.clone());
565
566		let DapRequest = json!({
567			"seq": RequestSeq,
568			"type": "request",
569			"command": Command,
570			"arguments": RequestArguments,
571		});
572
573		if let Some(Entry) = SessionEntry.as_ref() {
574			if let Some(Sender) = Entry.StdinSender.as_ref() {
575				let Body = serde_json::to_vec(&DapRequest).map_err(|Error| {
576					CommonError::IPCError {
577						Description:format!("Failed to serialize DAP request for session {}: {}", SessionID, Error),
578					}
579				})?;
580
581				let Header = format!("Content-Length: {}\r\n\r\n", Body.len());
582
583				let mut Frame = Vec::with_capacity(Header.len() + Body.len());
584
585				Frame.extend_from_slice(Header.as_bytes());
586
587				Frame.extend_from_slice(&Body);
588
589				Sender.send(Frame).map_err(|Error| {
590					CommonError::IPCError {
591						Description:format!("Adapter stdin channel for session {} closed: {}", SessionID, Error),
592					}
593				})?;
594
595				// stdio adapters reply asynchronously through the
596				// stdout reader task, which fans the response out via
597				// `sky://debug/dap-message`. Returning an ack now lets
598				// the workbench's request sequencer continue; the actual
599				// response is correlated by `request_seq` on the
600				// renderer side.
601				return Ok(json!({
602					"success": true,
603					"sessionId": SessionID,
604					"command": Command,
605					"transport": "stdio",
606				}));
607			}
608		}
609
610		// No live stdin pipe: route via reverse-RPC into the owning
611		// sidecar. This covers (1) sessions created with
612		// `DebugAdapterInlineImplementation` where the adapter runs
613		// inside the extension host, (2) `server` / `pipeServer`
614		// descriptors awaiting their connection wiring, and (3)
615		// commands fired before `RegisterDebugSession` has landed
616		// (rare race during spawn). The Cocoon-side handler dispatches
617		// based on session-id stored in `extHostDebug.ts`'s session map.
618		let TargetSidecar = SessionEntry
619			.as_ref()
620			.map(|E| E.SideCarIdentifier.clone())
621			.unwrap_or_else(|| "cocoon-main".to_string());
622
623		let SendDapMethod = format!("{}$sendDAPRequest", ProxyTarget::ExtHostDebug.GetTargetPrefix());
624
625		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
626
627		match IPCProvider
628			.SendRequestToSideCar(
629				TargetSidecar,
630				SendDapMethod,
631				json!([{ "sessionId": SessionID, "request": DapRequest }]),
632				15000,
633			)
634			.await
635		{
636			Ok(Response) => Ok(Response),
637
638			Err(Error) => {
639				dev_log!(
640					"exthost",
641					"warn: [DebugProvider] reverse-RPC SendCommand failed for session {}: {:?}",
642					SessionID,
643					Error
644				);
645
646				Err(Error)
647			},
648		}
649	}
650
651	async fn StopDebugging(&self, SessionID:String) -> Result<(), CommonError> {
652		dev_log!("exthost", "[DebugProvider] StopDebugging request for session '{}'", SessionID);
653
654		// Try a graceful DAP `disconnect` first so the adapter can flush
655		// pending state and let the debuggee detach cleanly. Failures
656		// are logged-and-tolerated; the unregister below force-closes
657		// the stdin pipe regardless.
658		if let Some(Entry) = self.ApplicationState.Feature.Debug.GetDebugSession(&SessionID) {
659			if let Some(Sender) = Entry.StdinSender.as_ref() {
660				let DisconnectRequest = json!({
661					"seq": 0,
662					"type": "request",
663					"command": "disconnect",
664					"arguments": { "restart": false, "terminateDebuggee": true },
665				});
666
667				if let Ok(Body) = serde_json::to_vec(&DisconnectRequest) {
668					let Header = format!("Content-Length: {}\r\n\r\n", Body.len());
669
670					let mut Frame = Vec::with_capacity(Header.len() + Body.len());
671
672					Frame.extend_from_slice(Header.as_bytes());
673
674					Frame.extend_from_slice(&Body);
675
676					let _ = Sender.send(Frame);
677				}
678			}
679		}
680
681		// Drop the entry. The drained `Sender` clone in the in-flight
682		// stdin writer task will see the channel close on its next `recv`
683		// and shut the adapter's stdin, which most adapters interpret
684		// as a graceful disconnect.
685		let _ = self.ApplicationState.Feature.Debug.UnregisterDebugSession(&SessionID);
686
687		let IPCProvider:Arc<dyn IPCProvider> = self.Require();
688
689		let TerminateMethod = format!("{}$onDidTerminateDebugSession", ProxyTarget::ExtHostDebug.GetTargetPrefix());
690
691		if let Err(error) = IPCProvider
692			.SendNotificationToSideCar("cocoon-main".to_string(), TerminateMethod, json!([{ "id": SessionID.clone() }]))
693			.await
694		{
695			dev_log!(
696				"exthost",
697				"warn: [DebugProvider] StopDebugging notification failed for '{}': {:?}",
698				SessionID,
699				error
700			);
701		}
702
703		let _ = self
704			.ApplicationHandle
705			.emit("sky://debug/sessionEnd", json!({ "sessionId": SessionID.clone() }));
706
707		Ok(())
708	}
709}