Skip to content

Commit cd6e9ba

Browse files
committed
dap: add debug adapter implementation
Adds a simple implementation of the debug adapter that supports the very basics of a debug adapter. It supports the launch request, the configuration done request, the creation of threads, stopping, resuming, and disconnecting from server. It does not support custom breakpoints, stack traces, or variable inspection yet. These are planned to be added in the future. It also does not support sending output through the debug adapter yet. Signed-off-by: Jonathan A. Sternberg <[email protected]>
1 parent 52b5d08 commit cd6e9ba

File tree

25 files changed

+3655
-187
lines changed

25 files changed

+3655
-187
lines changed

build/build.go

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -313,9 +313,12 @@ func toRepoOnly(in string) (string, error) {
313313
return strings.Join(out, ","), nil
314314
}
315315

316-
type Handler struct {
317-
Evaluate func(ctx context.Context, c gateway.Client, res *gateway.Result) error
318-
}
316+
type (
317+
EvaluateFunc func(ctx context.Context, name string, c gateway.Client, res *gateway.Result) error
318+
Handler struct {
319+
Evaluate EvaluateFunc
320+
}
321+
)
319322

320323
func Build(ctx context.Context, nodes []builder.Node, opts map[string]Options, docker *dockerutil.Client, cfg *confutil.Config, w progress.Writer) (resp map[string]*client.SolveResponse, err error) {
321324
return BuildWithResultHandler(ctx, nodes, opts, docker, cfg, w, nil)
@@ -510,12 +513,25 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
510513
rKey := resultKey(dp.driverIndex, k)
511514
results.Set(rKey, res)
512515

516+
forceEval := false
513517
if children := childTargets[rKey]; len(children) > 0 {
514-
if err := waitForChildren(ctx, bh, c, res, results, children); err != nil {
518+
// wait for the child targets to register their LLB before evaluating
519+
_, err := results.Get(ctx, children...)
520+
if err != nil {
515521
return nil, err
516522
}
517-
} else if bh != nil && bh.Evaluate != nil {
518-
if err := bh.Evaluate(ctx, c, res); err != nil {
523+
forceEval = true
524+
}
525+
526+
// invoke custom evaluate handler if it is present
527+
if bh != nil && bh.Evaluate != nil {
528+
if err := bh.Evaluate(ctx, k, c, res); err != nil {
529+
return nil, err
530+
}
531+
} else if forceEval {
532+
if err := res.EachRef(func(ref gateway.Reference) error {
533+
return ref.Evaluate(ctx)
534+
}); err != nil {
519535
return nil, err
520536
}
521537
}
@@ -1199,29 +1215,6 @@ func solve(ctx context.Context, c gateway.Client, req gateway.SolveRequest) (*ga
11991215
return res, nil
12001216
}
12011217

1202-
func waitForChildren(ctx context.Context, bh *Handler, c gateway.Client, res *gateway.Result, results *waitmap.Map, children []string) error {
1203-
// wait for the child targets to register their LLB before evaluating
1204-
_, err := results.Get(ctx, children...)
1205-
if err != nil {
1206-
return err
1207-
}
1208-
// we need to wait until the child targets have completed before we can release
1209-
eg, ctx := errgroup.WithContext(ctx)
1210-
eg.Go(func() error {
1211-
if bh != nil && bh.Evaluate != nil {
1212-
return bh.Evaluate(ctx, c, res)
1213-
}
1214-
return res.EachRef(func(ref gateway.Reference) error {
1215-
return ref.Evaluate(ctx)
1216-
})
1217-
})
1218-
eg.Go(func() error {
1219-
_, err := results.Get(ctx, children...)
1220-
return err
1221-
})
1222-
return eg.Wait()
1223-
}
1224-
12251218
func catchFrontendError(retErr, frontendErr *error) {
12261219
*frontendErr = *retErr
12271220
if errors.Is(*retErr, ErrRestart) {

build/invoke.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,18 @@ import (
1414
)
1515

1616
type InvokeConfig struct {
17-
Entrypoint []string
18-
Cmd []string
19-
NoCmd bool
20-
Env []string
21-
User string
22-
NoUser bool
23-
Cwd string
24-
NoCwd bool
25-
Tty bool
26-
Rollback bool
27-
Initial bool
28-
SuspendOn SuspendOn
17+
Entrypoint []string `json:"entrypoint,omitempty"`
18+
Cmd []string `json:"cmd,omitempty"`
19+
NoCmd bool `json:"noCmd,omitempty"`
20+
Env []string `json:"env,omitempty"`
21+
User string `json:"user,omitempty"`
22+
NoUser bool `json:"noUser,omitempty"`
23+
Cwd string `json:"cwd,omitempty"`
24+
NoCwd bool `json:"noCwd,omitempty"`
25+
Tty bool `json:"tty,omitempty"`
26+
Rollback bool `json:"rollback,omitempty"`
27+
Initial bool `json:"initial,omitempty"`
28+
SuspendOn SuspendOn `json:"suspendOn,omitempty"`
2929
}
3030

3131
func (cfg *InvokeConfig) NeedsDebug(err error) bool {
@@ -43,6 +43,18 @@ func (s SuspendOn) DebugEnabled(err error) bool {
4343
return err != nil || s == SuspendAlways
4444
}
4545

46+
func (s *SuspendOn) UnmarshalText(text []byte) error {
47+
switch string(text) {
48+
case "error":
49+
*s = SuspendError
50+
case "always":
51+
*s = SuspendAlways
52+
default:
53+
return errors.Errorf("unknown suspend name: %s", string(text))
54+
}
55+
return nil
56+
}
57+
4658
type Container struct {
4759
cancelOnce sync.Once
4860
containerCancel func(error)

commands/build.go

Lines changed: 22 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"github.com/docker/buildx/build"
2222
"github.com/docker/buildx/builder"
2323
"github.com/docker/buildx/commands/debug"
24-
"github.com/docker/buildx/monitor"
2524
"github.com/docker/buildx/store"
2625
"github.com/docker/buildx/store/storeutil"
2726
"github.com/docker/buildx/util/buildflags"
@@ -56,7 +55,6 @@ import (
5655
"github.com/sirupsen/logrus"
5756
"github.com/spf13/cobra"
5857
"github.com/spf13/pflag"
59-
"github.com/tonistiigi/go-csvvalue"
6058
"go.opentelemetry.io/otel/attribute"
6159
"go.opentelemetry.io/otel/metric"
6260
"google.golang.org/grpc/codes"
@@ -102,7 +100,7 @@ type buildOptions struct {
102100
exportPush bool
103101
exportLoad bool
104102

105-
invokeConfig *invokeConfig
103+
debugger debug.Debugger
106104
}
107105

108106
func (o *buildOptions) toOptions() (*BuildOptions, error) {
@@ -408,25 +406,25 @@ func getImageID(resp map[string]string) string {
408406
}
409407

410408
func runBuildWithOptions(ctx context.Context, dockerCli command.Cli, opts *BuildOptions, options buildOptions, printer *progress.Printer) (_ *client.SolveResponse, _ *build.Inputs, retErr error) {
411-
if options.invokeConfig != nil && (options.dockerfileName == "-" || options.contextPath == "-") {
412-
// stdin must be usable for monitor
413-
return nil, nil, errors.Errorf("Dockerfile or context from stdin is not supported with invoke")
414-
}
409+
var bh build.Handler
410+
if options.debugger != nil {
411+
if options.dockerfileName == "-" || options.contextPath == "-" {
412+
// stdin must be usable for debugger
413+
return nil, nil, errors.Errorf("Dockerfile or context from stdin is not supported with debugger")
414+
}
415415

416-
var (
417-
in io.ReadCloser
418-
m *monitor.Monitor
419-
bh build.Handler
420-
)
421-
if options.invokeConfig == nil {
422-
in = dockerCli.In()
423-
} else {
424-
m = monitor.New(&options.invokeConfig.InvokeConfig, dockerCli.In(), os.Stdout, os.Stderr, printer)
425-
defer m.Close()
416+
dbg, err := options.debugger.Start(dockerCli, printer)
417+
if err != nil {
418+
return nil, nil, err
419+
}
420+
defer dbg.Stop()
421+
422+
bh = dbg.Handler()
426423

427-
bh = m.Handler()
424+
dockerCli.SetIn(nil)
428425
}
429426

427+
in := dockerCli.In()
430428
for {
431429
resp, inputs, err := RunBuild(ctx, dockerCli, opts, in, printer, &bh)
432430
if err != nil {
@@ -450,13 +448,15 @@ type debuggableBuild struct {
450448
rootOpts *rootOptions
451449
}
452450

453-
func (b *debuggableBuild) NewDebugger(cfg *debug.DebugConfig) *cobra.Command {
454-
return buildCmd(b.dockerCli, b.rootOpts, cfg)
451+
func (b *debuggableBuild) WithDebugger(debugger debug.Debugger) *cobra.Command {
452+
return buildCmd(b.dockerCli, b.rootOpts, debugger)
455453
}
456454

457-
func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugConfig *debug.DebugConfig) *cobra.Command {
455+
func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debug.Debugger) *cobra.Command {
458456
cFlags := &commonFlags{}
459-
options := &buildOptions{}
457+
options := &buildOptions{
458+
debugger: debugger,
459+
}
460460

461461
cmd := &cobra.Command{
462462
Use: "build [OPTIONS] PATH | URL | -",
@@ -481,14 +481,6 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugConfig *debug.D
481481
options.progress = cFlags.progress
482482
cmd.Flags().VisitAll(checkWarnedFlags)
483483

484-
if debugConfig != nil && (debugConfig.InvokeFlag != "" || debugConfig.OnFlag != "") {
485-
iConfig := new(invokeConfig)
486-
if err := iConfig.parseInvokeConfig(debugConfig.InvokeFlag, debugConfig.OnFlag); err != nil {
487-
return err
488-
}
489-
options.invokeConfig = iConfig
490-
}
491-
492484
return runBuild(cmd.Context(), dockerCli, *options)
493485
},
494486
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -880,96 +872,6 @@ func printValue(w io.Writer, printer callFunc, version string, format string, re
880872
return printer([]byte(res["result.json"]), w)
881873
}
882874

883-
type invokeConfig struct {
884-
build.InvokeConfig
885-
invokeFlag string
886-
}
887-
888-
func (cfg *invokeConfig) parseInvokeConfig(invoke, on string) error {
889-
switch on {
890-
case "always":
891-
cfg.SuspendOn = build.SuspendAlways
892-
case "error":
893-
cfg.SuspendOn = build.SuspendError
894-
default:
895-
if invoke != "" {
896-
cfg.SuspendOn = build.SuspendAlways
897-
}
898-
}
899-
900-
cfg.invokeFlag = invoke
901-
cfg.Tty = true
902-
cfg.NoCmd = true
903-
switch invoke {
904-
case "default", "":
905-
return nil
906-
case "on-error":
907-
// NOTE: we overwrite the command to run because the original one should fail on the failed step.
908-
// TODO: make this configurable via flags or restorable from LLB.
909-
// Discussion: https://github.com/docker/buildx/pull/1640#discussion_r1113295900
910-
cfg.Cmd = []string{"/bin/sh"}
911-
cfg.NoCmd = false
912-
return nil
913-
}
914-
915-
csvParser := csvvalue.NewParser()
916-
csvParser.LazyQuotes = true
917-
fields, err := csvParser.Fields(invoke, nil)
918-
if err != nil {
919-
return err
920-
}
921-
if len(fields) == 1 && !strings.Contains(fields[0], "=") {
922-
cfg.Cmd = []string{fields[0]}
923-
cfg.NoCmd = false
924-
return nil
925-
}
926-
cfg.NoUser = true
927-
cfg.NoCwd = true
928-
for _, field := range fields {
929-
parts := strings.SplitN(field, "=", 2)
930-
if len(parts) != 2 {
931-
return errors.Errorf("invalid value %s", field)
932-
}
933-
key := strings.ToLower(parts[0])
934-
value := parts[1]
935-
switch key {
936-
case "args":
937-
cfg.Cmd = append(cfg.Cmd, maybeJSONArray(value)...)
938-
cfg.NoCmd = false
939-
case "entrypoint":
940-
cfg.Entrypoint = append(cfg.Entrypoint, maybeJSONArray(value)...)
941-
if cfg.Cmd == nil {
942-
cfg.Cmd = []string{}
943-
cfg.NoCmd = false
944-
}
945-
case "env":
946-
cfg.Env = append(cfg.Env, maybeJSONArray(value)...)
947-
case "user":
948-
cfg.User = value
949-
cfg.NoUser = false
950-
case "cwd":
951-
cfg.Cwd = value
952-
cfg.NoCwd = false
953-
case "tty":
954-
cfg.Tty, err = strconv.ParseBool(value)
955-
if err != nil {
956-
return errors.Errorf("failed to parse tty: %v", err)
957-
}
958-
default:
959-
return errors.Errorf("unknown key %q", key)
960-
}
961-
}
962-
return nil
963-
}
964-
965-
func maybeJSONArray(v string) []string {
966-
var list []string
967-
if err := json.Unmarshal([]byte(v), &list); err == nil {
968-
return list
969-
}
970-
return []string{v}
971-
}
972-
973875
func callAlias(target *string, value string) cobrautil.BoolFuncValue {
974876
return func(s string) error {
975877
v, err := strconv.ParseBool(s)

commands/debug/dap.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package debug
2+
3+
import (
4+
"context"
5+
"io"
6+
7+
"github.com/docker/buildx/dap"
8+
"github.com/docker/buildx/util/progress"
9+
"github.com/docker/cli/cli/command"
10+
"github.com/pkg/errors"
11+
)
12+
13+
type AdapterProtocolDebugger struct{}
14+
15+
func (d *AdapterProtocolDebugger) Start(dockerCli command.Cli, printer *progress.Printer) (DebuggerInstance, error) {
16+
conn := dap.IoConn(readWriter{
17+
Reader: dockerCli.In(),
18+
Writer: dockerCli.Out(),
19+
})
20+
21+
adapter := dap.New()
22+
if err := adapter.Start(context.Background(), conn); err != nil {
23+
return nil, errors.Wrap(err, "debug adapter did not start")
24+
}
25+
return adapter, nil
26+
}
27+
28+
type readWriter struct {
29+
io.Reader
30+
io.Writer
31+
}

0 commit comments

Comments
 (0)