Merge pull request #3735 from jsternberg/dap-exit-code
dap: pass exit code through exited event
This commit is contained in:
+10
-5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user