diff --git a/daemon.go b/daemon.go index 8f69faa..d743b99 100644 --- a/daemon.go +++ b/daemon.go @@ -1267,10 +1267,21 @@ func (d *Daemon) stopSession() { _ = c.DisconnectRequest(true) c.Close() } - if d.adapterCmd != nil && d.adapterCmd.Process != nil { - _ = d.adapterCmd.Process.Kill() - _ = d.adapterCmd.Wait() - d.adapterCmd = nil + cmd := d.adapterCmd + d.adapterCmd = nil + if cmd != nil && cmd.Process != nil { + // Give the adapter time to shut down gracefully before hard-killing + done := make(chan struct{}) + go func() { + _ = cmd.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(3 * time.Second): + _ = cmd.Process.Kill() + <-done + } } if d.cleanupFn != nil { d.cleanupFn() diff --git a/daemon_test.go b/daemon_test.go index aa080a9..a349289 100644 --- a/daemon_test.go +++ b/daemon_test.go @@ -3,8 +3,10 @@ package dap import ( "encoding/json" "fmt" + "os/exec" "strings" "testing" + "time" ) func TestOutputBufferBoundedAtWrite(t *testing.T) { @@ -87,6 +89,50 @@ func TestTempBinaryCleanup_NilSafe(t *testing.T) { d.stopSession() // should not panic } +func TestStopSessionGracefulShutdown(t *testing.T) { + // Process that exits quickly on its own — should not need SIGKILL + cmd := exec.Command("sleep", "0") + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + d := &Daemon{adapterCmd: cmd} + + start := time.Now() + d.stopSession() + elapsed := time.Since(start) + + if elapsed > 2*time.Second { + t.Errorf("graceful shutdown took too long: %v (expected < 2s)", elapsed) + } + if d.adapterCmd != nil { + t.Error("adapterCmd should be nil after stopSession") + } +} + +func TestStopSessionKillsHangingProcess(t *testing.T) { + // Process that ignores signals and hangs — should be killed after timeout + cmd := exec.Command("sleep", "60") + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + d := &Daemon{adapterCmd: cmd} + + start := time.Now() + d.stopSession() + elapsed := time.Since(start) + + // Should take ~3s (the timeout), not 60s + if elapsed > 5*time.Second { + t.Errorf("expected kill after ~3s, took %v", elapsed) + } + if elapsed < 2*time.Second { + t.Errorf("expected to wait for timeout, but finished in %v", elapsed) + } + if d.adapterCmd != nil { + t.Error("adapterCmd should be nil after stopSession") + } +} + func TestParseBreakpointSpec(t *testing.T) { tests := []struct { name string