Merge pull request #3235 from jsternberg/dap-handler
dap: add debug adapter implementation
This commit is contained in:
+22
-29
@@ -313,9 +313,12 @@ func toRepoOnly(in string) (string, error) {
|
||||
return strings.Join(out, ","), nil
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
Evaluate func(ctx context.Context, c gateway.Client, res *gateway.Result) error
|
||||
}
|
||||
type (
|
||||
EvaluateFunc func(ctx context.Context, name string, c gateway.Client, res *gateway.Result) error
|
||||
Handler struct {
|
||||
Evaluate EvaluateFunc
|
||||
}
|
||||
)
|
||||
|
||||
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) {
|
||||
return BuildWithResultHandler(ctx, nodes, opts, docker, cfg, w, nil)
|
||||
@@ -510,12 +513,25 @@ func BuildWithResultHandler(ctx context.Context, nodes []builder.Node, opts map[
|
||||
rKey := resultKey(dp.driverIndex, k)
|
||||
results.Set(rKey, res)
|
||||
|
||||
forceEval := false
|
||||
if children := childTargets[rKey]; len(children) > 0 {
|
||||
if err := waitForChildren(ctx, bh, c, res, results, children); err != nil {
|
||||
// wait for the child targets to register their LLB before evaluating
|
||||
_, err := results.Get(ctx, children...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if bh != nil && bh.Evaluate != nil {
|
||||
if err := bh.Evaluate(ctx, c, res); err != nil {
|
||||
forceEval = true
|
||||
}
|
||||
|
||||
// invoke custom evaluate handler if it is present
|
||||
if bh != nil && bh.Evaluate != nil {
|
||||
if err := bh.Evaluate(ctx, k, c, res); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if forceEval {
|
||||
if err := res.EachRef(func(ref gateway.Reference) error {
|
||||
return ref.Evaluate(ctx)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -1199,29 +1215,6 @@ func solve(ctx context.Context, c gateway.Client, req gateway.SolveRequest) (*ga
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func waitForChildren(ctx context.Context, bh *Handler, c gateway.Client, res *gateway.Result, results *waitmap.Map, children []string) error {
|
||||
// wait for the child targets to register their LLB before evaluating
|
||||
_, err := results.Get(ctx, children...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// we need to wait until the child targets have completed before we can release
|
||||
eg, ctx := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
if bh != nil && bh.Evaluate != nil {
|
||||
return bh.Evaluate(ctx, c, res)
|
||||
}
|
||||
return res.EachRef(func(ref gateway.Reference) error {
|
||||
return ref.Evaluate(ctx)
|
||||
})
|
||||
})
|
||||
eg.Go(func() error {
|
||||
_, err := results.Get(ctx, children...)
|
||||
return err
|
||||
})
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func catchFrontendError(retErr, frontendErr *error) {
|
||||
*frontendErr = *retErr
|
||||
if errors.Is(*retErr, ErrRestart) {
|
||||
|
||||
+24
-12
@@ -14,18 +14,18 @@ import (
|
||||
)
|
||||
|
||||
type InvokeConfig struct {
|
||||
Entrypoint []string
|
||||
Cmd []string
|
||||
NoCmd bool
|
||||
Env []string
|
||||
User string
|
||||
NoUser bool
|
||||
Cwd string
|
||||
NoCwd bool
|
||||
Tty bool
|
||||
Rollback bool
|
||||
Initial bool
|
||||
SuspendOn SuspendOn
|
||||
Entrypoint []string `json:"entrypoint,omitempty"`
|
||||
Cmd []string `json:"cmd,omitempty"`
|
||||
NoCmd bool `json:"noCmd,omitempty"`
|
||||
Env []string `json:"env,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
NoUser bool `json:"noUser,omitempty"`
|
||||
Cwd string `json:"cwd,omitempty"`
|
||||
NoCwd bool `json:"noCwd,omitempty"`
|
||||
Tty bool `json:"tty,omitempty"`
|
||||
Rollback bool `json:"rollback,omitempty"`
|
||||
Initial bool `json:"initial,omitempty"`
|
||||
SuspendOn SuspendOn `json:"suspendOn,omitempty"`
|
||||
}
|
||||
|
||||
func (cfg *InvokeConfig) NeedsDebug(err error) bool {
|
||||
@@ -43,6 +43,18 @@ func (s SuspendOn) DebugEnabled(err error) bool {
|
||||
return err != nil || s == SuspendAlways
|
||||
}
|
||||
|
||||
func (s *SuspendOn) UnmarshalText(text []byte) error {
|
||||
switch string(text) {
|
||||
case "error":
|
||||
*s = SuspendError
|
||||
case "always":
|
||||
*s = SuspendAlways
|
||||
default:
|
||||
return errors.Errorf("unknown suspend name: %s", string(text))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Container struct {
|
||||
cancelOnce sync.Once
|
||||
containerCancel func(error)
|
||||
|
||||
+13
-20
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// NewResultHandle stores a gateway client, gateway result, and the error from
|
||||
// NewResultHandle stores a gateway client, gateway reference, and the error from
|
||||
// an evaluate call if it is present.
|
||||
//
|
||||
// This ResultHandle can be used to execute additional build steps in the same
|
||||
@@ -24,9 +24,10 @@ import (
|
||||
// failures and successes.
|
||||
//
|
||||
// If the returned ResultHandle is not nil, the caller must call Done() on it.
|
||||
func NewResultHandle(ctx context.Context, c gateway.Client, res *gateway.Result, err error) *ResultHandle {
|
||||
func NewResultHandle(ctx context.Context, c gateway.Client, ref gateway.Reference, meta map[string][]byte, err error) *ResultHandle {
|
||||
rCtx := &ResultHandle{
|
||||
res: res,
|
||||
ref: ref,
|
||||
meta: meta,
|
||||
gwClient: c,
|
||||
}
|
||||
if err != nil && !errors.As(err, &rCtx.solveErr) {
|
||||
@@ -37,8 +38,9 @@ func NewResultHandle(ctx context.Context, c gateway.Client, res *gateway.Result,
|
||||
|
||||
// ResultHandle is a build result with the client that built it.
|
||||
type ResultHandle struct {
|
||||
res *gateway.Result
|
||||
ref gateway.Reference
|
||||
solveErr *errdefs.SolveError
|
||||
meta map[string][]byte
|
||||
gwClient gateway.Client
|
||||
|
||||
doneOnce sync.Once
|
||||
@@ -74,9 +76,9 @@ func (r *ResultHandle) NewContainer(ctx context.Context, cfg *InvokeConfig) (gat
|
||||
}
|
||||
|
||||
func (r *ResultHandle) getContainerConfig(cfg *InvokeConfig) (containerCfg gateway.NewContainerRequest, _ error) {
|
||||
if r.res != nil && r.solveErr == nil {
|
||||
if r.ref != nil && r.solveErr == nil {
|
||||
logrus.Debugf("creating container from successful build")
|
||||
ccfg, err := containerConfigFromResult(r.res, cfg)
|
||||
ccfg, err := containerConfigFromResult(r.ref, cfg)
|
||||
if err != nil {
|
||||
return containerCfg, err
|
||||
}
|
||||
@@ -94,9 +96,9 @@ func (r *ResultHandle) getContainerConfig(cfg *InvokeConfig) (containerCfg gatew
|
||||
|
||||
func (r *ResultHandle) getProcessConfig(cfg *InvokeConfig, stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) (_ gateway.StartRequest, err error) {
|
||||
processCfg := newStartRequest(stdin, stdout, stderr)
|
||||
if r.res != nil && r.solveErr == nil {
|
||||
if r.ref != nil && r.solveErr == nil {
|
||||
logrus.Debugf("creating container from successful build")
|
||||
if err := populateProcessConfigFromResult(&processCfg, r.res, cfg); err != nil {
|
||||
if err := populateProcessConfigFromResult(&processCfg, r.meta, cfg); err != nil {
|
||||
return processCfg, err
|
||||
}
|
||||
} else {
|
||||
@@ -108,20 +110,11 @@ func (r *ResultHandle) getProcessConfig(cfg *InvokeConfig, stdin io.ReadCloser,
|
||||
return processCfg, nil
|
||||
}
|
||||
|
||||
func containerConfigFromResult(res *gateway.Result, cfg *InvokeConfig) (*gateway.NewContainerRequest, error) {
|
||||
func containerConfigFromResult(ref gateway.Reference, cfg *InvokeConfig) (*gateway.NewContainerRequest, error) {
|
||||
if cfg.Initial {
|
||||
return nil, errors.Errorf("starting from the container from the initial state of the step is supported only on the failed steps")
|
||||
}
|
||||
|
||||
ps, err := exptypes.ParsePlatforms(res.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ref, ok := res.FindRef(ps.Platforms[0].ID)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("no reference found")
|
||||
}
|
||||
|
||||
return &gateway.NewContainerRequest{
|
||||
Mounts: []gateway.Mount{
|
||||
{
|
||||
@@ -133,8 +126,8 @@ func containerConfigFromResult(res *gateway.Result, cfg *InvokeConfig) (*gateway
|
||||
}, nil
|
||||
}
|
||||
|
||||
func populateProcessConfigFromResult(req *gateway.StartRequest, res *gateway.Result, cfg *InvokeConfig) error {
|
||||
imgData := res.Metadata[exptypes.ExporterImageConfigKey]
|
||||
func populateProcessConfigFromResult(req *gateway.StartRequest, meta map[string][]byte, cfg *InvokeConfig) error {
|
||||
imgData := meta[exptypes.ExporterImageConfigKey]
|
||||
var img *ocispecs.Image
|
||||
if len(imgData) > 0 {
|
||||
img = &ocispecs.Image{}
|
||||
|
||||
+55
-127
@@ -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 {
|
||||
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
|
||||
|
||||
+456
@@ -0,0 +1,456 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/google/go-dap"
|
||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Adapter[T any] struct {
|
||||
srv *Server
|
||||
eg *errgroup.Group
|
||||
cfg build.InvokeConfig
|
||||
|
||||
initialized chan struct{}
|
||||
started chan launchResponse[T]
|
||||
configuration chan struct{}
|
||||
|
||||
evaluateReqCh chan *evaluateRequest
|
||||
|
||||
threads map[int]*thread
|
||||
threadsMu sync.RWMutex
|
||||
nextThreadID int
|
||||
}
|
||||
|
||||
func New[T any](cfg *build.InvokeConfig) *Adapter[T] {
|
||||
d := &Adapter[T]{
|
||||
initialized: make(chan struct{}),
|
||||
started: make(chan launchResponse[T], 1),
|
||||
configuration: make(chan struct{}),
|
||||
evaluateReqCh: make(chan *evaluateRequest),
|
||||
threads: make(map[int]*thread),
|
||||
nextThreadID: 1,
|
||||
}
|
||||
if cfg != nil {
|
||||
d.cfg = *cfg
|
||||
}
|
||||
d.srv = NewServer(d.dapHandler())
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) Start(ctx context.Context, conn Conn) (T, error) {
|
||||
d.eg, _ = errgroup.WithContext(ctx)
|
||||
d.eg.Go(func() error {
|
||||
return d.srv.Serve(ctx, conn)
|
||||
})
|
||||
|
||||
<-d.initialized
|
||||
|
||||
resp, ok := <-d.started
|
||||
if !ok {
|
||||
resp.Error = context.Canceled
|
||||
}
|
||||
return resp.Config, resp.Error
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) Stop() error {
|
||||
if d.eg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
d.srv.Go(func(c Context) {
|
||||
c.C() <- &dap.TerminatedEvent{
|
||||
Event: dap.Event{
|
||||
Event: "terminated",
|
||||
},
|
||||
}
|
||||
// TODO: detect exit code from threads
|
||||
// c.C() <- &dap.ExitedEvent{
|
||||
// Event: dap.Event{
|
||||
// Event: "exited",
|
||||
// },
|
||||
// Body: dap.ExitedEventBody{
|
||||
// ExitCode: exitCode,
|
||||
// },
|
||||
// }
|
||||
})
|
||||
d.srv.Stop()
|
||||
|
||||
err := d.eg.Wait()
|
||||
d.eg = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) Initialize(c Context, req *dap.InitializeRequest, resp *dap.InitializeResponse) error {
|
||||
close(d.initialized)
|
||||
|
||||
// Set capabilities.
|
||||
resp.Body.SupportsConfigurationDoneRequest = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type launchResponse[T any] struct {
|
||||
Config T
|
||||
Error error
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) Launch(c Context, req *dap.LaunchRequest, resp *dap.LaunchResponse) error {
|
||||
defer close(d.started)
|
||||
|
||||
var cfg T
|
||||
if err := json.Unmarshal(req.Arguments, &cfg); err != nil {
|
||||
d.started <- launchResponse[T]{Error: err}
|
||||
return err
|
||||
}
|
||||
|
||||
d.start(c)
|
||||
|
||||
d.started <- launchResponse[T]{Config: cfg}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) Disconnect(c Context, req *dap.DisconnectRequest, resp *dap.DisconnectResponse) error {
|
||||
close(d.evaluateReqCh)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) start(c Context) {
|
||||
c.Go(d.launch)
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) Continue(c Context, req *dap.ContinueRequest, resp *dap.ContinueResponse) error {
|
||||
d.threadsMu.RLock()
|
||||
t := d.threads[req.Arguments.ThreadId]
|
||||
d.threadsMu.RUnlock()
|
||||
|
||||
t.Resume(c)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) SetBreakpoints(c Context, req *dap.SetBreakpointsRequest, resp *dap.SetBreakpointsResponse) error {
|
||||
// TODO: implement breakpoints
|
||||
for range req.Arguments.Breakpoints {
|
||||
// Fail to create all breakpoints that were requested.
|
||||
resp.Body.Breakpoints = append(resp.Body.Breakpoints, dap.Breakpoint{
|
||||
Verified: false,
|
||||
Message: "breakpoints unsupported",
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) ConfigurationDone(c Context, req *dap.ConfigurationDoneRequest, resp *dap.ConfigurationDoneResponse) error {
|
||||
d.configuration <- struct{}{}
|
||||
close(d.configuration)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) launch(c Context) {
|
||||
// Send initialized event.
|
||||
c.C() <- &dap.InitializedEvent{
|
||||
Event: dap.Event{
|
||||
Event: "initialized",
|
||||
},
|
||||
}
|
||||
|
||||
// Wait for configuration.
|
||||
select {
|
||||
case <-c.Done():
|
||||
return
|
||||
case <-d.configuration:
|
||||
// TODO: actual configuration
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.Done():
|
||||
return
|
||||
case req, ok := <-d.evaluateReqCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
t := d.newThread(c, req.name)
|
||||
started := c.Go(func(c Context) {
|
||||
defer d.deleteThread(c, t)
|
||||
defer close(req.errCh)
|
||||
req.errCh <- t.Evaluate(c, req.c, req.ref, req.meta, d.cfg)
|
||||
})
|
||||
|
||||
if !started {
|
||||
req.errCh <- context.Canceled
|
||||
close(req.errCh)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) newThread(ctx Context, name string) (t *thread) {
|
||||
d.threadsMu.Lock()
|
||||
id := d.nextThreadID
|
||||
t = &thread{
|
||||
id: id,
|
||||
name: name,
|
||||
}
|
||||
d.threads[t.id] = t
|
||||
d.nextThreadID++
|
||||
d.threadsMu.Unlock()
|
||||
|
||||
ctx.C() <- &dap.ThreadEvent{
|
||||
Event: dap.Event{Event: "thread"},
|
||||
Body: dap.ThreadEventBody{
|
||||
Reason: "started",
|
||||
ThreadId: t.id,
|
||||
},
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) getThread(id int) (t *thread) {
|
||||
d.threadsMu.Lock()
|
||||
t = d.threads[id]
|
||||
d.threadsMu.Unlock()
|
||||
return t
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) deleteThread(ctx Context, t *thread) {
|
||||
d.threadsMu.Lock()
|
||||
delete(d.threads, t.id)
|
||||
d.threadsMu.Unlock()
|
||||
|
||||
ctx.C() <- &dap.ThreadEvent{
|
||||
Event: dap.Event{Event: "thread"},
|
||||
Body: dap.ThreadEventBody{
|
||||
Reason: "exited",
|
||||
ThreadId: t.id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type evaluateRequest struct {
|
||||
name string
|
||||
c gateway.Client
|
||||
ref gateway.Reference
|
||||
meta map[string][]byte
|
||||
errCh chan<- error
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) EvaluateResult(ctx context.Context, name string, c gateway.Client, res *gateway.Result) error {
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
if res.Ref != nil {
|
||||
eg.Go(func() error {
|
||||
return d.evaluateRef(ctx, name, c, res.Ref, res.Metadata)
|
||||
})
|
||||
}
|
||||
|
||||
for k, ref := range res.Refs {
|
||||
refName := fmt.Sprintf("%s (%s)", name, k)
|
||||
eg.Go(func() error {
|
||||
return d.evaluateRef(ctx, refName, c, ref, res.Metadata)
|
||||
})
|
||||
}
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) evaluateRef(ctx context.Context, name string, c gateway.Client, ref gateway.Reference, meta map[string][]byte) error {
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
// Send a solve request to the launch routine
|
||||
// which will perform the solve in the context of the server.
|
||||
ereq := &evaluateRequest{
|
||||
name: name,
|
||||
c: c,
|
||||
ref: ref,
|
||||
meta: meta,
|
||||
errCh: errCh,
|
||||
}
|
||||
select {
|
||||
case d.evaluateReqCh <- ereq:
|
||||
case <-ctx.Done():
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
|
||||
// Wait for the response.
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) Threads(c Context, req *dap.ThreadsRequest, resp *dap.ThreadsResponse) error {
|
||||
d.threadsMu.RLock()
|
||||
defer d.threadsMu.RUnlock()
|
||||
|
||||
resp.Body.Threads = []dap.Thread{}
|
||||
for _, t := range d.threads {
|
||||
resp.Body.Threads = append(resp.Body.Threads, dap.Thread{
|
||||
Id: t.id,
|
||||
Name: t.name,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) StackTrace(c Context, req *dap.StackTraceRequest, resp *dap.StackTraceResponse) error {
|
||||
t := d.getThread(req.Arguments.ThreadId)
|
||||
if t == nil {
|
||||
return errors.Errorf("no such thread: %d", req.Arguments.ThreadId)
|
||||
}
|
||||
|
||||
resp.Body.StackFrames = t.StackFrames()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) evaluate(ctx context.Context, name string, c gateway.Client, res *gateway.Result) error {
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
started := d.srv.Go(func(ctx Context) {
|
||||
defer close(errCh)
|
||||
errCh <- d.EvaluateResult(ctx, name, c, res)
|
||||
})
|
||||
if !started {
|
||||
return context.Canceled
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
return err
|
||||
case <-ctx.Done():
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) Handler() build.Handler {
|
||||
return build.Handler{
|
||||
Evaluate: d.evaluate,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) dapHandler() Handler {
|
||||
return Handler{
|
||||
Initialize: d.Initialize,
|
||||
Launch: d.Launch,
|
||||
Continue: d.Continue,
|
||||
SetBreakpoints: d.SetBreakpoints,
|
||||
ConfigurationDone: d.ConfigurationDone,
|
||||
Disconnect: d.Disconnect,
|
||||
Threads: d.Threads,
|
||||
StackTrace: d.StackTrace,
|
||||
}
|
||||
}
|
||||
|
||||
type thread struct {
|
||||
id int
|
||||
name string
|
||||
|
||||
paused chan struct{}
|
||||
rCtx *build.ResultHandle
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (t *thread) Evaluate(ctx Context, c gateway.Client, ref gateway.Reference, meta map[string][]byte, cfg build.InvokeConfig) error {
|
||||
err := ref.Evaluate(ctx)
|
||||
if reason, desc := t.needsDebug(cfg, err); reason != "" {
|
||||
rCtx := build.NewResultHandle(ctx, c, ref, meta, err)
|
||||
|
||||
select {
|
||||
case <-t.pause(ctx, rCtx, reason, desc):
|
||||
case <-ctx.Done():
|
||||
t.Resume(ctx)
|
||||
return context.Cause(ctx)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *thread) needsDebug(cfg build.InvokeConfig, err error) (reason, desc string) {
|
||||
if !cfg.NeedsDebug(err) {
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
reason = "exception"
|
||||
desc = "Encountered an error during result evaluation"
|
||||
} else {
|
||||
reason = "pause"
|
||||
desc = "Result evaluation completed"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thread) pause(c Context, rCtx *build.ResultHandle, reason, desc string) <-chan struct{} {
|
||||
if t.paused == nil {
|
||||
t.paused = make(chan struct{})
|
||||
}
|
||||
t.rCtx = rCtx
|
||||
|
||||
c.C() <- &dap.StoppedEvent{
|
||||
Event: dap.Event{Event: "stopped"},
|
||||
Body: dap.StoppedEventBody{
|
||||
Reason: reason,
|
||||
Description: desc,
|
||||
ThreadId: t.id,
|
||||
},
|
||||
}
|
||||
return t.paused
|
||||
}
|
||||
|
||||
func (t *thread) Resume(c Context) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
if t.paused == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if t.rCtx != nil {
|
||||
t.rCtx.Done()
|
||||
t.rCtx = nil
|
||||
}
|
||||
|
||||
close(t.paused)
|
||||
t.paused = nil
|
||||
}
|
||||
|
||||
// TODO: return a suitable stack frame for the thread.
|
||||
// For now, just returns nothing.
|
||||
func (t *thread) StackFrames() []dap.StackFrame {
|
||||
return []dap.StackFrame{}
|
||||
}
|
||||
|
||||
func (d *Adapter[T]) Out() io.Writer {
|
||||
return &adapterWriter[T]{d}
|
||||
}
|
||||
|
||||
type adapterWriter[T any] struct {
|
||||
*Adapter[T]
|
||||
}
|
||||
|
||||
func (d *adapterWriter[T]) Write(p []byte) (n int, err error) {
|
||||
started := d.srv.Go(func(c Context) {
|
||||
<-d.initialized
|
||||
|
||||
c.C() <- &dap.OutputEvent{
|
||||
Event: dap.Event{Event: "output"},
|
||||
Body: dap.OutputEventBody{
|
||||
Category: "stdout",
|
||||
Output: string(p),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
if !started {
|
||||
return 0, io.ErrClosedPipe
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func TestLaunch(t *testing.T) {
|
||||
adapter, conn, client := NewTestAdapter[any](t)
|
||||
|
||||
ctx, cancel := context.WithTimeoutCause(context.Background(), 10*time.Second, context.DeadlineExceeded)
|
||||
defer cancel()
|
||||
|
||||
eg, _ := errgroup.WithContext(ctx)
|
||||
eg.Go(func() error {
|
||||
_, err := adapter.Start(ctx, conn)
|
||||
assert.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
|
||||
var (
|
||||
initialized = make(chan struct{})
|
||||
configurationDone <-chan *dap.ConfigurationDoneResponse
|
||||
)
|
||||
|
||||
client.RegisterEvent("initialized", func(em dap.EventMessage) {
|
||||
// Send configuration done since we don't do any configuration.
|
||||
configurationDone = DoRequest[*dap.ConfigurationDoneResponse](t, client, &dap.ConfigurationDoneRequest{
|
||||
Request: dap.Request{Command: "configurationDone"},
|
||||
})
|
||||
close(initialized)
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
initializeResp := <-DoRequest[*dap.InitializeResponse](t, client, &dap.InitializeRequest{
|
||||
Request: dap.Request{Command: "initialize"},
|
||||
})
|
||||
assert.True(t, initializeResp.Success)
|
||||
assert.True(t, initializeResp.Body.SupportsConfigurationDoneRequest)
|
||||
|
||||
launchResp := <-DoRequest[*dap.LaunchResponse](t, client, &dap.LaunchRequest{
|
||||
Request: dap.Request{Command: "launch"},
|
||||
})
|
||||
assert.True(t, launchResp.Success)
|
||||
|
||||
// We should have received the initialized event.
|
||||
select {
|
||||
case <-initialized:
|
||||
default:
|
||||
assert.Fail(t, "did not receive initialized event")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-configurationDone:
|
||||
case <-time.After(time.Second):
|
||||
assert.Fail(t, "did not receive configurationDone response")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
eg.Wait()
|
||||
}
|
||||
|
||||
func NewTestAdapter[T any](t *testing.T) (*Adapter[T], Conn, *Client) {
|
||||
t.Helper()
|
||||
|
||||
rd1, wr1 := io.Pipe()
|
||||
rd2, wr2 := io.Pipe()
|
||||
|
||||
srvConn := logConn(t, "server", NewConn(rd1, wr2))
|
||||
t.Cleanup(func() {
|
||||
srvConn.Close()
|
||||
})
|
||||
|
||||
clientConn := logConn(t, "client", NewConn(rd2, wr1))
|
||||
t.Cleanup(func() {
|
||||
clientConn.Close()
|
||||
})
|
||||
|
||||
adapter := New[T](nil)
|
||||
t.Cleanup(func() { adapter.Stop() })
|
||||
|
||||
client := NewClient(clientConn)
|
||||
return adapter, srvConn, client
|
||||
}
|
||||
|
||||
func logConn(t *testing.T, prefix string, conn Conn) Conn {
|
||||
return &loggingConn{
|
||||
Conn: conn,
|
||||
t: t,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
type loggingConn struct {
|
||||
Conn
|
||||
t *testing.T
|
||||
prefix string
|
||||
}
|
||||
|
||||
func (c *loggingConn) SendMsg(m dap.Message) error {
|
||||
b, _ := json.Marshal(m)
|
||||
c.t.Logf("[%s] send: %v", c.prefix, string(b))
|
||||
|
||||
err := c.Conn.SendMsg(m)
|
||||
if err != nil {
|
||||
c.t.Logf("[%s] send error: %v", c.prefix, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *loggingConn) RecvMsg(ctx context.Context) (dap.Message, error) {
|
||||
m, err := c.Conn.RecvMsg(ctx)
|
||||
if err != nil {
|
||||
c.t.Logf("[%s] recv error: %v", c.prefix, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(m)
|
||||
c.t.Logf("[%s] recv: %v", c.prefix, string(b))
|
||||
return m, nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
conn Conn
|
||||
|
||||
requests map[int]chan<- dap.ResponseMessage
|
||||
requestsMu sync.Mutex
|
||||
|
||||
events map[string]func(dap.EventMessage)
|
||||
eventsMu sync.RWMutex
|
||||
|
||||
seq atomic.Int64
|
||||
eg *errgroup.Group
|
||||
cancel context.CancelCauseFunc
|
||||
}
|
||||
|
||||
func NewClient(conn Conn) *Client {
|
||||
c := &Client{
|
||||
conn: conn,
|
||||
requests: make(map[int]chan<- dap.ResponseMessage),
|
||||
events: make(map[string]func(dap.EventMessage)),
|
||||
}
|
||||
|
||||
var ctx context.Context
|
||||
ctx, c.cancel = context.WithCancelCause(context.Background())
|
||||
|
||||
c.eg, _ = errgroup.WithContext(context.Background())
|
||||
c.eg.Go(func() error {
|
||||
for {
|
||||
m, err := conn.RecvMsg(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch m := m.(type) {
|
||||
case dap.RequestMessage:
|
||||
// TODO: no reverse requests are currently supported
|
||||
conn.SendMsg(&dap.Response{
|
||||
ProtocolMessage: dap.ProtocolMessage{
|
||||
Seq: c.nextSeq(),
|
||||
Type: "response",
|
||||
},
|
||||
RequestSeq: m.GetRequest().GetSeq(),
|
||||
Success: false,
|
||||
Command: m.GetRequest().Command,
|
||||
Message: "not implemented",
|
||||
})
|
||||
case dap.ResponseMessage:
|
||||
c.requestsMu.Lock()
|
||||
req := m.GetResponse().GetResponse().RequestSeq
|
||||
ch := c.requests[req]
|
||||
delete(c.requests, req)
|
||||
c.requestsMu.Unlock()
|
||||
|
||||
if ch != nil {
|
||||
ch <- m
|
||||
}
|
||||
case dap.EventMessage:
|
||||
c.invokeEventCallback(m)
|
||||
}
|
||||
}
|
||||
})
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) Do(t *testing.T, req dap.RequestMessage) <-chan dap.ResponseMessage {
|
||||
req.GetRequest().Type = "request"
|
||||
req.GetRequest().Seq = c.nextSeq()
|
||||
|
||||
ch := make(chan dap.ResponseMessage, 1)
|
||||
if err := c.conn.SendMsg(req); err != nil {
|
||||
assert.NoError(t, err)
|
||||
close(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
c.requestsMu.Lock()
|
||||
c.requests[req.GetSeq()] = ch
|
||||
c.requestsMu.Unlock()
|
||||
return ch
|
||||
}
|
||||
|
||||
func DoRequest[ResponseMessage dap.ResponseMessage, RequestMessage dap.RequestMessage](t *testing.T, c *Client, req RequestMessage) <-chan ResponseMessage {
|
||||
ch := make(chan ResponseMessage, 1)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
|
||||
if m := <-c.Do(t, req); m != nil {
|
||||
ch <- m.(ResponseMessage)
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *Client) RegisterEvent(event string, fn func(dap.EventMessage)) {
|
||||
c.eventsMu.Lock()
|
||||
defer c.eventsMu.Unlock()
|
||||
|
||||
c.events[event] = fn
|
||||
}
|
||||
|
||||
func (c *Client) invokeEventCallback(event dap.EventMessage) {
|
||||
c.eventsMu.RLock()
|
||||
fn := c.events[event.GetEvent().Event]
|
||||
c.eventsMu.RUnlock()
|
||||
|
||||
if fn != nil {
|
||||
fn(event)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() error {
|
||||
c.cancel(context.Canceled)
|
||||
return c.eg.Wait()
|
||||
}
|
||||
|
||||
func (c *Client) nextSeq() int {
|
||||
seq := c.seq.Add(1)
|
||||
return int(seq)
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type Conn interface {
|
||||
SendMsg(m dap.Message) error
|
||||
RecvMsg(ctx context.Context) (dap.Message, error)
|
||||
io.Closer
|
||||
}
|
||||
|
||||
type conn struct {
|
||||
recvCh <-chan dap.Message
|
||||
sendCh chan<- dap.Message
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelCauseFunc
|
||||
|
||||
eg *errgroup.Group
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
func NewConn(rd io.Reader, wr io.Writer) Conn {
|
||||
recvCh := make(chan dap.Message, 100)
|
||||
sendCh := make(chan dap.Message, 100)
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
// Reader input may never close so this is an orphaned goroutine.
|
||||
// It's ok if it does actually close but not necessary for the
|
||||
// proper functioning of this connection.
|
||||
//
|
||||
// The reason this might not close is because stdin close is controlled
|
||||
// by the OS and can't be closed from within the program.
|
||||
go func() {
|
||||
defer close(errCh)
|
||||
defer close(recvCh)
|
||||
|
||||
rd := bufio.NewReader(rd)
|
||||
for {
|
||||
m, err := dap.ReadProtocolMessage(rd)
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
// TODO: not actually using this yet
|
||||
errCh <- err
|
||||
}
|
||||
return
|
||||
}
|
||||
recvCh <- m
|
||||
}
|
||||
}()
|
||||
|
||||
eg, _ := errgroup.WithContext(context.Background())
|
||||
eg.Go(func() error {
|
||||
for m := range sendCh {
|
||||
if err := dap.WriteProtocolMessage(wr, m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithCancelCause(context.Background())
|
||||
return &conn{
|
||||
recvCh: recvCh,
|
||||
sendCh: sendCh,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
eg: eg,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) SendMsg(m dap.Message) error {
|
||||
select {
|
||||
case c.sendCh <- m:
|
||||
return nil
|
||||
default:
|
||||
return errors.New("send channel full")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) RecvMsg(ctx context.Context) (dap.Message, error) {
|
||||
select {
|
||||
case m, ok := <-c.recvCh:
|
||||
if !ok {
|
||||
return nil, io.EOF
|
||||
}
|
||||
return m, nil
|
||||
case <-ctx.Done():
|
||||
return nil, context.Cause(ctx)
|
||||
case <-c.ctx.Done():
|
||||
return nil, c.ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) Close() error {
|
||||
c.cancel(context.Canceled)
|
||||
c.once.Do(func() {
|
||||
close(c.sendCh)
|
||||
})
|
||||
return c.eg.Wait()
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
context.Context
|
||||
C() chan<- dap.Message
|
||||
Go(f func(c Context)) bool
|
||||
}
|
||||
|
||||
type dispatchContext struct {
|
||||
context.Context
|
||||
srv *Server
|
||||
ch chan<- dap.Message
|
||||
}
|
||||
|
||||
func (c *dispatchContext) C() chan<- dap.Message {
|
||||
return c.ch
|
||||
}
|
||||
|
||||
func (c *dispatchContext) Go(f func(c Context)) bool {
|
||||
return c.srv.Go(f)
|
||||
}
|
||||
|
||||
type HandlerFunc[Req dap.RequestMessage, Resp dap.ResponseMessage] func(c Context, req Req, resp Resp) error
|
||||
|
||||
func (h HandlerFunc[Req, Resp]) Do(c Context, req Req) (resp Resp, err error) {
|
||||
if h == nil {
|
||||
return resp, errors.New("not implemented")
|
||||
}
|
||||
|
||||
respT := reflect.TypeFor[Resp]()
|
||||
rv := reflect.New(respT.Elem())
|
||||
resp = rv.Interface().(Resp)
|
||||
err = h(c, req, resp)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
Initialize HandlerFunc[*dap.InitializeRequest, *dap.InitializeResponse]
|
||||
Launch HandlerFunc[*dap.LaunchRequest, *dap.LaunchResponse]
|
||||
Attach HandlerFunc[*dap.AttachRequest, *dap.AttachResponse]
|
||||
SetBreakpoints HandlerFunc[*dap.SetBreakpointsRequest, *dap.SetBreakpointsResponse]
|
||||
ConfigurationDone HandlerFunc[*dap.ConfigurationDoneRequest, *dap.ConfigurationDoneResponse]
|
||||
Disconnect HandlerFunc[*dap.DisconnectRequest, *dap.DisconnectResponse]
|
||||
Terminate HandlerFunc[*dap.TerminateRequest, *dap.TerminateResponse]
|
||||
Continue HandlerFunc[*dap.ContinueRequest, *dap.ContinueResponse]
|
||||
Restart HandlerFunc[*dap.RestartRequest, *dap.RestartResponse]
|
||||
Threads HandlerFunc[*dap.ThreadsRequest, *dap.ThreadsResponse]
|
||||
StackTrace HandlerFunc[*dap.StackTraceRequest, *dap.StackTraceResponse]
|
||||
Evaluate HandlerFunc[*dap.EvaluateRequest, *dap.EvaluateResponse]
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package dap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/google/go-dap"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var ErrServerStopped = errors.New("dap: server stopped")
|
||||
|
||||
type Server struct {
|
||||
h Handler
|
||||
|
||||
mu sync.RWMutex
|
||||
ch chan dap.Message
|
||||
|
||||
eg *errgroup.Group
|
||||
ctx context.Context
|
||||
cancel context.CancelCauseFunc
|
||||
|
||||
initialized bool
|
||||
}
|
||||
|
||||
func NewServer(h Handler) *Server {
|
||||
return &Server{h: h}
|
||||
}
|
||||
|
||||
func (s *Server) Serve(ctx context.Context, conn Conn) error {
|
||||
writeCh := make(chan dap.Message)
|
||||
s.ch = writeCh
|
||||
|
||||
s.ctx, s.cancel = context.WithCancelCause(ctx)
|
||||
|
||||
// Start an error group to handle server-initiated tasks.
|
||||
s.eg, _ = errgroup.WithContext(s.ctx)
|
||||
s.eg.Go(func() error {
|
||||
<-s.ctx.Done()
|
||||
return s.ctx.Err()
|
||||
})
|
||||
|
||||
eg, _ := errgroup.WithContext(s.ctx)
|
||||
eg.Go(func() error {
|
||||
return s.readLoop(conn)
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
return s.writeLoop(conn, writeCh)
|
||||
})
|
||||
|
||||
eg.Go(func() error {
|
||||
// TODO: reevaluate this logic for shutting down
|
||||
defer close(writeCh)
|
||||
err := s.eg.Wait()
|
||||
|
||||
s.mu.Lock()
|
||||
s.ch = nil
|
||||
s.mu.Unlock()
|
||||
return err
|
||||
})
|
||||
|
||||
return eg.Wait()
|
||||
}
|
||||
|
||||
func (s *Server) readLoop(conn Conn) error {
|
||||
for {
|
||||
m, err := conn.RecvMsg(s.ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch m := m.(type) {
|
||||
case dap.RequestMessage:
|
||||
if ok := s.dispatch(m); !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) dispatch(m dap.RequestMessage) bool {
|
||||
fn := func(c Context) {
|
||||
rmsg, err := s.handleMessage(c, m)
|
||||
if err != nil {
|
||||
rmsg = &dap.Response{}
|
||||
rmsg.GetResponse().Message = err.Error()
|
||||
}
|
||||
rmsg.GetResponse().RequestSeq = m.GetSeq()
|
||||
rmsg.GetResponse().Command = m.GetRequest().Command
|
||||
rmsg.GetResponse().Success = err == nil
|
||||
c.C() <- rmsg
|
||||
}
|
||||
return s.Go(fn)
|
||||
}
|
||||
|
||||
func (s *Server) handleMessage(c Context, m dap.Message) (dap.ResponseMessage, error) {
|
||||
switch req := m.(type) {
|
||||
case *dap.InitializeRequest:
|
||||
resp, err := s.handleInitialize(c, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
case *dap.LaunchRequest:
|
||||
return s.h.Launch.Do(c, req)
|
||||
case *dap.AttachRequest:
|
||||
return s.h.Attach.Do(c, req)
|
||||
case *dap.SetBreakpointsRequest:
|
||||
return s.h.SetBreakpoints.Do(c, req)
|
||||
case *dap.ConfigurationDoneRequest:
|
||||
return s.h.ConfigurationDone.Do(c, req)
|
||||
case *dap.DisconnectRequest:
|
||||
return s.h.Disconnect.Do(c, req)
|
||||
case *dap.TerminateRequest:
|
||||
return s.h.Terminate.Do(c, req)
|
||||
case *dap.ContinueRequest:
|
||||
return s.h.Continue.Do(c, req)
|
||||
case *dap.RestartRequest:
|
||||
return s.h.Restart.Do(c, req)
|
||||
case *dap.ThreadsRequest:
|
||||
return s.h.Threads.Do(c, req)
|
||||
case *dap.StackTraceRequest:
|
||||
return s.h.StackTrace.Do(c, req)
|
||||
case *dap.EvaluateRequest:
|
||||
return s.h.Evaluate.Do(c, req)
|
||||
default:
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleInitialize(c Context, req *dap.InitializeRequest) (*dap.InitializeResponse, error) {
|
||||
if s.initialized {
|
||||
return nil, errors.New("already initialized")
|
||||
}
|
||||
|
||||
resp, err := s.h.Initialize.Do(c, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.initialized = true
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) writeLoop(conn Conn, respCh <-chan dap.Message) error {
|
||||
var seq int
|
||||
for m := range respCh {
|
||||
switch m := m.(type) {
|
||||
case dap.RequestMessage:
|
||||
m.GetRequest().Seq = seq
|
||||
m.GetRequest().Type = "request"
|
||||
case dap.EventMessage:
|
||||
m.GetEvent().Seq = seq
|
||||
m.GetEvent().Type = "event"
|
||||
case dap.ResponseMessage:
|
||||
m.GetResponse().Seq = seq
|
||||
m.GetResponse().Type = "response"
|
||||
}
|
||||
seq++
|
||||
|
||||
if err := conn.SendMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) Go(fn func(c Context)) bool {
|
||||
acquireChannel := func() (chan<- dap.Message, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.ch, s.ch != nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancelCause(s.ctx)
|
||||
c := &dispatchContext{
|
||||
Context: ctx,
|
||||
srv: s,
|
||||
}
|
||||
|
||||
started := make(chan bool, 1)
|
||||
s.eg.Go(func() error {
|
||||
var ok bool
|
||||
c.ch, ok = acquireChannel()
|
||||
started <- ok
|
||||
|
||||
if c.ch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
defer cancel(context.Canceled)
|
||||
fn(c)
|
||||
return nil
|
||||
})
|
||||
return <-started
|
||||
}
|
||||
|
||||
func (s *Server) Stop() {
|
||||
s.mu.Lock()
|
||||
s.ch = nil
|
||||
s.mu.Unlock()
|
||||
s.cancel(ErrServerStopped)
|
||||
}
|
||||
@@ -10,10 +10,11 @@ Extended build capabilities with BuildKit
|
||||
### Subcommands
|
||||
|
||||
| Name | Description |
|
||||
|:-------------------------------------|:------------------------------------------------|
|
||||
|:-------------------------------------|:----------------------------------------------------------------|
|
||||
| [`bake`](buildx_bake.md) | Build from a file |
|
||||
| [`build`](buildx_build.md) | Start a build |
|
||||
| [`create`](buildx_create.md) | Create a new builder instance |
|
||||
| [`dap`](buildx_dap.md) | Start debug adapter protocol compatible debugger (EXPERIMENTAL) |
|
||||
| [`debug`](buildx_debug.md) | Start debugger (EXPERIMENTAL) |
|
||||
| [`dial-stdio`](buildx_dial-stdio.md) | Proxy current stdio streams to builder instance |
|
||||
| [`du`](buildx_du.md) | Disk usage |
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# docker buildx dap
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Start debug adapter protocol compatible debugger (EXPERIMENTAL)
|
||||
|
||||
### Subcommands
|
||||
|
||||
| Name | Description |
|
||||
|:-------------------------------|:--------------|
|
||||
| [`build`](buildx_dap_build.md) | Start a build |
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:----------------|:---------|:--------|:-----------------------------------------------------------|
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
| `--on` | `string` | `error` | When to pause the adapter ([always, error]) (EXPERIMENTAL) |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# docker buildx dap build
|
||||
|
||||
<!---MARKER_GEN_START-->
|
||||
Start a build
|
||||
|
||||
### Aliases
|
||||
|
||||
`docker build`, `docker builder build`, `docker image build`, `docker buildx b`
|
||||
|
||||
### Options
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
|:--------------------|:--------------|:----------|:-------------------------------------------------------------------------------------------------------------|
|
||||
| `--add-host` | `stringSlice` | | Add a custom host-to-IP mapping (format: `host:ip`) |
|
||||
| `--allow` | `stringArray` | | Allow extra privileged entitlement (e.g., `network.host`, `security.insecure`) |
|
||||
| `--annotation` | `stringArray` | | Add annotation to the image |
|
||||
| `--attest` | `stringArray` | | Attestation parameters (format: `type=sbom,generator=image`) |
|
||||
| `--build-arg` | `stringArray` | | Set build-time variables |
|
||||
| `--build-context` | `stringArray` | | Additional build contexts (e.g., name=path) |
|
||||
| `--builder` | `string` | | Override the configured builder instance |
|
||||
| `--cache-from` | `stringArray` | | External cache sources (e.g., `user/app:cache`, `type=local,src=path/to/dir`) |
|
||||
| `--cache-to` | `stringArray` | | Cache export destinations (e.g., `user/app:cache`, `type=local,dest=path/to/dir`) |
|
||||
| `--call` | `string` | `build` | Set method for evaluating build (`check`, `outline`, `targets`) |
|
||||
| `--cgroup-parent` | `string` | | Set the parent cgroup for the `RUN` instructions during build |
|
||||
| `--check` | `bool` | | Shorthand for `--call=check` |
|
||||
| `-D`, `--debug` | `bool` | | Enable debug logging |
|
||||
| `-f`, `--file` | `string` | | Name of the Dockerfile (default: `PATH/Dockerfile`) |
|
||||
| `--iidfile` | `string` | | Write the image ID to a file |
|
||||
| `--label` | `stringArray` | | Set metadata for an image |
|
||||
| `--load` | `bool` | | Shorthand for `--output=type=docker` |
|
||||
| `--metadata-file` | `string` | | Write build result metadata to a file |
|
||||
| `--network` | `string` | `default` | Set the networking mode for the `RUN` instructions during build |
|
||||
| `--no-cache` | `bool` | | Do not use cache when building the image |
|
||||
| `--no-cache-filter` | `stringArray` | | Do not cache specified stages |
|
||||
| `-o`, `--output` | `stringArray` | | Output destination (format: `type=local,dest=path`) |
|
||||
| `--platform` | `stringArray` | | Set target platform for build |
|
||||
| `--progress` | `string` | `auto` | Set type of progress output (`auto`, `quiet`, `plain`, `tty`, `rawjson`). Use plain to show container output |
|
||||
| `--provenance` | `string` | | Shorthand for `--attest=type=provenance` |
|
||||
| `--pull` | `bool` | | Always attempt to pull all referenced images |
|
||||
| `--push` | `bool` | | Shorthand for `--output=type=registry` |
|
||||
| `-q`, `--quiet` | `bool` | | Suppress the build output and print image ID on success |
|
||||
| `--sbom` | `string` | | Shorthand for `--attest=type=sbom` |
|
||||
| `--secret` | `stringArray` | | Secret to expose to the build (format: `id=mysecret[,src=/local/secret]`) |
|
||||
| `--shm-size` | `bytes` | `0` | Shared memory size for build containers |
|
||||
| `--ssh` | `stringArray` | | SSH agent socket or keys to expose to the build (format: `default\|<id>[=<socket>\|<key>[,<key>]]`) |
|
||||
| `-t`, `--tag` | `stringArray` | | Name and optionally a tag (format: `name:tag`) |
|
||||
| `--target` | `string` | | Set the target build stage to build |
|
||||
| `--ulimit` | `ulimit` | | Ulimit options |
|
||||
|
||||
|
||||
<!---MARKER_GEN_END-->
|
||||
|
||||
@@ -21,6 +21,7 @@ require (
|
||||
github.com/docker/docker v28.3.0+incompatible
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/gofrs/flock v0.12.1
|
||||
github.com/google/go-dap v0.12.0
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/hashicorp/go-cty-funcs v0.0.0-20250210171435-dda779884a9f
|
||||
|
||||
@@ -179,6 +179,8 @@ github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYu
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/go-dap v0.12.0 h1:rVcjv3SyMIrpaOoTAdFDyHs99CwVOItIJGKLQFQhNeM=
|
||||
github.com/google/go-dap v0.12.0/go.mod h1:tNjCASCm5cqePi/RVXXWEVqtnNLV1KTWtYOqu6rZNzc=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
|
||||
+13
-2
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/docker/buildx/util/ioset"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/google/shlex"
|
||||
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||
"github.com/moby/buildkit/identity"
|
||||
"github.com/moby/buildkit/solver/errdefs"
|
||||
@@ -56,7 +57,7 @@ func (m *Monitor) Handler() build.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Monitor) Evaluate(ctx context.Context, c gateway.Client, res *gateway.Result) error {
|
||||
func (m *Monitor) Evaluate(ctx context.Context, _ string, c gateway.Client, res *gateway.Result) error {
|
||||
buildErr := res.EachRef(func(ref gateway.Reference) error {
|
||||
return ref.Evaluate(ctx)
|
||||
})
|
||||
@@ -70,7 +71,17 @@ func (m *Monitor) Evaluate(ctx context.Context, c gateway.Client, res *gateway.R
|
||||
logrus.Warnf("failed to print error information: %v", err)
|
||||
}
|
||||
|
||||
rCtx := build.NewResultHandle(ctx, c, res, buildErr)
|
||||
ps, err := exptypes.ParsePlatforms(res.Metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ref, ok := res.FindRef(ps.Platforms[0].ID)
|
||||
if !ok {
|
||||
return errors.Errorf("no reference found for %s", ps.Platforms[0].ID)
|
||||
}
|
||||
|
||||
rCtx := build.NewResultHandle(ctx, c, ref, res.Metadata, buildErr)
|
||||
if monitorErr := m.Run(ctx, rCtx); monitorErr != nil {
|
||||
if errors.Is(monitorErr, build.ErrRestart) {
|
||||
return build.ErrRestart
|
||||
|
||||
@@ -2,10 +2,10 @@ package progress
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/docker/buildx/util/logutil"
|
||||
"github.com/mitchellh/hashstructure/v2"
|
||||
"github.com/moby/buildkit/client"
|
||||
@@ -25,7 +25,7 @@ const (
|
||||
)
|
||||
|
||||
type Printer struct {
|
||||
out console.File
|
||||
out io.Writer
|
||||
mode progressui.DisplayMode
|
||||
opt *printerOpts
|
||||
|
||||
@@ -121,7 +121,7 @@ func (p *Printer) ClearLogSource(v any) {
|
||||
}
|
||||
}
|
||||
|
||||
func NewPrinter(ctx context.Context, out console.File, mode progressui.DisplayMode, opts ...PrinterOpt) (*Printer, error) {
|
||||
func NewPrinter(ctx context.Context, out io.Writer, mode progressui.DisplayMode, opts ...PrinterOpt) (*Printer, error) {
|
||||
opt := &printerOpts{}
|
||||
for _, o := range opts {
|
||||
o(opt)
|
||||
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
# go-dap: Go implementation of the Debug Adapter Protocol
|
||||
|
||||
[](https://pkg.go.dev/github.com/google/go-dap)
|
||||
[](https://github.com/google/go-dap/actions)
|
||||
[](https://goreportcard.com/report/github.com/google/go-dap)
|
||||
|
||||
For an overview of DAP, see
|
||||
https://microsoft.github.io/debug-adapter-protocol/overview
|
||||
|
||||
## Contributing
|
||||
|
||||
We'd love to accept your patches and contributions to this project. See
|
||||
[docs/contributing](https://github.com/google/go-dap/blob/master/docs/contributing.md)
|
||||
for more details.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the Apache License 2.0
|
||||
|
||||
This is not an officially supported Google product.
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file contains utilities for decoding JSON-encoded bytes into DAP message.
|
||||
|
||||
package dap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// DecodeProtocolMessageFieldError describes which JSON attribute
|
||||
// has an unsupported value that the decoding cannot handle.
|
||||
type DecodeProtocolMessageFieldError struct {
|
||||
Seq int
|
||||
SubType string
|
||||
FieldName string
|
||||
FieldValue string
|
||||
Message json.RawMessage
|
||||
}
|
||||
|
||||
func (e *DecodeProtocolMessageFieldError) Error() string {
|
||||
return fmt.Sprintf("%s %s '%s' is not supported (seq: %d)", e.SubType, e.FieldName, e.FieldValue, e.Seq)
|
||||
}
|
||||
|
||||
// defaultCodec is used to decode vanilla DAP messages.
|
||||
var defaultCodec = NewCodec()
|
||||
|
||||
// Codec is responsible for turning byte blobs into DAP messages.
|
||||
type Codec struct {
|
||||
eventCtor map[string]messageCtor
|
||||
requestCtor map[string]messageCtor
|
||||
responseCtor map[string]messageCtor
|
||||
}
|
||||
|
||||
// NewCodec constructs a new codec that extends the vanilla DAP protocol.
|
||||
// Unless you need to register custom DAP messages, use
|
||||
// DecodeProtocolMessage instead.
|
||||
func NewCodec() *Codec {
|
||||
ret := &Codec{
|
||||
eventCtor: make(map[string]messageCtor),
|
||||
requestCtor: make(map[string]messageCtor),
|
||||
responseCtor: make(map[string]messageCtor),
|
||||
}
|
||||
for k, v := range eventCtor {
|
||||
ret.eventCtor[k] = v
|
||||
}
|
||||
for k, v := range requestCtor {
|
||||
ret.requestCtor[k] = v
|
||||
}
|
||||
for k, v := range responseCtor {
|
||||
ret.responseCtor[k] = v
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// RegisterRequest registers a new custom DAP command, so that it can be
|
||||
// unmarshalled by DecodeMessage. Returns an error when the command already
|
||||
// exists.
|
||||
//
|
||||
// The ctor functions need to return a new instance of the underlying DAP
|
||||
// message type. A typical usage looks like this:
|
||||
//
|
||||
// reqCtor := func() Message { return &LaunchRequest{} }
|
||||
// respCtor := func() Message { return &LaunchResponse{} }
|
||||
// codec.RegisterRequest("launch", reqCtor, respCtor)
|
||||
func (c *Codec) RegisterRequest(command string, requestCtor, responseCtor func() Message) error {
|
||||
_, hasReqCtor := c.requestCtor[command]
|
||||
_, hasRespCtor := c.responseCtor[command]
|
||||
if hasReqCtor || hasRespCtor {
|
||||
return fmt.Errorf("command %q is already registered", command)
|
||||
}
|
||||
c.requestCtor[command] = requestCtor
|
||||
c.responseCtor[command] = responseCtor
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterEvent registers a new custom DAP event, so that it can be
|
||||
// unmarshalled by DecodeMessage. Returns an error when the event already
|
||||
// exists.
|
||||
//
|
||||
// The ctor function needs to return a new instance of the underlying DAP
|
||||
// message type. A typical usage looks like this:
|
||||
//
|
||||
// ctor := func() Message { return &StoppedEvent{} }
|
||||
// codec.RegisterEvent("stopped", ctor)
|
||||
func (c *Codec) RegisterEvent(event string, ctor func() Message) error {
|
||||
if _, hasEventCtor := c.eventCtor[event]; hasEventCtor {
|
||||
return fmt.Errorf("event %q is already registered", event)
|
||||
}
|
||||
c.eventCtor[event] = ctor
|
||||
return nil
|
||||
}
|
||||
|
||||
// DecodeMessage parses the JSON-encoded data and returns the result of
|
||||
// the appropriate type within the ProtocolMessage hierarchy. If message type,
|
||||
// command, etc cannot be cast, returns DecodeProtocolMessageFieldError.
|
||||
// See also godoc for json.Unmarshal, which is used for underlying decoding.
|
||||
func (c *Codec) DecodeMessage(data []byte) (Message, error) {
|
||||
// This struct is the union of the ResponseMessage, RequestMessage, and
|
||||
// EventMessage types. It is an optimization that saves an additional
|
||||
// json.Unmarshal call.
|
||||
var m struct {
|
||||
ProtocolMessage
|
||||
Command string `json:"command"`
|
||||
Event string `json:"event"`
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch m.Type {
|
||||
case "request":
|
||||
return c.decodeRequest(m.Command, m.Seq, data)
|
||||
case "response":
|
||||
return c.decodeResponse(m.Command, m.Seq, m.Success, data)
|
||||
case "event":
|
||||
return c.decodeEvent(m.Event, m.Seq, data)
|
||||
default:
|
||||
return nil, &DecodeProtocolMessageFieldError{m.Seq, "ProtocolMessage", "type", m.Type, json.RawMessage(data)}
|
||||
}
|
||||
}
|
||||
|
||||
// decodeRequest determines what request type in the ProtocolMessage hierarchy
|
||||
// data corresponds to and uses json.Unmarshal to populate the corresponding
|
||||
// struct to be returned.
|
||||
func (c *Codec) decodeRequest(command string, seq int, data []byte) (Message, error) {
|
||||
ctor, ok := c.requestCtor[command]
|
||||
if !ok {
|
||||
return nil, &DecodeProtocolMessageFieldError{seq, "Request", "command", command, json.RawMessage(data)}
|
||||
}
|
||||
requestPtr := ctor()
|
||||
err := json.Unmarshal(data, requestPtr)
|
||||
return requestPtr, err
|
||||
}
|
||||
|
||||
// decodeResponse determines what response type in the ProtocolMessage hierarchy
|
||||
// data corresponds to and uses json.Unmarshal to populate the corresponding
|
||||
// struct to be returned.
|
||||
func (c *Codec) decodeResponse(command string, seq int, success bool, data []byte) (Message, error) {
|
||||
if !success {
|
||||
var er ErrorResponse
|
||||
err := json.Unmarshal(data, &er)
|
||||
return &er, err
|
||||
}
|
||||
ctor, ok := c.responseCtor[command]
|
||||
if !ok {
|
||||
return nil, &DecodeProtocolMessageFieldError{seq, "Response", "command", command, json.RawMessage(data)}
|
||||
}
|
||||
responsePtr := ctor()
|
||||
err := json.Unmarshal(data, responsePtr)
|
||||
return responsePtr, err
|
||||
}
|
||||
|
||||
// decodeEvent determines what event type in the ProtocolMessage hierarchy
|
||||
// data corresponds to and uses json.Unmarshal to populate the corresponding
|
||||
// struct to be returned.
|
||||
func (c *Codec) decodeEvent(event string, seq int, data []byte) (Message, error) {
|
||||
ctor, ok := c.eventCtor[event]
|
||||
if !ok {
|
||||
return nil, &DecodeProtocolMessageFieldError{seq, "Event", "event", event, json.RawMessage(data)}
|
||||
}
|
||||
eventPtr := ctor()
|
||||
err := json.Unmarshal(data, eventPtr)
|
||||
return eventPtr, err
|
||||
}
|
||||
|
||||
// DecodeProtocolMessage parses the JSON-encoded ProtocolMessage and returns
|
||||
// the message embedded in it. If message type, command, etc cannot be cast,
|
||||
// returns DecodeProtocolMessageFieldError. See also godoc for json.Unmarshal,
|
||||
// which is used for underlying decoding.
|
||||
func DecodeProtocolMessage(data []byte) (Message, error) {
|
||||
return defaultCodec.DecodeMessage(data)
|
||||
}
|
||||
|
||||
type messageCtor func() Message
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// Copyright 2021 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package dap contains data types and code for Debug Adapter Protocol (DAP) specification.
|
||||
// https://github.com/microsoft/vscode-debugadapter-node/blob/main/debugProtocol.json
|
||||
package dap
|
||||
|
||||
//go:generate go run ./cmd/gentypes/gentypes.go -o schematypes.go -u cmd/gentypes/debugProtocol.json
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
// Copyright 2020 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// This file contains utilities for DAP Base protocol I/O.
|
||||
// For additional information, see "Base protocol" section in
|
||||
// https://microsoft.github.io/debug-adapter-protocol/overview.
|
||||
|
||||
package dap
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BaseProtocolError represents base protocol error, which occurs when the raw
|
||||
// message does not conform to the header+content format of the base protocol.
|
||||
type BaseProtocolError struct {
|
||||
Err string
|
||||
}
|
||||
|
||||
func (bpe *BaseProtocolError) Error() string { return bpe.Err }
|
||||
|
||||
var (
|
||||
// ErrHeaderDelimiterNotCrLfCrLf is returned when only partial header
|
||||
// delimiter \r\n\r\n is encountered.
|
||||
ErrHeaderDelimiterNotCrLfCrLf = &BaseProtocolError{fmt.Sprintf("header delimiter is not %q", crLfcrLf)}
|
||||
|
||||
// ErrHeaderNotContentLength is returned when the parsed header is
|
||||
// not of valid Content-Length format.
|
||||
ErrHeaderNotContentLength = &BaseProtocolError{fmt.Sprintf("header format is not %q", contentLengthHeaderRegex)}
|
||||
|
||||
// ErrHeaderContentTooLong is returned when the content length specified in
|
||||
// the header is above contentMaxLength.
|
||||
ErrHeaderContentTooLong = &BaseProtocolError{fmt.Sprintf("content length over %v bytes", contentMaxLength)}
|
||||
)
|
||||
|
||||
const (
|
||||
crLfcrLf = "\r\n\r\n"
|
||||
contentLengthHeaderFmt = "Content-Length: %d\r\n\r\n"
|
||||
contentMaxLength = 4 * 1024 * 1024
|
||||
)
|
||||
|
||||
var (
|
||||
contentLengthHeaderRegex = regexp.MustCompile("^Content-Length: ([0-9]+)$")
|
||||
)
|
||||
|
||||
// WriteBaseMessage formats content with Content-Length header and delimiters
|
||||
// as per the base protocol and writes the resulting message to w.
|
||||
func WriteBaseMessage(w io.Writer, content []byte) error {
|
||||
header := fmt.Sprintf(contentLengthHeaderFmt, len(content))
|
||||
if _, err := w.Write([]byte(header)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(content)
|
||||
return err
|
||||
}
|
||||
|
||||
// ReadBaseMessage reads one message from r consisting of a Content-Length
|
||||
// header and a content part. It parses the header to determine the size of
|
||||
// the content part and extracts and returns the actual content of the message.
|
||||
// Returns nil bytes on error, which can be one of the standard IO errors or
|
||||
// a BaseProtocolError defined in this package.
|
||||
func ReadBaseMessage(r *bufio.Reader) ([]byte, error) {
|
||||
contentLength, err := readContentLengthHeader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if contentLength > contentMaxLength {
|
||||
return nil, ErrHeaderContentTooLong
|
||||
}
|
||||
content := make([]byte, contentLength)
|
||||
if _, err = io.ReadFull(r, content); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// readContentLengthHeader looks for the only header field that is supported
|
||||
// and required:
|
||||
// Content-Length: [0-9]+\r\n\r\n
|
||||
// Extracts and returns the content length.
|
||||
func readContentLengthHeader(r *bufio.Reader) (contentLength int64, err error) {
|
||||
// Look for <some header>\r\n\r\n
|
||||
headerWithCr, err := r.ReadString('\r')
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
nextThree := make([]byte, 3)
|
||||
if _, err = io.ReadFull(r, nextThree); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if string(nextThree) != "\n\r\n" {
|
||||
return 0, ErrHeaderDelimiterNotCrLfCrLf
|
||||
}
|
||||
|
||||
// If header is in the right format, get the length
|
||||
header := strings.TrimSuffix(headerWithCr, "\r")
|
||||
headerAndLength := contentLengthHeaderRegex.FindStringSubmatch(header)
|
||||
if len(headerAndLength) < 2 {
|
||||
return 0, ErrHeaderNotContentLength
|
||||
}
|
||||
return strconv.ParseInt(headerAndLength[1], 10, 64)
|
||||
}
|
||||
|
||||
// WriteProtocolMessage encodes message and writes it to w.
|
||||
func WriteProtocolMessage(w io.Writer, message Message) error {
|
||||
b, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return WriteBaseMessage(w, b)
|
||||
}
|
||||
|
||||
// ReadProtocolMessage reads a message from r, decodes and returns it.
|
||||
func ReadProtocolMessage(r *bufio.Reader) (Message, error) {
|
||||
content, err := ReadBaseMessage(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return DecodeProtocolMessage(content)
|
||||
}
|
||||
+1917
File diff suppressed because it is too large
Load Diff
Vendored
+3
@@ -361,6 +361,9 @@ github.com/google/go-cmp/cmp/internal/diff
|
||||
github.com/google/go-cmp/cmp/internal/flags
|
||||
github.com/google/go-cmp/cmp/internal/function
|
||||
github.com/google/go-cmp/cmp/internal/value
|
||||
# github.com/google/go-dap v0.12.0
|
||||
## explicit; go 1.18
|
||||
github.com/google/go-dap
|
||||
# github.com/google/gofuzz v1.2.0
|
||||
## explicit; go 1.12
|
||||
github.com/google/gofuzz
|
||||
|
||||
Reference in New Issue
Block a user