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
|
return strings.Join(out, ","), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type (
|
||||||
Evaluate func(ctx context.Context, c gateway.Client, res *gateway.Result) error
|
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) {
|
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)
|
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)
|
rKey := resultKey(dp.driverIndex, k)
|
||||||
results.Set(rKey, res)
|
results.Set(rKey, res)
|
||||||
|
|
||||||
|
forceEval := false
|
||||||
if children := childTargets[rKey]; len(children) > 0 {
|
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
|
return nil, err
|
||||||
}
|
}
|
||||||
} else if bh != nil && bh.Evaluate != nil {
|
forceEval = true
|
||||||
if err := bh.Evaluate(ctx, c, res); err != nil {
|
}
|
||||||
|
|
||||||
|
// 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
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1199,29 +1215,6 @@ func solve(ctx context.Context, c gateway.Client, req gateway.SolveRequest) (*ga
|
|||||||
return res, nil
|
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) {
|
func catchFrontendError(retErr, frontendErr *error) {
|
||||||
*frontendErr = *retErr
|
*frontendErr = *retErr
|
||||||
if errors.Is(*retErr, ErrRestart) {
|
if errors.Is(*retErr, ErrRestart) {
|
||||||
|
|||||||
+24
-12
@@ -14,18 +14,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type InvokeConfig struct {
|
type InvokeConfig struct {
|
||||||
Entrypoint []string
|
Entrypoint []string `json:"entrypoint,omitempty"`
|
||||||
Cmd []string
|
Cmd []string `json:"cmd,omitempty"`
|
||||||
NoCmd bool
|
NoCmd bool `json:"noCmd,omitempty"`
|
||||||
Env []string
|
Env []string `json:"env,omitempty"`
|
||||||
User string
|
User string `json:"user,omitempty"`
|
||||||
NoUser bool
|
NoUser bool `json:"noUser,omitempty"`
|
||||||
Cwd string
|
Cwd string `json:"cwd,omitempty"`
|
||||||
NoCwd bool
|
NoCwd bool `json:"noCwd,omitempty"`
|
||||||
Tty bool
|
Tty bool `json:"tty,omitempty"`
|
||||||
Rollback bool
|
Rollback bool `json:"rollback,omitempty"`
|
||||||
Initial bool
|
Initial bool `json:"initial,omitempty"`
|
||||||
SuspendOn SuspendOn
|
SuspendOn SuspendOn `json:"suspendOn,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *InvokeConfig) NeedsDebug(err error) bool {
|
func (cfg *InvokeConfig) NeedsDebug(err error) bool {
|
||||||
@@ -43,6 +43,18 @@ func (s SuspendOn) DebugEnabled(err error) bool {
|
|||||||
return err != nil || s == SuspendAlways
|
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 {
|
type Container struct {
|
||||||
cancelOnce sync.Once
|
cancelOnce sync.Once
|
||||||
containerCancel func(error)
|
containerCancel func(error)
|
||||||
|
|||||||
+13
-20
@@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"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.
|
// an evaluate call if it is present.
|
||||||
//
|
//
|
||||||
// This ResultHandle can be used to execute additional build steps in the same
|
// This ResultHandle can be used to execute additional build steps in the same
|
||||||
@@ -24,9 +24,10 @@ import (
|
|||||||
// failures and successes.
|
// failures and successes.
|
||||||
//
|
//
|
||||||
// If the returned ResultHandle is not nil, the caller must call Done() on it.
|
// 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{
|
rCtx := &ResultHandle{
|
||||||
res: res,
|
ref: ref,
|
||||||
|
meta: meta,
|
||||||
gwClient: c,
|
gwClient: c,
|
||||||
}
|
}
|
||||||
if err != nil && !errors.As(err, &rCtx.solveErr) {
|
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.
|
// ResultHandle is a build result with the client that built it.
|
||||||
type ResultHandle struct {
|
type ResultHandle struct {
|
||||||
res *gateway.Result
|
ref gateway.Reference
|
||||||
solveErr *errdefs.SolveError
|
solveErr *errdefs.SolveError
|
||||||
|
meta map[string][]byte
|
||||||
gwClient gateway.Client
|
gwClient gateway.Client
|
||||||
|
|
||||||
doneOnce sync.Once
|
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) {
|
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")
|
logrus.Debugf("creating container from successful build")
|
||||||
ccfg, err := containerConfigFromResult(r.res, cfg)
|
ccfg, err := containerConfigFromResult(r.ref, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return containerCfg, err
|
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) {
|
func (r *ResultHandle) getProcessConfig(cfg *InvokeConfig, stdin io.ReadCloser, stdout io.WriteCloser, stderr io.WriteCloser) (_ gateway.StartRequest, err error) {
|
||||||
processCfg := newStartRequest(stdin, stdout, stderr)
|
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")
|
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
|
return processCfg, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -108,20 +110,11 @@ func (r *ResultHandle) getProcessConfig(cfg *InvokeConfig, stdin io.ReadCloser,
|
|||||||
return processCfg, nil
|
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 {
|
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")
|
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{
|
return &gateway.NewContainerRequest{
|
||||||
Mounts: []gateway.Mount{
|
Mounts: []gateway.Mount{
|
||||||
{
|
{
|
||||||
@@ -133,8 +126,8 @@ func containerConfigFromResult(res *gateway.Result, cfg *InvokeConfig) (*gateway
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func populateProcessConfigFromResult(req *gateway.StartRequest, res *gateway.Result, cfg *InvokeConfig) error {
|
func populateProcessConfigFromResult(req *gateway.StartRequest, meta map[string][]byte, cfg *InvokeConfig) error {
|
||||||
imgData := res.Metadata[exptypes.ExporterImageConfigKey]
|
imgData := meta[exptypes.ExporterImageConfigKey]
|
||||||
var img *ocispecs.Image
|
var img *ocispecs.Image
|
||||||
if len(imgData) > 0 {
|
if len(imgData) > 0 {
|
||||||
img = &ocispecs.Image{}
|
img = &ocispecs.Image{}
|
||||||
|
|||||||
+57
-129
@@ -20,7 +20,6 @@ import (
|
|||||||
"github.com/containerd/console"
|
"github.com/containerd/console"
|
||||||
"github.com/docker/buildx/build"
|
"github.com/docker/buildx/build"
|
||||||
"github.com/docker/buildx/builder"
|
"github.com/docker/buildx/builder"
|
||||||
"github.com/docker/buildx/monitor"
|
|
||||||
"github.com/docker/buildx/store"
|
"github.com/docker/buildx/store"
|
||||||
"github.com/docker/buildx/store/storeutil"
|
"github.com/docker/buildx/store/storeutil"
|
||||||
"github.com/docker/buildx/util/buildflags"
|
"github.com/docker/buildx/util/buildflags"
|
||||||
@@ -28,6 +27,7 @@ import (
|
|||||||
"github.com/docker/buildx/util/confutil"
|
"github.com/docker/buildx/util/confutil"
|
||||||
"github.com/docker/buildx/util/desktop"
|
"github.com/docker/buildx/util/desktop"
|
||||||
"github.com/docker/buildx/util/dockerutil"
|
"github.com/docker/buildx/util/dockerutil"
|
||||||
|
"github.com/docker/buildx/util/ioset"
|
||||||
"github.com/docker/buildx/util/metricutil"
|
"github.com/docker/buildx/util/metricutil"
|
||||||
"github.com/docker/buildx/util/osutil"
|
"github.com/docker/buildx/util/osutil"
|
||||||
"github.com/docker/buildx/util/platformutil"
|
"github.com/docker/buildx/util/platformutil"
|
||||||
@@ -55,7 +55,6 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/tonistiigi/go-csvvalue"
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
"go.opentelemetry.io/otel/attribute"
|
||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
@@ -100,8 +99,6 @@ type buildOptions struct {
|
|||||||
pull bool
|
pull bool
|
||||||
exportPush bool
|
exportPush bool
|
||||||
exportLoad bool
|
exportLoad bool
|
||||||
|
|
||||||
invokeConfig *invokeConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *buildOptions) toOptions() (*BuildOptions, error) {
|
func (o *buildOptions) toOptions() (*BuildOptions, error) {
|
||||||
@@ -276,7 +273,7 @@ func (o *buildOptionsHash) String() string {
|
|||||||
return o.result
|
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()
|
mp := dockerCli.MeterProvider()
|
||||||
|
|
||||||
ctx, end, err := tracing.TraceCurrentCommand(ctx, []string{"build", options.contextPath},
|
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
|
driverType := b.Driver
|
||||||
|
|
||||||
var term bool
|
|
||||||
if _, err := console.ConsoleFromFile(os.Stderr); err == nil {
|
|
||||||
term = true
|
|
||||||
}
|
|
||||||
attributes := buildMetricAttributes(dockerCli, driverType, &options)
|
attributes := buildMetricAttributes(dockerCli, driverType, &options)
|
||||||
|
|
||||||
ctx2, cancel := context.WithCancelCause(context.TODO())
|
ctx2, cancel := context.WithCancelCause(context.TODO())
|
||||||
@@ -332,8 +325,38 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
var printer *progress.Printer
|
||||||
printer, err = progress.NewPrinter(ctx2, os.Stderr, progressMode,
|
printer, err = progress.NewPrinter(ctx2, out, progressMode,
|
||||||
progress.WithDesc(
|
progress.WithDesc(
|
||||||
fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver),
|
fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver),
|
||||||
fmt.Sprintf("%s:%s", b.Driver, b.Name),
|
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)
|
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 {
|
if err := printer.Wait(); retErr == nil {
|
||||||
retErr = err
|
retErr = err
|
||||||
@@ -406,26 +429,19 @@ func getImageID(resp map[string]string) string {
|
|||||||
return dgst
|
return dgst
|
||||||
}
|
}
|
||||||
|
|
||||||
func runBuildWithOptions(ctx context.Context, dockerCli command.Cli, opts *BuildOptions, options buildOptions, printer *progress.Printer) (_ *client.SolveResponse, _ *build.Inputs, retErr error) {
|
func runBuildWithOptions(ctx context.Context, dockerCli command.Cli, opts *BuildOptions, dbg debuggerInstance, printer *progress.Printer) (_ *client.SolveResponse, _ *build.Inputs, retErr error) {
|
||||||
if options.invokeConfig != nil && (options.dockerfileName == "-" || options.contextPath == "-") {
|
var bh build.Handler
|
||||||
// stdin must be usable for monitor
|
if dbg != nil {
|
||||||
return nil, nil, errors.Errorf("Dockerfile or context from stdin is not supported with invoke")
|
if err := dbg.Start(printer, opts); err != nil {
|
||||||
}
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
defer dbg.Stop()
|
||||||
|
|
||||||
var (
|
bh = dbg.Handler()
|
||||||
in io.ReadCloser
|
dockerCli.SetIn(nil)
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
in := dockerCli.In()
|
||||||
for {
|
for {
|
||||||
resp, inputs, err := RunBuild(ctx, dockerCli, opts, in, printer, &bh)
|
resp, inputs, err := RunBuild(ctx, dockerCli, opts, in, printer, &bh)
|
||||||
if err != nil {
|
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{}
|
cFlags := &commonFlags{}
|
||||||
options := &buildOptions{}
|
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",
|
"aliases": "docker build, docker builder build, docker image build, docker buildx b",
|
||||||
},
|
},
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
options.contextPath = args[0]
|
if len(args) > 0 {
|
||||||
|
options.contextPath = args[0]
|
||||||
|
}
|
||||||
options.builder = rootOpts.builder
|
options.builder = rootOpts.builder
|
||||||
options.metadataFile = cFlags.metadataFile
|
options.metadataFile = cFlags.metadataFile
|
||||||
options.noCache = false
|
options.noCache = false
|
||||||
@@ -467,15 +485,7 @@ func buildCmd(dockerCli command.Cli, rootOpts *rootOptions, debugConfig *debugOp
|
|||||||
options.progress = cFlags.progress
|
options.progress = cFlags.progress
|
||||||
cmd.Flags().VisitAll(checkWarnedFlags)
|
cmd.Flags().VisitAll(checkWarnedFlags)
|
||||||
|
|
||||||
if debugConfig != nil && (debugConfig.InvokeFlag != "" || debugConfig.OnFlag != "") {
|
return runBuild(cmd.Context(), dockerCli, debugger, *options)
|
||||||
iConfig := new(invokeConfig)
|
|
||||||
if err := iConfig.parseInvokeConfig(debugConfig.InvokeFlag, debugConfig.OnFlag); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
options.invokeConfig = iConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
return runBuild(cmd.Context(), dockerCli, *options)
|
|
||||||
},
|
},
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
return nil, cobra.ShellCompDirectiveFilterDirs
|
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)
|
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 {
|
func callAlias(target *string, value string) cobrautil.BoolFuncValue {
|
||||||
return func(s string) error {
|
return func(s string) error {
|
||||||
v, err := strconv.ParseBool(s)
|
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
|
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
|
package commands
|
||||||
|
|
||||||
import (
|
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/cobrautil"
|
||||||
|
"github.com/docker/buildx/util/ioset"
|
||||||
|
"github.com/docker/buildx/util/progress"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"github.com/tonistiigi/go-csvvalue"
|
||||||
)
|
)
|
||||||
|
|
||||||
type debugOptions struct {
|
type debugOptions struct {
|
||||||
@@ -14,9 +26,21 @@ type debugOptions struct {
|
|||||||
OnFlag string
|
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 {
|
func debugCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
||||||
var options debugOptions
|
var options debugOptions
|
||||||
|
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "debug",
|
Use: "debug",
|
||||||
Short: "Start debugger",
|
Short: "Start debugger",
|
||||||
@@ -32,3 +56,123 @@ func debugCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
|
|||||||
cmd.AddCommand(buildCmd(dockerCli, rootOpts, &options))
|
cmd.AddCommand(buildCmd(dockerCli, rootOpts, &options))
|
||||||
return cmd
|
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() {
|
if confutil.IsExperimental() {
|
||||||
cmd.AddCommand(debugCmd(dockerCli, opts))
|
cmd.AddCommand(debugCmd(dockerCli, opts))
|
||||||
|
cmd.AddCommand(dapCmd(dockerCli, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.RegisterFlagCompletionFunc( //nolint:errcheck
|
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)
|
||||||
|
}
|
||||||
+18
-17
@@ -9,23 +9,24 @@ Extended build capabilities with BuildKit
|
|||||||
|
|
||||||
### Subcommands
|
### Subcommands
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
|:-------------------------------------|:------------------------------------------------|
|
|:-------------------------------------|:----------------------------------------------------------------|
|
||||||
| [`bake`](buildx_bake.md) | Build from a file |
|
| [`bake`](buildx_bake.md) | Build from a file |
|
||||||
| [`build`](buildx_build.md) | Start a build |
|
| [`build`](buildx_build.md) | Start a build |
|
||||||
| [`create`](buildx_create.md) | Create a new builder instance |
|
| [`create`](buildx_create.md) | Create a new builder instance |
|
||||||
| [`debug`](buildx_debug.md) | Start debugger (EXPERIMENTAL) |
|
| [`dap`](buildx_dap.md) | Start debug adapter protocol compatible debugger (EXPERIMENTAL) |
|
||||||
| [`dial-stdio`](buildx_dial-stdio.md) | Proxy current stdio streams to builder instance |
|
| [`debug`](buildx_debug.md) | Start debugger (EXPERIMENTAL) |
|
||||||
| [`du`](buildx_du.md) | Disk usage |
|
| [`dial-stdio`](buildx_dial-stdio.md) | Proxy current stdio streams to builder instance |
|
||||||
| [`history`](buildx_history.md) | Commands to work on build records |
|
| [`du`](buildx_du.md) | Disk usage |
|
||||||
| [`imagetools`](buildx_imagetools.md) | Commands to work on images in registry |
|
| [`history`](buildx_history.md) | Commands to work on build records |
|
||||||
| [`inspect`](buildx_inspect.md) | Inspect current builder instance |
|
| [`imagetools`](buildx_imagetools.md) | Commands to work on images in registry |
|
||||||
| [`ls`](buildx_ls.md) | List builder instances |
|
| [`inspect`](buildx_inspect.md) | Inspect current builder instance |
|
||||||
| [`prune`](buildx_prune.md) | Remove build cache |
|
| [`ls`](buildx_ls.md) | List builder instances |
|
||||||
| [`rm`](buildx_rm.md) | Remove one or more builder instances |
|
| [`prune`](buildx_prune.md) | Remove build cache |
|
||||||
| [`stop`](buildx_stop.md) | Stop builder instance |
|
| [`rm`](buildx_rm.md) | Remove one or more builder instances |
|
||||||
| [`use`](buildx_use.md) | Set the current builder instance |
|
| [`stop`](buildx_stop.md) | Stop builder instance |
|
||||||
| [`version`](buildx_version.md) | Show buildx version information |
|
| [`use`](buildx_use.md) | Set the current builder instance |
|
||||||
|
| [`version`](buildx_version.md) | Show buildx version information |
|
||||||
|
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|||||||
@@ -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/docker v28.3.0+incompatible
|
||||||
github.com/docker/go-units v0.5.0
|
github.com/docker/go-units v0.5.0
|
||||||
github.com/gofrs/flock v0.12.1
|
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/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hashicorp/go-cty-funcs v0.0.0-20250210171435-dda779884a9f
|
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.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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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.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 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
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/ioset"
|
||||||
"github.com/docker/buildx/util/progress"
|
"github.com/docker/buildx/util/progress"
|
||||||
"github.com/google/shlex"
|
"github.com/google/shlex"
|
||||||
|
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
||||||
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||||
"github.com/moby/buildkit/identity"
|
"github.com/moby/buildkit/identity"
|
||||||
"github.com/moby/buildkit/solver/errdefs"
|
"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 {
|
buildErr := res.EachRef(func(ref gateway.Reference) error {
|
||||||
return ref.Evaluate(ctx)
|
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)
|
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 monitorErr := m.Run(ctx, rCtx); monitorErr != nil {
|
||||||
if errors.Is(monitorErr, build.ErrRestart) {
|
if errors.Is(monitorErr, build.ErrRestart) {
|
||||||
return build.ErrRestart
|
return build.ErrRestart
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package progress
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/containerd/console"
|
|
||||||
"github.com/docker/buildx/util/logutil"
|
"github.com/docker/buildx/util/logutil"
|
||||||
"github.com/mitchellh/hashstructure/v2"
|
"github.com/mitchellh/hashstructure/v2"
|
||||||
"github.com/moby/buildkit/client"
|
"github.com/moby/buildkit/client"
|
||||||
@@ -25,7 +25,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Printer struct {
|
type Printer struct {
|
||||||
out console.File
|
out io.Writer
|
||||||
mode progressui.DisplayMode
|
mode progressui.DisplayMode
|
||||||
opt *printerOpts
|
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{}
|
opt := &printerOpts{}
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(opt)
|
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/flags
|
||||||
github.com/google/go-cmp/cmp/internal/function
|
github.com/google/go-cmp/cmp/internal/function
|
||||||
github.com/google/go-cmp/cmp/internal/value
|
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
|
# github.com/google/gofuzz v1.2.0
|
||||||
## explicit; go 1.12
|
## explicit; go 1.12
|
||||||
github.com/google/gofuzz
|
github.com/google/gofuzz
|
||||||
|
|||||||
Reference in New Issue
Block a user