debugger: Fix shutdown issues (#27071)

This PR fixes a few issues around shutting down a debug adapter.

The first issue I discovered was when I shut down all sessions via
`shutdown all adapters` command. We would still fetch the threads
request again, because we receive a thread event that indicated that it
exited. But this will always time out because the debug adapter is
already shutdown at this point, so by updating the check so we don't
allow fetching a request when the session is terminated fixes the issue.

The second issue fixes a bug where we would always shut down the parent
session, when a child session is terminated. This was reintroduced by
the big refactor. This is not something we want, because you could
receive multiple StartDebugging reverse requests, so if one child is
shutting down that does not mean the other ones should have been
shutting down as well.
Issue was original fixed in
https://github.com/RemcoSmitsDev/zed/pull/80#issuecomment-2573943661.


## TODO:
- [x] Add tests

Release Notes:

- N/A
This commit is contained in:
Remco Smits 2025-03-20 19:32:37 +01:00 committed by GitHub
parent 7b80cd865d
commit ac452799b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 287 additions and 6 deletions

View file

@ -28,7 +28,7 @@ use dap::{
use fs::Fs;
use futures::{
channel::{mpsc, oneshot},
future::Shared,
future::{join_all, Shared},
};
use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task};
use http_client::HttpClient;
@ -348,6 +348,12 @@ impl DapStore {
);
let session_id = local_store.next_session_id();
if let Some(session) = &parent_session {
session.update(cx, |session, _| {
session.add_child_session_id(session_id);
});
}
let (initialized_tx, initialized_rx) = oneshot::channel();
let start_client_task = Session::local(
@ -764,13 +770,40 @@ impl DapStore {
return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id)));
};
let shutdown_parent_task = session
let shutdown_children = session
.read(cx)
.child_session_ids()
.iter()
.map(|session_id| self.shutdown_session(*session_id, cx))
.collect::<Vec<_>>();
let shutdown_parent_task = if let Some(parent_session) = session
.read(cx)
.parent_id()
.map(|parent_id| self.shutdown_session(parent_id, cx));
.and_then(|session_id| self.session_by_id(session_id))
{
let shutdown_id = parent_session.update(cx, |parent_session, _| {
parent_session.remove_child_session_id(session_id);
if parent_session.child_session_ids().len() == 0 {
Some(parent_session.session_id())
} else {
None
}
});
shutdown_id.map(|session_id| self.shutdown_session(session_id, cx))
} else {
None
};
let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx));
cx.background_spawn(async move {
if shutdown_children.len() > 0 {
let _ = join_all(shutdown_children).await;
}
shutdown_task.await;
if let Some(parent_task) = shutdown_parent_task {

View file

@ -11,7 +11,7 @@ use super::dap_command::{
};
use super::dap_store::DapAdapterDelegate;
use anyhow::{anyhow, Result};
use collections::{HashMap, IndexMap, IndexSet};
use collections::{HashMap, HashSet, IndexMap, IndexSet};
use dap::adapters::{DebugAdapter, DebugAdapterBinary};
use dap::messages::Response;
use dap::OutputEventCategory;
@ -522,6 +522,11 @@ impl ThreadStates {
self.known_thread_states.clear();
}
fn exit_all_threads(&mut self) {
self.global_state = Some(ThreadStatus::Exited);
self.known_thread_states.clear();
}
fn continue_all_threads(&mut self) {
self.global_state = Some(ThreadStatus::Running);
self.known_thread_states.clear();
@ -577,6 +582,7 @@ pub struct Session {
mode: Mode,
pub(super) capabilities: Capabilities,
id: SessionId,
child_session_ids: HashSet<SessionId>,
parent_id: Option<SessionId>,
ignore_breakpoints: bool,
modules: Vec<dap::Module>,
@ -753,6 +759,7 @@ impl Session {
Self {
mode: Mode::Local(mode),
id: session_id,
child_session_ids: HashSet::default(),
parent_id: parent_session.map(|session| session.read(cx).id),
variables: Default::default(),
capabilities,
@ -785,13 +792,13 @@ impl Session {
_upstream_project_id: upstream_project_id,
}),
id: session_id,
child_session_ids: HashSet::default(),
parent_id: None,
capabilities: Capabilities::default(),
ignore_breakpoints,
variables: Default::default(),
stack_frames: Default::default(),
thread_states: ThreadStates::default(),
output_token: OutputToken(0),
output: circular_buffer::CircularBuffer::boxed(),
requests: HashMap::default(),
@ -808,6 +815,18 @@ impl Session {
self.id
}
pub fn child_session_ids(&self) -> HashSet<SessionId> {
self.child_session_ids.clone()
}
pub fn add_child_session_id(&mut self, session_id: SessionId) {
self.child_session_ids.insert(session_id);
}
pub fn remove_child_session_id(&mut self, session_id: SessionId) {
self.child_session_ids.remove(&session_id);
}
pub fn parent_id(&self) -> Option<SessionId> {
self.parent_id
}
@ -1051,6 +1070,7 @@ impl Session {
if !self.thread_states.any_stopped_thread()
&& request.type_id() != TypeId::of::<ThreadsCommand>()
|| self.is_session_terminated
{
return;
}
@ -1331,6 +1351,10 @@ impl Session {
}
pub fn shutdown(&mut self, cx: &mut Context<Self>) -> Task<()> {
self.is_session_terminated = true;
self.thread_states.exit_all_threads();
cx.notify();
let task = if self
.capabilities
.supports_terminate_request