diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index d87705e763e32b348f3650ee883472cbba177eb9..3d14c41936583039e35913da4a8f5aaa2b3fd565 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -602,7 +602,7 @@ async fn test_handle_start_debugging_reverse_request( }); let child_client = child_session.update(cx, |session, _| session.adapter_client().unwrap()); - client + child_client .on_request::(move |_, _| { Ok(dap::ThreadsResponse { threads: vec![dap::Thread { @@ -645,6 +645,230 @@ async fn test_handle_start_debugging_reverse_request( shutdown_session.await.unwrap(); } +#[gpui::test] +async fn test_shutdown_children_when_parent_session_shutdown( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let dap_store = project.update(cx, |project, _| project.dap_store()); + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) + }); + + let parent_session = task.await.unwrap(); + let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap()); + + client + .on_request::(move |_, _| { + Ok(dap::ThreadsResponse { + threads: vec![dap::Thread { + id: 1, + name: "Thread 1".into(), + }], + }) + }) + .await; + + client.on_response::(move |_| {}).await; + + // start first child session + client + .fake_reverse_request::(StartDebuggingRequestArguments { + configuration: json!({}), + request: StartDebuggingRequestArgumentsRequest::Launch, + }) + .await; + + cx.run_until_parked(); + + // start second child session + client + .fake_reverse_request::(StartDebuggingRequestArguments { + configuration: json!({}), + request: StartDebuggingRequestArgumentsRequest::Launch, + }) + .await; + + cx.run_until_parked(); + + // configure first child session + let first_child_session = dap_store.read_with(cx, |dap_store, _| { + dap_store.session_by_id(SessionId(1)).unwrap() + }); + let first_child_client = + first_child_session.update(cx, |session, _| session.adapter_client().unwrap()); + + first_child_client + .on_request::(move |_, _| Ok(())) + .await; + + // configure second child session + let second_child_session = dap_store.read_with(cx, |dap_store, _| { + dap_store.session_by_id(SessionId(2)).unwrap() + }); + let second_child_client = + second_child_session.update(cx, |session, _| session.adapter_client().unwrap()); + + second_child_client + .on_request::(move |_, _| Ok(())) + .await; + + cx.run_until_parked(); + + // shutdown parent session + dap_store + .update(cx, |dap_store, cx| { + dap_store.shutdown_session(parent_session.read(cx).session_id(), cx) + }) + .await + .unwrap(); + + // assert parent session and all children sessions are shutdown + dap_store.update(cx, |dap_store, cx| { + assert!(dap_store + .session_by_id(parent_session.read(cx).session_id()) + .is_none()); + assert!(dap_store + .session_by_id(first_child_session.read(cx).session_id()) + .is_none()); + assert!(dap_store + .session_by_id(second_child_session.read(cx).session_id()) + .is_none()); + }); +} + +#[gpui::test] +async fn test_shutdown_parent_session_if_all_children_are_shutdown( + executor: BackgroundExecutor, + cx: &mut TestAppContext, +) { + init_test(cx); + + let fs = FakeFs::new(executor.clone()); + + fs.insert_tree( + "/project", + json!({ + "main.rs": "First line\nSecond line\nThird line\nFourth line", + }), + ) + .await; + + let project = Project::test(fs, ["/project".as_ref()], cx).await; + let dap_store = project.update(cx, |project, _| project.dap_store()); + let workspace = init_test_workspace(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + let task = project.update(cx, |project, cx| { + project.start_debug_session(dap::test_config(DebugRequestType::Launch, None, None), cx) + }); + + let parent_session = task.await.unwrap(); + let client = parent_session.update(cx, |session, _| session.adapter_client().unwrap()); + + client.on_response::(move |_| {}).await; + + // start first child session + client + .fake_reverse_request::(StartDebuggingRequestArguments { + configuration: json!({}), + request: StartDebuggingRequestArgumentsRequest::Launch, + }) + .await; + + cx.run_until_parked(); + + // start second child session + client + .fake_reverse_request::(StartDebuggingRequestArguments { + configuration: json!({}), + request: StartDebuggingRequestArgumentsRequest::Launch, + }) + .await; + + cx.run_until_parked(); + + // configure first child session + let first_child_session = dap_store.read_with(cx, |dap_store, _| { + dap_store.session_by_id(SessionId(1)).unwrap() + }); + let first_child_client = + first_child_session.update(cx, |session, _| session.adapter_client().unwrap()); + + first_child_client + .on_request::(move |_, _| Ok(())) + .await; + + // configure second child session + let second_child_session = dap_store.read_with(cx, |dap_store, _| { + dap_store.session_by_id(SessionId(2)).unwrap() + }); + let second_child_client = + second_child_session.update(cx, |session, _| session.adapter_client().unwrap()); + + second_child_client + .on_request::(move |_, _| Ok(())) + .await; + + cx.run_until_parked(); + + // shutdown first child session + dap_store + .update(cx, |dap_store, cx| { + dap_store.shutdown_session(first_child_session.read(cx).session_id(), cx) + }) + .await + .unwrap(); + + // assert parent session and second child session still exist + dap_store.update(cx, |dap_store, cx| { + assert!(dap_store + .session_by_id(parent_session.read(cx).session_id()) + .is_some()); + assert!(dap_store + .session_by_id(first_child_session.read(cx).session_id()) + .is_none()); + assert!(dap_store + .session_by_id(second_child_session.read(cx).session_id()) + .is_some()); + }); + + // shutdown first child session + dap_store + .update(cx, |dap_store, cx| { + dap_store.shutdown_session(second_child_session.read(cx).session_id(), cx) + }) + .await + .unwrap(); + + // assert parent session got shutdown by second child session + // because it was the last child + dap_store.update(cx, |dap_store, cx| { + assert!(dap_store + .session_by_id(parent_session.read(cx).session_id()) + .is_none()); + assert!(dap_store + .session_by_id(second_child_session.read(cx).session_id()) + .is_none()); + }); +} + #[gpui::test] async fn test_debug_panel_item_thread_status_reset_on_failure( executor: BackgroundExecutor, diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index b0c22ba7915509b81d7e021dac348c910d99aa77..f102e6182a275da034e9940e0fe26fdea63e448e 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -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::>(); + + 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 { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index d7c975fb4723da57c44018bb65a82e56f42c0d28..978bf165fb6c352accc890e035c83f149ef18bf1 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -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, parent_id: Option, ignore_breakpoints: bool, modules: Vec, @@ -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 { + 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 { self.parent_id } @@ -1051,6 +1070,7 @@ impl Session { if !self.thread_states.any_stopped_thread() && request.type_id() != TypeId::of::() + || self.is_session_terminated { return; } @@ -1331,6 +1351,10 @@ impl Session { } pub fn shutdown(&mut self, cx: &mut Context) -> Task<()> { + self.is_session_terminated = true; + self.thread_states.exit_all_threads(); + cx.notify(); + let task = if self .capabilities .supports_terminate_request