fix: graceful shutdown of debug adapter on stop#16
Conversation
Instead of immediately SIGKILL-ing the adapter process, send DisconnectRequest(terminateDebuggee=true) and wait up to 3s for it to exit cleanly. Only hard-kill if it doesn't exit in time. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Review Summary by QodoGraceful shutdown of debug adapter on stop
WalkthroughsDescription• Implement graceful shutdown of debug adapter with 3-second timeout • Send DisconnectRequest before terminating adapter process • Add two test cases for graceful and forced shutdown scenarios Diagramflowchart LR
A["stopSession called"] --> B["Send DisconnectRequest"]
B --> C["Wait for adapter exit"]
C --> D{"Exit within 3s?"}
D -->|Yes| E["Clean shutdown"]
D -->|No| F["Send SIGKILL"]
F --> G["Wait for process"]
E --> H["Clear adapterCmd"]
G --> H
File Changes1. daemon.go
|
Code Review by Qodo
1. Racy adapterCmd access
|
| // Give the adapter time to shut down gracefully before hard-killing | ||
| done := make(chan struct{}) | ||
| go func() { | ||
| _ = d.adapterCmd.Wait() | ||
| close(done) | ||
| }() | ||
| select { | ||
| case <-done: | ||
| case <-time.After(3 * time.Second): | ||
| _ = d.adapterCmd.Process.Kill() | ||
| <-done |
There was a problem hiding this comment.
1. Racy adaptercmd access 🐞 Bug ⛯ Reliability
stopSession launches a goroutine that calls d.adapterCmd.Wait() and later dereferences d.adapterCmd.Process.Kill() without synchronizing access to adapterCmd. Because dispatch explicitly allows stop to run without cmdMu while startAdapter assigns d.adapterCmd concurrently, this can race, wait/kill the wrong process, or panic on nil dereference.
Agent Prompt
## Issue description
`stopSession()` spawns a goroutine that reads `d.adapterCmd` (`Wait()`) and later dereferences `d.adapterCmd.Process` (`Kill()`) without any synchronization, while `startAdapter()` can concurrently assign `d.adapterCmd` and `dispatch()` intentionally allows `stop` to run without `cmdMu`. This creates a data race and can cause waiting/killing the wrong process or panics.
## Issue Context
- `stop`/`pause` bypass `cmdMu`, so `stopSession()` can run concurrently with `handleDebug()` / `startAdapter()`.
- `stopSession()` currently references `d.adapterCmd` from inside a goroutine, which can observe a different value than the one checked.
## Fix Focus Areas
- daemon.go[39-92]
- daemon.go[441-454]
- daemon.go[1261-1284]
- daemon.go[1301-1316]
## Suggested approach
1. Add a dedicated mutex (e.g., `adapterMu sync.Mutex`) to `Daemon` to guard `adapterCmd` (and optionally `adapterAddr`).
2. In `startAdapter()`, lock `adapterMu` when assigning `d.adapterCmd`.
3. In `stopSession()`, lock `adapterMu`, copy `cmd := d.adapterCmd`, set `d.adapterCmd = nil`, unlock, then operate on `cmd` only.
4. In the goroutine, call `cmd.Wait()` (not `d.adapterCmd.Wait()`).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
stopSession bypasses cmdMu, so d.adapterCmd could be reassigned by startAdapter concurrently. Copy to a local before the goroutine, mirroring the existing pattern used for d.client. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AlmogBaku
left a comment
There was a problem hiding this comment.
Fixed in commit 1f04997. We now capture d.adapterCmd into a local cmd before the goroutine and immediately set d.adapterCmd = nil. The goroutine then operates only on the local cmd, preventing concurrent races with startAdapter(). This mirrors the existing pattern used for d.client at line 1263-1265.
Summary
dap stop, wait up to 3s for the debug adapter to exit gracefully afterDisconnectRequest(terminateDebuggee=true)before resorting to SIGKILLTest plan
TestStopSessionGracefulShutdown— verifies fast-exiting processes aren't unnecessarily delayedTestStopSessionKillsHangingProcess— verifies hanging processes get killed after 3s timeoutmake all)🤖 Generated with Claude Code