debugger: Fix restart only working once per session (#51247)

`Session::restart_task` is set to `Some` when a restart is initiated but
never cleared back to `None`. The guard at the top of `restart()` checks
`self.restart_task.is_some()` and returns early, so only the first
restart attempt succeeds.

This primarily affects debug adapters that advertise
`supportsRestartRequest` dynamically via a `CapabilitiesEvent` after
launch, such as the Flutter debug adapter.

Related: https://github.com/zed-extensions/dart/issues/45

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [ ] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
(N/A — no UI changes)

Release Notes:

- debugger: Fixed debug session restart only working once when the
adapter supports DAP restart requests.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Anthony Eid <anthony@zed.dev>
This commit is contained in:
Nelson Campos 2026-03-12 10:05:52 -03:00 committed by GitHub
parent e0881e38f9
commit 314b7e55fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 97 additions and 13 deletions

View file

@ -132,7 +132,13 @@ pub fn start_debug_session_with<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
.workspace()
.read(cx)
.panel::<DebugPanel>(cx)
.and_then(|panel| panel.read(cx).active_session())
.and_then(|panel| {
panel
.read(cx)
.sessions_with_children
.keys()
.max_by_key(|session| session.read(cx).session_id(cx))
})
.map(|session| session.read(cx).running_state().read(cx).session())
.cloned()
.context("Failed to get active session")

View file

@ -27,7 +27,7 @@ use std::{
path::Path,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
atomic::{AtomicBool, AtomicUsize, Ordering},
},
};
use terminal_view::terminal_panel::TerminalPanel;
@ -2481,3 +2481,75 @@ async fn test_adapter_shutdown_with_child_sessions_on_app_quit(
"Child session should have received disconnect request"
);
}
#[gpui::test]
async fn test_restart_request_is_not_sent_more_than_once_until_response(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(executor.clone());
fs.insert_tree(
path!("/project"),
json!({
"main.rs": "First line\nSecond line\nThird line\nFourth line",
}),
)
.await;
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let session = start_debug_session(&workspace, cx, move |client| {
client.on_request::<dap::requests::Initialize, _>(move |_, _| {
Ok(dap::Capabilities {
supports_restart_request: Some(true),
..Default::default()
})
});
})
.unwrap();
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
let restart_count = Arc::new(AtomicUsize::new(0));
client.on_request::<dap::requests::Restart, _>({
let restart_count = restart_count.clone();
move |_, _| {
restart_count.fetch_add(1, Ordering::SeqCst);
Ok(())
}
});
// This works because the restart request sender is on the foreground thread
// so it will start running after the gpui update stack is cleared
session.update(cx, |session, cx| {
session.restart(None, cx);
session.restart(None, cx);
session.restart(None, cx);
});
cx.run_until_parked();
assert_eq!(
restart_count.load(Ordering::SeqCst),
1,
"Only one restart request should be sent while a restart is in-flight"
);
session.update(cx, |session, cx| {
session.restart(None, cx);
});
cx.run_until_parked();
assert_eq!(
restart_count.load(Ordering::SeqCst),
2,
"A second restart should be allowed after the first one completes"
);
}

View file

@ -2187,21 +2187,27 @@ impl Session {
self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated();
self.restart_task = Some(cx.spawn(async move |this, cx| {
let _ = this.update(cx, |session, cx| {
this.update(cx, |session, cx| {
if supports_dap_restart {
session
.request(
RestartCommand {
raw: args.unwrap_or(Value::Null),
},
Self::fallback_to_manual_restart,
cx,
)
.detach();
session.request(
RestartCommand {
raw: args.unwrap_or(Value::Null),
},
Self::fallback_to_manual_restart,
cx,
)
} else {
cx.emit(SessionStateEvent::Restart);
Task::ready(None)
}
});
})
.unwrap_or_else(|_| Task::ready(None))
.await;
this.update(cx, |session, _cx| {
session.restart_task = None;
})
.ok();
}));
}