dap: support stopOnEntry to configure behavior when starting the debugger

This will configure the default behavior when beginning to evaluate a
build target. When `stopOnEntry` is used, it will default to `stepNext`.
Otherwise, `stepContinue` will be used.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2025-07-07 09:39:23 -05:00
parent 0c747263ef
commit a291698eaf
7 changed files with 76 additions and 67 deletions
+4 -15
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/docker/buildx/dap" "github.com/docker/buildx/dap"
"github.com/docker/buildx/dap/common"
"github.com/docker/buildx/util/cobrautil" "github.com/docker/buildx/util/cobrautil"
"github.com/docker/buildx/util/ioset" "github.com/docker/buildx/util/ioset"
"github.com/docker/buildx/util/progress" "github.com/docker/buildx/util/progress"
@@ -20,31 +21,18 @@ func dapCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
} }
cobrautil.MarkCommandExperimental(cmd) cobrautil.MarkCommandExperimental(cmd)
flags := cmd.Flags()
flags.StringVar(&options.OnFlag, "on", "error", "When to pause the adapter ([always, error])")
cobrautil.MarkFlagsExperimental(flags, "on")
dapBuildCmd := buildCmd(dockerCli, rootOpts, &options) dapBuildCmd := buildCmd(dockerCli, rootOpts, &options)
dapBuildCmd.Args = cobra.RangeArgs(0, 1) dapBuildCmd.Args = cobra.RangeArgs(0, 1)
cmd.AddCommand(dapBuildCmd) cmd.AddCommand(dapBuildCmd)
return cmd return cmd
} }
type dapOptions struct { type dapOptions struct{}
// OnFlag is a flag to configure the timing of launching the debugger.
OnFlag string
}
func (d *dapOptions) New(in ioset.In) (debuggerInstance, error) { func (d *dapOptions) New(in ioset.In) (debuggerInstance, error) {
invokeConfig, err := parseInvokeConfig("", d.OnFlag)
if err != nil {
return nil, err
}
conn := dap.NewConn(in.Stdin, in.Stdout) conn := dap.NewConn(in.Stdin, in.Stdout)
return &adapterProtocolDebugger{ return &adapterProtocolDebugger{
Adapter: dap.New[LaunchConfig](invokeConfig), Adapter: dap.New[LaunchConfig](),
conn: conn, conn: conn,
}, nil }, nil
} }
@@ -53,6 +41,7 @@ type LaunchConfig struct {
Dockerfile string `json:"dockerfile,omitempty"` Dockerfile string `json:"dockerfile,omitempty"`
ContextPath string `json:"contextPath,omitempty"` ContextPath string `json:"contextPath,omitempty"`
Target string `json:"target,omitempty"` Target string `json:"target,omitempty"`
common.Config
} }
type adapterProtocolDebugger struct { type adapterProtocolDebugger struct {
+41 -42
View File
@@ -11,19 +11,20 @@ import (
"sync/atomic" "sync/atomic"
"github.com/docker/buildx/build" "github.com/docker/buildx/build"
"github.com/docker/buildx/dap/common"
"github.com/google/go-dap" "github.com/google/go-dap"
gateway "github.com/moby/buildkit/frontend/gateway/client" gateway "github.com/moby/buildkit/frontend/gateway/client"
"github.com/pkg/errors" "github.com/pkg/errors"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
type Adapter[T any] struct { type Adapter[C LaunchConfig] struct {
srv *Server srv *Server
eg *errgroup.Group eg *errgroup.Group
cfg build.InvokeConfig cfg common.Config
initialized chan struct{} initialized chan struct{}
started chan launchResponse[T] started chan launchResponse[C]
configuration chan struct{} configuration chan struct{}
evaluateReqCh chan *evaluateRequest evaluateReqCh chan *evaluateRequest
@@ -36,24 +37,21 @@ type Adapter[T any] struct {
idPool *idPool idPool *idPool
} }
func New[T any](cfg *build.InvokeConfig) *Adapter[T] { func New[C LaunchConfig]() *Adapter[C] {
d := &Adapter[T]{ d := &Adapter[C]{
initialized: make(chan struct{}), initialized: make(chan struct{}),
started: make(chan launchResponse[T], 1), started: make(chan launchResponse[C], 1),
configuration: make(chan struct{}), configuration: make(chan struct{}),
evaluateReqCh: make(chan *evaluateRequest), evaluateReqCh: make(chan *evaluateRequest),
threads: make(map[int]*thread), threads: make(map[int]*thread),
nextThreadID: 1, nextThreadID: 1,
idPool: new(idPool), idPool: new(idPool),
} }
if cfg != nil {
d.cfg = *cfg
}
d.srv = NewServer(d.dapHandler()) d.srv = NewServer(d.dapHandler())
return d return d
} }
func (d *Adapter[T]) Start(ctx context.Context, conn Conn) (T, error) { func (d *Adapter[C]) Start(ctx context.Context, conn Conn) (C, error) {
d.eg, _ = errgroup.WithContext(ctx) d.eg, _ = errgroup.WithContext(ctx)
d.eg.Go(func() error { d.eg.Go(func() error {
return d.srv.Serve(ctx, conn) return d.srv.Serve(ctx, conn)
@@ -65,10 +63,11 @@ func (d *Adapter[T]) Start(ctx context.Context, conn Conn) (T, error) {
if !ok { if !ok {
resp.Error = context.Canceled resp.Error = context.Canceled
} }
d.cfg = resp.Config.GetConfig()
return resp.Config, resp.Error return resp.Config, resp.Error
} }
func (d *Adapter[T]) Stop() error { func (d *Adapter[C]) Stop() error {
if d.eg == nil { if d.eg == nil {
return nil return nil
} }
@@ -96,7 +95,7 @@ func (d *Adapter[T]) Stop() error {
return err return err
} }
func (d *Adapter[T]) Initialize(c Context, req *dap.InitializeRequest, resp *dap.InitializeResponse) error { func (d *Adapter[C]) Initialize(c Context, req *dap.InitializeRequest, resp *dap.InitializeResponse) error {
close(d.initialized) close(d.initialized)
// Set capabilities. // Set capabilities.
@@ -104,36 +103,36 @@ func (d *Adapter[T]) Initialize(c Context, req *dap.InitializeRequest, resp *dap
return nil return nil
} }
type launchResponse[T any] struct { type launchResponse[C any] struct {
Config T Config C
Error error Error error
} }
func (d *Adapter[T]) Launch(c Context, req *dap.LaunchRequest, resp *dap.LaunchResponse) error { func (d *Adapter[C]) Launch(c Context, req *dap.LaunchRequest, resp *dap.LaunchResponse) error {
defer close(d.started) defer close(d.started)
var cfg T var cfg C
if err := json.Unmarshal(req.Arguments, &cfg); err != nil { if err := json.Unmarshal(req.Arguments, &cfg); err != nil {
d.started <- launchResponse[T]{Error: err} d.started <- launchResponse[C]{Error: err}
return err return err
} }
d.start(c) d.start(c)
d.started <- launchResponse[T]{Config: cfg} d.started <- launchResponse[C]{Config: cfg}
return nil return nil
} }
func (d *Adapter[T]) Disconnect(c Context, req *dap.DisconnectRequest, resp *dap.DisconnectResponse) error { func (d *Adapter[C]) Disconnect(c Context, req *dap.DisconnectRequest, resp *dap.DisconnectResponse) error {
close(d.evaluateReqCh) close(d.evaluateReqCh)
return nil return nil
} }
func (d *Adapter[T]) start(c Context) { func (d *Adapter[C]) start(c Context) {
c.Go(d.launch) c.Go(d.launch)
} }
func (d *Adapter[T]) Continue(c Context, req *dap.ContinueRequest, resp *dap.ContinueResponse) error { func (d *Adapter[C]) Continue(c Context, req *dap.ContinueRequest, resp *dap.ContinueResponse) error {
d.threadsMu.RLock() d.threadsMu.RLock()
t := d.threads[req.Arguments.ThreadId] t := d.threads[req.Arguments.ThreadId]
d.threadsMu.RUnlock() d.threadsMu.RUnlock()
@@ -142,7 +141,7 @@ func (d *Adapter[T]) Continue(c Context, req *dap.ContinueRequest, resp *dap.Con
return nil return nil
} }
func (d *Adapter[T]) Next(c Context, req *dap.NextRequest, resp *dap.NextResponse) error { func (d *Adapter[C]) Next(c Context, req *dap.NextRequest, resp *dap.NextResponse) error {
d.threadsMu.RLock() d.threadsMu.RLock()
t := d.threads[req.Arguments.ThreadId] t := d.threads[req.Arguments.ThreadId]
d.threadsMu.RUnlock() d.threadsMu.RUnlock()
@@ -151,7 +150,7 @@ func (d *Adapter[T]) Next(c Context, req *dap.NextRequest, resp *dap.NextRespons
return nil return nil
} }
func (d *Adapter[T]) SetBreakpoints(c Context, req *dap.SetBreakpointsRequest, resp *dap.SetBreakpointsResponse) error { func (d *Adapter[C]) SetBreakpoints(c Context, req *dap.SetBreakpointsRequest, resp *dap.SetBreakpointsResponse) error {
// TODO: implement breakpoints // TODO: implement breakpoints
for range req.Arguments.Breakpoints { for range req.Arguments.Breakpoints {
// Fail to create all breakpoints that were requested. // Fail to create all breakpoints that were requested.
@@ -163,13 +162,13 @@ func (d *Adapter[T]) SetBreakpoints(c Context, req *dap.SetBreakpointsRequest, r
return nil return nil
} }
func (d *Adapter[T]) ConfigurationDone(c Context, req *dap.ConfigurationDoneRequest, resp *dap.ConfigurationDoneResponse) error { func (d *Adapter[C]) ConfigurationDone(c Context, req *dap.ConfigurationDoneRequest, resp *dap.ConfigurationDoneResponse) error {
d.configuration <- struct{}{} d.configuration <- struct{}{}
close(d.configuration) close(d.configuration)
return nil return nil
} }
func (d *Adapter[T]) launch(c Context) { func (d *Adapter[C]) launch(c Context) {
// Send initialized event. // Send initialized event.
c.C() <- &dap.InitializedEvent{ c.C() <- &dap.InitializedEvent{
Event: dap.Event{ Event: dap.Event{
@@ -198,7 +197,7 @@ func (d *Adapter[T]) launch(c Context) {
started := c.Go(func(c Context) { started := c.Go(func(c Context) {
defer d.deleteThread(c, t) defer d.deleteThread(c, t)
defer close(req.errCh) defer close(req.errCh)
req.errCh <- t.Evaluate(c, req.c, req.ref, req.meta, req.inputs) req.errCh <- t.Evaluate(c, req.c, req.ref, req.meta, req.inputs, d.cfg)
}) })
if !started { if !started {
@@ -209,7 +208,7 @@ func (d *Adapter[T]) launch(c Context) {
} }
} }
func (d *Adapter[T]) newThread(ctx Context, name string) (t *thread) { func (d *Adapter[C]) newThread(ctx Context, name string) (t *thread) {
d.threadsMu.Lock() d.threadsMu.Lock()
id := d.nextThreadID id := d.nextThreadID
t = &thread{ t = &thread{
@@ -232,14 +231,14 @@ func (d *Adapter[T]) newThread(ctx Context, name string) (t *thread) {
return t return t
} }
func (d *Adapter[T]) getThread(id int) (t *thread) { func (d *Adapter[C]) getThread(id int) (t *thread) {
d.threadsMu.Lock() d.threadsMu.Lock()
t = d.threads[id] t = d.threads[id]
d.threadsMu.Unlock() d.threadsMu.Unlock()
return t return t
} }
func (d *Adapter[T]) deleteThread(ctx Context, t *thread) { func (d *Adapter[C]) deleteThread(ctx Context, t *thread) {
d.threadsMu.Lock() d.threadsMu.Lock()
delete(d.threads, t.id) delete(d.threads, t.id)
d.threadsMu.Unlock() d.threadsMu.Unlock()
@@ -262,7 +261,7 @@ type evaluateRequest struct {
errCh chan<- error errCh chan<- error
} }
func (d *Adapter[T]) EvaluateResult(ctx context.Context, name string, c gateway.Client, res *gateway.Result, inputs build.Inputs) error { func (d *Adapter[C]) EvaluateResult(ctx context.Context, name string, c gateway.Client, res *gateway.Result, inputs build.Inputs) error {
eg, _ := errgroup.WithContext(ctx) eg, _ := errgroup.WithContext(ctx)
if res.Ref != nil { if res.Ref != nil {
eg.Go(func() error { eg.Go(func() error {
@@ -279,7 +278,7 @@ func (d *Adapter[T]) EvaluateResult(ctx context.Context, name string, c gateway.
return eg.Wait() return eg.Wait()
} }
func (d *Adapter[T]) evaluateRef(ctx context.Context, name string, c gateway.Client, ref gateway.Reference, meta map[string][]byte, inputs build.Inputs) error { func (d *Adapter[C]) evaluateRef(ctx context.Context, name string, c gateway.Client, ref gateway.Reference, meta map[string][]byte, inputs build.Inputs) error {
errCh := make(chan error, 1) errCh := make(chan error, 1)
// Send a solve request to the launch routine // Send a solve request to the launch routine
@@ -307,7 +306,7 @@ func (d *Adapter[T]) evaluateRef(ctx context.Context, name string, c gateway.Cli
} }
} }
func (d *Adapter[T]) Threads(c Context, req *dap.ThreadsRequest, resp *dap.ThreadsResponse) error { func (d *Adapter[C]) Threads(c Context, req *dap.ThreadsRequest, resp *dap.ThreadsResponse) error {
d.threadsMu.RLock() d.threadsMu.RLock()
defer d.threadsMu.RUnlock() defer d.threadsMu.RUnlock()
@@ -321,7 +320,7 @@ func (d *Adapter[T]) Threads(c Context, req *dap.ThreadsRequest, resp *dap.Threa
return nil return nil
} }
func (d *Adapter[T]) StackTrace(c Context, req *dap.StackTraceRequest, resp *dap.StackTraceResponse) error { func (d *Adapter[C]) StackTrace(c Context, req *dap.StackTraceRequest, resp *dap.StackTraceResponse) error {
t := d.getThread(req.Arguments.ThreadId) t := d.getThread(req.Arguments.ThreadId)
if t == nil { if t == nil {
return errors.Errorf("no such thread: %d", req.Arguments.ThreadId) return errors.Errorf("no such thread: %d", req.Arguments.ThreadId)
@@ -331,7 +330,7 @@ func (d *Adapter[T]) StackTrace(c Context, req *dap.StackTraceRequest, resp *dap
return nil return nil
} }
func (d *Adapter[T]) Source(c Context, req *dap.SourceRequest, resp *dap.SourceResponse) error { func (d *Adapter[C]) Source(c Context, req *dap.SourceRequest, resp *dap.SourceResponse) error {
fname := req.Arguments.Source.Path fname := req.Arguments.Source.Path
dt, ok := d.sourceMap.Get(fname) dt, ok := d.sourceMap.Get(fname)
@@ -343,7 +342,7 @@ func (d *Adapter[T]) Source(c Context, req *dap.SourceRequest, resp *dap.SourceR
return nil return nil
} }
func (d *Adapter[T]) evaluate(ctx context.Context, name string, c gateway.Client, res *gateway.Result, opt build.Options) error { func (d *Adapter[C]) evaluate(ctx context.Context, name string, c gateway.Client, res *gateway.Result, opt build.Options) error {
errCh := make(chan error, 1) errCh := make(chan error, 1)
started := d.srv.Go(func(ctx Context) { started := d.srv.Go(func(ctx Context) {
@@ -362,13 +361,13 @@ func (d *Adapter[T]) evaluate(ctx context.Context, name string, c gateway.Client
} }
} }
func (d *Adapter[T]) Handler() build.Handler { func (d *Adapter[C]) Handler() build.Handler {
return build.Handler{ return build.Handler{
Evaluate: d.evaluate, Evaluate: d.evaluate,
} }
} }
func (d *Adapter[T]) dapHandler() Handler { func (d *Adapter[C]) dapHandler() Handler {
return Handler{ return Handler{
Initialize: d.Initialize, Initialize: d.Initialize,
Launch: d.Launch, Launch: d.Launch,
@@ -383,15 +382,15 @@ func (d *Adapter[T]) dapHandler() Handler {
} }
} }
func (d *Adapter[T]) Out() io.Writer { func (d *Adapter[C]) Out() io.Writer {
return &adapterWriter[T]{d} return &adapterWriter[C]{d}
} }
type adapterWriter[T any] struct { type adapterWriter[C LaunchConfig] struct {
*Adapter[T] *Adapter[C]
} }
func (d *adapterWriter[T]) Write(p []byte) (n int, err error) { func (d *adapterWriter[C]) Write(p []byte) (n int, err error) {
started := d.srv.Go(func(c Context) { started := d.srv.Go(func(c Context) {
<-d.initialized <-d.initialized
+4 -3
View File
@@ -7,13 +7,14 @@ import (
"testing" "testing"
"time" "time"
"github.com/docker/buildx/dap/common"
"github.com/google/go-dap" "github.com/google/go-dap"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
func TestLaunch(t *testing.T) { func TestLaunch(t *testing.T) {
adapter, conn, client := NewTestAdapter[any](t) adapter, conn, client := NewTestAdapter[common.Config](t)
ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, context.DeadlineExceeded) ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, context.DeadlineExceeded)
defer cancel() defer cancel()
@@ -68,7 +69,7 @@ func TestLaunch(t *testing.T) {
eg.Wait() eg.Wait()
} }
func NewTestAdapter[T any](t *testing.T) (*Adapter[T], Conn, *Client) { func NewTestAdapter[C LaunchConfig](t *testing.T) (*Adapter[C], Conn, *Client) {
t.Helper() t.Helper()
rd1, wr1 := io.Pipe() rd1, wr1 := io.Pipe()
@@ -84,7 +85,7 @@ func NewTestAdapter[T any](t *testing.T) (*Adapter[T], Conn, *Client) {
clientConn.Close() clientConn.Close()
}) })
adapter := New[T](nil) adapter := New[C]()
t.Cleanup(func() { adapter.Stop() }) t.Cleanup(func() { adapter.Stop() })
client := NewClient(clientConn) client := NewClient(clientConn)
+9
View File
@@ -0,0 +1,9 @@
package common
type Config struct {
StopOnEntry bool `json:"stopOnEntry,omitempty"`
}
func (c Config) GetConfig() Config {
return c
}
+7
View File
@@ -0,0 +1,7 @@
package dap
import "github.com/docker/buildx/dap/common"
type LaunchConfig interface {
GetConfig() common.Config
}
+7 -2
View File
@@ -7,6 +7,7 @@ import (
"sync" "sync"
"github.com/docker/buildx/build" "github.com/docker/buildx/build"
"github.com/docker/buildx/dap/common"
"github.com/google/go-dap" "github.com/google/go-dap"
"github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/client/llb"
gateway "github.com/moby/buildkit/frontend/gateway/client" gateway "github.com/moby/buildkit/frontend/gateway/client"
@@ -67,13 +68,17 @@ const (
stepNext stepNext
) )
func (t *thread) Evaluate(ctx Context, c gateway.Client, ref gateway.Reference, meta map[string][]byte, inputs build.Inputs) error { func (t *thread) Evaluate(ctx Context, c gateway.Client, ref gateway.Reference, meta map[string][]byte, inputs build.Inputs, cfg common.Config) error {
if err := t.init(ctx, c, ref, meta, inputs); err != nil { if err := t.init(ctx, c, ref, meta, inputs); err != nil {
return err return err
} }
defer t.reset() defer t.reset()
step := stepNext step := stepContinue
if cfg.StopOnEntry {
step = stepNext
}
for { for {
pos, err := t.seekNext(ctx, step) pos, err := t.seekNext(ctx, step)
+1 -2
View File
@@ -13,10 +13,9 @@ Start debug adapter protocol compatible debugger (EXPERIMENTAL)
### Options ### Options
| Name | Type | Default | Description | | Name | Type | Default | Description |
|:----------------|:---------|:--------|:-----------------------------------------------------------| |:----------------|:---------|:--------|:-----------------------------------------|
| `--builder` | `string` | | Override the configured builder instance | | `--builder` | `string` | | Override the configured builder instance |
| `-D`, `--debug` | `bool` | | Enable debug logging | | `-D`, `--debug` | `bool` | | Enable debug logging |
| `--on` | `string` | `error` | When to pause the adapter ([always, error]) (EXPERIMENTAL) |
<!---MARKER_GEN_END--> <!---MARKER_GEN_END-->