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. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
+57
-129
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/containerd/console"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/builder"
|
||||
"github.com/docker/buildx/monitor"
|
||||
"github.com/docker/buildx/store"
|
||||
"github.com/docker/buildx/store/storeutil"
|
||||
"github.com/docker/buildx/util/buildflags"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/desktop"
|
||||
"github.com/docker/buildx/util/dockerutil"
|
||||
"github.com/docker/buildx/util/ioset"
|
||||
"github.com/docker/buildx/util/metricutil"
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/docker/buildx/util/platformutil"
|
||||
@@ -55,7 +55,6 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"google.golang.org/grpc/codes"
|
||||
@@ -100,8 +99,6 @@ type buildOptions struct {
|
||||
pull bool
|
||||
exportPush bool
|
||||
exportLoad bool
|
||||
|
||||
invokeConfig *invokeConfig
|
||||
}
|
||||
|
||||
func (o *buildOptions) toOptions() (*BuildOptions, error) {
|
||||
@@ -276,7 +273,7 @@ func (o *buildOptionsHash) String() string {
|
||||
return o.result
|
||||
}
|
||||
|
||||
func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) (err error) {
|
||||
func runBuild(ctx context.Context, dockerCli command.Cli, debugOpts debuggerOptions, options buildOptions) (err error) {
|
||||
mp := dockerCli.MeterProvider()
|
||||
|
||||
ctx, end, err := tracing.TraceCurrentCommand(ctx, []string{"build", options.contextPath},
|
||||
@@ -320,10 +317,6 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
}
|
||||
driverType := b.Driver
|
||||
|
||||
var term bool
|
||||
if _, err := console.ConsoleFromFile(os.Stderr); err == nil {
|
||||
term = true
|
||||
}
|
||||
attributes := buildMetricAttributes(dockerCli, driverType, &options)
|
||||
|
||||
ctx2, cancel := context.WithCancelCause(context.TODO())
|
||||
@@ -332,8 +325,38 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
out io.Writer = os.Stderr
|
||||
dbg debuggerInstance
|
||||
)
|
||||
|
||||
if debugOpts != nil {
|
||||
if options.dockerfileName == "-" || options.contextPath == "-" {
|
||||
// stdin must be usable for debugger
|
||||
return errors.Errorf("Dockerfile or context from stdin is not supported with debugger")
|
||||
}
|
||||
|
||||
dbg, err = debugOpts.New(ioset.In{
|
||||
Stdin: io.NopCloser(dockerCli.In()),
|
||||
Stdout: nopCloser{dockerCli.Out()},
|
||||
Stderr: nopCloser{dockerCli.Err()},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out = dbg.Out()
|
||||
}
|
||||
|
||||
var term bool
|
||||
if c, ok := out.(console.File); ok {
|
||||
if _, err := console.ConsoleFromFile(c); err == nil {
|
||||
term = true
|
||||
}
|
||||
}
|
||||
|
||||
var printer *progress.Printer
|
||||
printer, err = progress.NewPrinter(ctx2, os.Stderr, progressMode,
|
||||
printer, err = progress.NewPrinter(ctx2, out, progressMode,
|
||||
progress.WithDesc(
|
||||
fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver),
|
||||
fmt.Sprintf("%s:%s", b.Driver, b.Name),
|
||||
@@ -348,7 +371,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
||||
}
|
||||
|
||||
done := timeBuildCommand(mp, attributes)
|
||||
resp, inputs, retErr := runBuildWithOptions(ctx, dockerCli, opts, options, printer)
|
||||
resp, inputs, retErr := runBuildWithOptions(ctx, dockerCli, opts, dbg, printer)
|
||||
|
||||
if err := printer.Wait(); retErr == nil {
|
||||
retErr = err
|
||||
@@ -406,26 +429,19 @@ func getImageID(resp map[string]string) string {
|
||||
return dgst
|
||||
}
|
||||
|
||||
func runBuildWithOptions(ctx context.Context, dockerCli command.Cli, opts *BuildOptions, options buildOptions, printer *progress.Printer) (_ *client.SolveResponse, _ *build.Inputs, retErr error) {
|
||||
if options.invokeConfig != nil && (options.dockerfileName == "-" || options.contextPath == "-") {
|
||||
// stdin must be usable for monitor
|
||||
return nil, nil, errors.Errorf("Dockerfile or context from stdin is not supported with invoke")
|
||||
}
|
||||
func runBuildWithOptions(ctx context.Context, dockerCli command.Cli, opts *BuildOptions, dbg debuggerInstance, printer *progress.Printer) (_ *client.SolveResponse, _ *build.Inputs, retErr error) {
|
||||
var bh build.Handler
|
||||
if dbg != nil {
|
||||
if err := dbg.Start(printer, opts); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer dbg.Stop()
|
||||
|
||||
var (
|
||||
in io.ReadCloser
|
||||
m *monitor.Monitor
|
||||
bh build.Handler
|
||||
)
|
||||
if options.invokeConfig == nil {
|
||||
in = dockerCli.In()
|
||||
} else {
|
||||
m = monitor.New(&options.invokeConfig.InvokeConfig, dockerCli.In(), os.Stdout, os.Stderr, printer)
|
||||
defer m.Close()
|
||||
|
||||
bh = m.Handler()
|
||||
bh = dbg.Handler()
|
||||
dockerCli.SetIn(nil)
|
||||
}
|
||||
|
||||
in := dockerCli.In()
|
||||
for {
|
||||
resp, inputs, err := RunBuild(ctx, dockerCli, opts, in, printer, &bh)
|
||||
if err != nil {
|
||||
@@ -440,7 +456,7 @@ func runBuildWithOptions(ctx context.Context, dockerCli command.Cli, opts *Build
|
||||
}
|
||||
}
|
||||
|
||||
func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugConfig *debugOptions) *cobra.Command {
|
||||
func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugger debuggerOptions) *cobra.Command {
|
||||
cFlags := &commonFlags{}
|
||||
options := &buildOptions{}
|
||||
|
||||
@@ -453,7 +469,9 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugConfig *debugOp
|
||||
"aliases": "docker build, docker builder build, docker image build, docker buildx b",
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
options.contextPath = args[0]
|
||||
if len(args) > 0 {
|
||||
options.contextPath = args[0]
|
||||
}
|
||||
options.builder = rootOpts.builder
|
||||
options.metadataFile = cFlags.metadataFile
|
||||
options.noCache = false
|
||||
@@ -467,15 +485,7 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugConfig *debugOp
|
||||
options.progress = cFlags.progress
|
||||
cmd.Flags().VisitAll(checkWarnedFlags)
|
||||
|
||||
if debugConfig != nil && (debugConfig.InvokeFlag != "" || debugConfig.OnFlag != "") {
|
||||
iConfig := new(invokeConfig)
|
||||
if err := iConfig.parseInvokeConfig(debugConfig.InvokeFlag, debugConfig.OnFlag); err != nil {
|
||||
return err
|
||||
}
|
||||
options.invokeConfig = iConfig
|
||||
}
|
||||
|
||||
return runBuild(cmd.Context(), dockerCli, *options)
|
||||
return runBuild(cmd.Context(), dockerCli, debugger, *options)
|
||||
},
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return nil, cobra.ShellCompDirectiveFilterDirs
|
||||
@@ -866,96 +876,6 @@ func printValue(w io.Writer, printer callFunc, version string, format string, re
|
||||
return printer([]byte(res["result.json"]), w)
|
||||
}
|
||||
|
||||
type invokeConfig struct {
|
||||
build.InvokeConfig
|
||||
invokeFlag string
|
||||
}
|
||||
|
||||
func (cfg *invokeConfig) parseInvokeConfig(invoke, on string) error {
|
||||
switch on {
|
||||
case "always":
|
||||
cfg.SuspendOn = build.SuspendAlways
|
||||
case "error":
|
||||
cfg.SuspendOn = build.SuspendError
|
||||
default:
|
||||
if invoke != "" {
|
||||
cfg.SuspendOn = build.SuspendAlways
|
||||
}
|
||||
}
|
||||
|
||||
cfg.invokeFlag = invoke
|
||||
cfg.Tty = true
|
||||
cfg.NoCmd = true
|
||||
switch invoke {
|
||||
case "default", "":
|
||||
return nil
|
||||
case "on-error":
|
||||
// NOTE: we overwrite the command to run because the original one should fail on the failed step.
|
||||
// TODO: make this configurable via flags or restorable from LLB.
|
||||
// Discussion: https://github.com/docker/buildx/pull/1640#discussion_r1113295900
|
||||
cfg.Cmd = []string{"/bin/sh"}
|
||||
cfg.NoCmd = false
|
||||
return nil
|
||||
}
|
||||
|
||||
csvParser := csvvalue.NewParser()
|
||||
csvParser.LazyQuotes = true
|
||||
fields, err := csvParser.Fields(invoke, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fields) == 1 && !strings.Contains(fields[0], "=") {
|
||||
cfg.Cmd = []string{fields[0]}
|
||||
cfg.NoCmd = false
|
||||
return nil
|
||||
}
|
||||
cfg.NoUser = true
|
||||
cfg.NoCwd = true
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key := strings.ToLower(parts[0])
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "args":
|
||||
cfg.Cmd = append(cfg.Cmd, maybeJSONArray(value)...)
|
||||
cfg.NoCmd = false
|
||||
case "entrypoint":
|
||||
cfg.Entrypoint = append(cfg.Entrypoint, maybeJSONArray(value)...)
|
||||
if cfg.Cmd == nil {
|
||||
cfg.Cmd = []string{}
|
||||
cfg.NoCmd = false
|
||||
}
|
||||
case "env":
|
||||
cfg.Env = append(cfg.Env, maybeJSONArray(value)...)
|
||||
case "user":
|
||||
cfg.User = value
|
||||
cfg.NoUser = false
|
||||
case "cwd":
|
||||
cfg.Cwd = value
|
||||
cfg.NoCwd = false
|
||||
case "tty":
|
||||
cfg.Tty, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return errors.Errorf("failed to parse tty: %v", err)
|
||||
}
|
||||
default:
|
||||
return errors.Errorf("unknown key %q", key)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func maybeJSONArray(v string) []string {
|
||||
var list []string
|
||||
if err := json.Unmarshal([]byte(v), &list); err == nil {
|
||||
return list
|
||||
}
|
||||
return []string{v}
|
||||
}
|
||||
|
||||
func callAlias(target *string, value string) cobrautil.BoolFuncValue {
|
||||
return func(s string) error {
|
||||
v, err := strconv.ParseBool(s)
|
||||
@@ -1210,3 +1130,11 @@ func RunBuild(ctx context.Context, dockerCli command.Cli, in *BuildOptions, inSt
|
||||
}
|
||||
return resp[defaultTargetName], inputs, nil
|
||||
}
|
||||
|
||||
type nopCloser struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
func (nopCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/buildx/dap"
|
||||
"github.com/docker/buildx/util/cobrautil"
|
||||
"github.com/docker/buildx/util/ioset"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func dapCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
||||
var options dapOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "dap",
|
||||
Short: "Start debug adapter protocol compatible debugger",
|
||||
}
|
||||
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.Args = cobra.RangeArgs(0, 1)
|
||||
cmd.AddCommand(dapBuildCmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
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) {
|
||||
invokeConfig, err := parseInvokeConfig("", d.OnFlag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conn := dap.NewConn(in.Stdin, in.Stdout)
|
||||
return &adapterProtocolDebugger{
|
||||
Adapter: dap.New[LaunchConfig](invokeConfig),
|
||||
conn: conn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type LaunchConfig struct {
|
||||
Dockerfile string `json:"dockerfile,omitempty"`
|
||||
ContextPath string `json:"contextPath,omitempty"`
|
||||
Target string `json:"target,omitempty"`
|
||||
}
|
||||
|
||||
type adapterProtocolDebugger struct {
|
||||
*dap.Adapter[LaunchConfig]
|
||||
conn dap.Conn
|
||||
}
|
||||
|
||||
func (d *adapterProtocolDebugger) Start(printer *progress.Printer, opts *BuildOptions) error {
|
||||
cfg, err := d.Adapter.Start(context.Background(), d.conn)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "debug adapter did not start")
|
||||
}
|
||||
|
||||
if cfg.Dockerfile != "" {
|
||||
opts.DockerfileName = cfg.Dockerfile
|
||||
}
|
||||
if cfg.ContextPath != "" {
|
||||
opts.ContextPath = cfg.ContextPath
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *adapterProtocolDebugger) Stop() error {
|
||||
defer d.conn.Close()
|
||||
return d.Adapter.Stop()
|
||||
}
|
||||
+145
-1
@@ -1,9 +1,21 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/monitor"
|
||||
"github.com/docker/buildx/util/cobrautil"
|
||||
"github.com/docker/buildx/util/ioset"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/tonistiigi/go-csvvalue"
|
||||
)
|
||||
|
||||
type debugOptions struct {
|
||||
@@ -14,9 +26,21 @@ type debugOptions struct {
|
||||
OnFlag string
|
||||
}
|
||||
|
||||
// debuggerOptions will start a debuggerOptions instance.
|
||||
type debuggerOptions interface {
|
||||
New(in ioset.In) (debuggerInstance, error)
|
||||
}
|
||||
|
||||
// debuggerInstance is an instance of a Debugger that has been started.
|
||||
type debuggerInstance interface {
|
||||
Start(printer *progress.Printer, opts *BuildOptions) error
|
||||
Handler() build.Handler
|
||||
Stop() error
|
||||
Out() io.Writer
|
||||
}
|
||||
|
||||
func debugCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
||||
var options debugOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "debug",
|
||||
Short: "Start debugger",
|
||||
@@ -32,3 +56,123 @@ func debugCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
||||
cmd.AddCommand(buildCmd(dockerCli, rootOpts, &options))
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (d *debugOptions) New(in ioset.In) (debuggerInstance, error) {
|
||||
cfg, err := parseInvokeConfig(d.InvokeFlag, d.OnFlag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &monitorDebuggerInstance{
|
||||
cfg: cfg,
|
||||
in: in.Stdin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type monitorDebuggerInstance struct {
|
||||
cfg *build.InvokeConfig
|
||||
in io.ReadCloser
|
||||
m *monitor.Monitor
|
||||
}
|
||||
|
||||
func (d *monitorDebuggerInstance) Start(printer *progress.Printer, opts *BuildOptions) error {
|
||||
d.m = monitor.New(d.cfg, d.in, os.Stdout, os.Stderr, printer)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *monitorDebuggerInstance) Handler() build.Handler {
|
||||
return d.m.Handler()
|
||||
}
|
||||
|
||||
func (d *monitorDebuggerInstance) Stop() error {
|
||||
return d.m.Close()
|
||||
}
|
||||
|
||||
func (d *monitorDebuggerInstance) Out() io.Writer {
|
||||
return os.Stderr
|
||||
}
|
||||
|
||||
func parseInvokeConfig(invoke, on string) (*build.InvokeConfig, error) {
|
||||
cfg := &build.InvokeConfig{}
|
||||
switch on {
|
||||
case "always":
|
||||
cfg.SuspendOn = build.SuspendAlways
|
||||
case "error":
|
||||
cfg.SuspendOn = build.SuspendError
|
||||
default:
|
||||
if invoke != "" {
|
||||
cfg.SuspendOn = build.SuspendAlways
|
||||
}
|
||||
}
|
||||
|
||||
cfg.Tty = true
|
||||
cfg.NoCmd = true
|
||||
switch invoke {
|
||||
case "default", "":
|
||||
return cfg, nil
|
||||
case "on-error":
|
||||
// NOTE: we overwrite the command to run because the original one should fail on the failed step.
|
||||
// TODO: make this configurable via flags or restorable from LLB.
|
||||
// Discussion: https://github.com/docker/buildx/pull/1640#discussion_r1113295900
|
||||
cfg.Cmd = []string{"/bin/sh"}
|
||||
cfg.NoCmd = false
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
csvParser := csvvalue.NewParser()
|
||||
csvParser.LazyQuotes = true
|
||||
fields, err := csvParser.Fields(invoke, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(fields) == 1 && !strings.Contains(fields[0], "=") {
|
||||
cfg.Cmd = []string{fields[0]}
|
||||
cfg.NoCmd = false
|
||||
return cfg, nil
|
||||
}
|
||||
cfg.NoUser = true
|
||||
cfg.NoCwd = true
|
||||
for _, field := range fields {
|
||||
parts := strings.SplitN(field, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.Errorf("invalid value %s", field)
|
||||
}
|
||||
key := strings.ToLower(parts[0])
|
||||
value := parts[1]
|
||||
switch key {
|
||||
case "args":
|
||||
cfg.Cmd = append(cfg.Cmd, maybeJSONArray(value)...)
|
||||
cfg.NoCmd = false
|
||||
case "entrypoint":
|
||||
cfg.Entrypoint = append(cfg.Entrypoint, maybeJSONArray(value)...)
|
||||
if cfg.Cmd == nil {
|
||||
cfg.Cmd = []string{}
|
||||
cfg.NoCmd = false
|
||||
}
|
||||
case "env":
|
||||
cfg.Env = append(cfg.Env, maybeJSONArray(value)...)
|
||||
case "user":
|
||||
cfg.User = value
|
||||
cfg.NoUser = false
|
||||
case "cwd":
|
||||
cfg.Cwd = value
|
||||
cfg.NoCwd = false
|
||||
case "tty":
|
||||
cfg.Tty, err = strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("failed to parse tty: %v", err)
|
||||
}
|
||||
default:
|
||||
return nil, errors.Errorf("unknown key %q", key)
|
||||
}
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func maybeJSONArray(v string) []string {
|
||||
var list []string
|
||||
if err := json.Unmarshal([]byte(v), &list); err == nil {
|
||||
return list
|
||||
}
|
||||
return []string{v}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,7 @@ func addCommands(cmd *cobra.Command, opts *rootOptions, dockerCli command.Cli) {
|
||||
)
|
||||
if confutil.IsExperimental() {
|
||||
cmd.AddCommand(debugCmd(dockerCli, opts))
|
||||
cmd.AddCommand(dapCmd(dockerCli, opts))
|
||||
}
|
||||
|
||||
cmd.RegisterFlagCompletionFunc( //nolint:errcheck
|
||||
|
||||
Reference in New Issue
Block a user