Merge pull request #3735 from jsternberg/dap-exit-code

dap: pass exit code through exited event
This commit is contained in:
Jonathan A. Sternberg
2026-03-23 10:42:57 -05:00
committed by GitHub
6 changed files with 106 additions and 20 deletions
+10 -5
View File
@@ -398,10 +398,6 @@ func runBuild(ctx context.Context, dockerCli command.Cli, debugOpts debuggerOpti
done := timeBuildCommand(mp, attributes)
resp, inputs, retErr := runBuildWithOptions(ctx, dockerCli, opts, dbg, printer)
if err := printer.Wait(); retErr == nil {
retErr = err
}
done(retErr)
if retErr != nil {
return retErr
@@ -462,12 +458,21 @@ func runBuildWithOptions(ctx context.Context, dockerCli command.Cli, opts *Build
if err := dbg.Start(printer, opts); err != nil {
return nil, nil, err
}
defer dbg.Stop()
defer func() { dbg.Stop(retErr) }()
bh = dbg.Handler()
dockerCli.SetIn(nil)
}
// Ensure messages sent to the printer are flushed before the debugger completes.
// This prevents late messages from not being sent because the connection was
// terminated before completion of the debugger.
defer func() {
if err := printer.Wait(); retErr == nil {
retErr = err
}
}()
in := dockerCli.In()
for {
resp, inputs, err := RunBuild(ctx, dockerCli, opts, in, printer, &bh)
+2 -2
View File
@@ -91,9 +91,9 @@ func (d *adapterProtocolDebugger) Start(printer *progress.Printer, opts *BuildOp
return nil
}
func (d *adapterProtocolDebugger) Stop() error {
func (d *adapterProtocolDebugger) Stop(retErr error) error {
defer d.conn.Close()
return d.Adapter.Stop()
return d.Adapter.Stop(retErr)
}
func dapAttachCmd() *cobra.Command {
+2 -2
View File
@@ -41,7 +41,7 @@ type debuggerOptions interface {
type debuggerInstance interface {
Start(printer *progress.Printer, opts *BuildOptions) error
Handler() build.Handler
Stop() error
Stop(retErr error) error
Out() io.Writer
}
@@ -98,7 +98,7 @@ func (d *monitorDebuggerInstance) Handler() build.Handler {
return d.m.Handler()
}
func (d *monitorDebuggerInstance) Stop() error {
func (d *monitorDebuggerInstance) Stop(_ error) error {
return d.m.Close()
}
+22 -10
View File
@@ -83,7 +83,7 @@ func (d *Adapter[C]) Start(conn Conn) (C, error) {
return resp.Config, resp.Error
}
func (d *Adapter[C]) Stop() error {
func (d *Adapter[C]) Stop(retErr error) error {
if d.eg == nil {
return nil
}
@@ -94,15 +94,27 @@ func (d *Adapter[C]) Stop() error {
Event: "terminated",
},
}
// TODO: detect exit code from threads
// c.C() <- &dap.ExitedEvent{
// Event: dap.Event{
// Event: "exited",
// },
// Body: dap.ExitedEventBody{
// ExitCode: exitCode,
// },
// }
// Send an exit code based on the returned error.
// Any error results in sending an exit code of 1 while
// no error sends zero for success.
//
// The exited event is sent after the terminated event.
// See the specification overview diagram on the bottom of the page
// for a detailed flowchart.
// https://microsoft.github.io/debug-adapter-protocol/overview
exitCode := 0
if retErr != nil {
exitCode = 1
}
c.C() <- &dap.ExitedEvent{
Event: dap.Event{
Event: "exited",
},
Body: dap.ExitedEventBody{
ExitCode: exitCode,
},
}
})
d.srv.Stop()
+1 -1
View File
@@ -253,7 +253,7 @@ func NewTestAdapter[C LaunchConfig](t *testing.T) (*Adapter[C], Conn, *daptest.C
client := daptest.NewClient(clientConn)
t.Cleanup(func() { client.Close() })
t.Cleanup(func() { adapter.Stop() })
t.Cleanup(func() { adapter.Stop(nil) })
return adapter, srvConn, client
}
+69
View File
@@ -5,6 +5,7 @@ import (
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"slices"
"strings"
@@ -87,6 +88,7 @@ var dapBuildTests = []func(t *testing.T, sb integration.Sandbox){
testDapBuildStepOut,
testDapBuildVariables,
testDapBuildDeferredEval,
testDapBuildExitedEvent,
}
func testDapBuild(t *testing.T, sb integration.Sandbox) {
@@ -914,6 +916,73 @@ func testDapBuildDeferredEval(t *testing.T, sb integration.Sandbox) {
require.ErrorAs(t, done(true), &exitErr)
}
func testDapBuildExitedEvent(t *testing.T, sb integration.Sandbox) {
t.Run("success", func(t *testing.T) {
dir := createTestProject(t)
client, done, err := dapBuildCmd(t, sb)
require.NoError(t, err)
ch := make(chan *dap.ExitedEvent, 1)
client.RegisterEvent("exited", func(em dap.EventMessage) {
ch <- em.(*dap.ExitedEvent)
close(ch)
})
// Project should just build normally.
doLaunch(t, client, commands.LaunchConfig{
Dockerfile: path.Join(dir, "Dockerfile"),
ContextPath: dir,
})
select {
case exited := <-ch:
require.Equal(t, 0, exited.Body.ExitCode)
case <-time.After(5 * time.Second):
require.Fail(t, "timeout reached")
}
require.NoError(t, done(true))
})
t.Run("failure", func(t *testing.T) {
dir := createTestProject(t)
client, done, err := dapBuildCmd(t, sb)
require.NoError(t, err)
ch := make(chan *dap.ExitedEvent, 1)
client.RegisterEvent("exited", func(em dap.EventMessage) {
ch <- em.(*dap.ExitedEvent)
close(ch)
})
// Delete foo from the test project so this will fail.
err = os.Remove(filepath.Join(dir, "foo"))
require.NoError(t, err)
interruptCh := pollInterruptEvents(client)
doLaunch(t, client, commands.LaunchConfig{
Dockerfile: path.Join(dir, "Dockerfile"),
ContextPath: dir,
})
// We will hit an interrupt because of the failure.
stopped := waitForInterrupt[*dap.StoppedEvent](t, interruptCh)
require.Equal(t, "exception", stopped.Body.Reason)
// Continue execution which should trigger the exited event.
doNext(t, client, stopped.Body.ThreadId)
select {
case exited := <-ch:
require.NotEqual(t, 0, exited.Body.ExitCode)
case <-time.After(time.Second):
require.Fail(t, "timeout reached")
}
var exitErr *exec.ExitError
require.ErrorAs(t, done(false), &exitErr)
})
}
func doLaunch(t *testing.T, client *daptest.Client, config commands.LaunchConfig, bps ...dap.SourceBreakpoint) {
t.Helper()