Files
buildx/commands/dap.go
T
Jonathan A. Sternberg ca9df87014 dap: fix the race condition in the dap unit tests
The context used for serving the dap server was being canceled too early
because it used defer which would initiate at the end of the function
while every other cleanup function used `t.Cleanup` which executes in
its own goroutine.

One possible solution was to move the cancel to the cleanup, but the
context being passed to serve and start doesn't make sense because if it
ever does get canceled, it'll likely cause a similar race condition with
`Stop`.

This removes the context from the methods that were causing this issue
in favor of just relying on the caller calling `Stop` when they are done
with the adapter and server.

This seems to have only affected tests and I don't believe it affected
the actual dap command.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
2026-03-10 12:03:35 -05:00

136 lines
2.9 KiB
Go

package commands
import (
"io"
"net"
"os"
"github.com/containerd/console"
"github.com/docker/buildx/dap"
"github.com/docker/buildx/dap/common"
"github.com/docker/buildx/util/cobrautil"
"github.com/docker/buildx/util/ioset"
"github.com/docker/buildx/util/progress"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
const (
dapEnvUserAgent = "BUILDX_DAP_USER_AGENT"
)
func dapCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
var options dapOptions
cmd := &cobra.Command{
Use: "dap",
Short: "Start debug adapter protocol compatible debugger",
DisableFlagsInUseLine: true,
}
cobrautil.MarkCommandExperimental(cmd)
dapBuildCmd := buildCmd(dockerCli, rootOpts, &options)
dapBuildCmd.Args = cobra.RangeArgs(0, 1)
// Remove aliases for documentation.
dapBuildCmd.Aliases = nil
delete(dapBuildCmd.Annotations, "aliases")
cmd.AddCommand(dapBuildCmd)
cmd.AddCommand(dapAttachCmd())
return cmd
}
type dapOptions struct{}
func (d *dapOptions) New(in ioset.In) (debuggerInstance, error) {
conn := dap.NewConn(in.Stdin, in.Stdout)
return &adapterProtocolDebugger{
Adapter: dap.New[LaunchConfig](),
conn: conn,
}, nil
}
func (d *dapOptions) Info() debuggerInfo {
return debuggerInfo{
Name: "dap",
UserAgent: os.Getenv(dapEnvUserAgent),
}
}
type LaunchConfig struct {
Dockerfile string `json:"dockerfile,omitempty"`
ContextPath string `json:"contextPath,omitempty"`
Target string `json:"target,omitempty"`
common.Config
}
type adapterProtocolDebugger struct {
*dap.Adapter[LaunchConfig]
conn dap.Conn
}
func (d *adapterProtocolDebugger) Start(printer *progress.Printer, opts *BuildOptions) error {
cfg, err := d.Adapter.Start(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
}
if cfg.Target != "" {
opts.Target = cfg.Target
}
return nil
}
func (d *adapterProtocolDebugger) Stop() error {
defer d.conn.Close()
return d.Adapter.Stop()
}
func dapAttachCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "attach PATH",
Short: "Attach to a container created by the dap evaluate request",
Args: cli.ExactArgs(1),
Hidden: true,
RunE: func(cmd *cobra.Command, args []string) error {
c, err := console.ConsoleFromFile(os.Stdout)
if err != nil {
return err
}
if err := c.SetRaw(); err != nil {
return err
}
dialer := net.Dialer{}
conn, err := dialer.DialContext(cmd.Context(), "unix", args[0])
if err != nil {
return err
}
fwd := ioset.NewSingleForwarder()
fwd.SetReader(os.Stdin)
fwd.SetWriter(conn, func() io.WriteCloser {
return conn
})
if _, err := io.Copy(os.Stdout, conn); err != nil && !errors.Is(err, io.EOF) {
return err
}
return nil
},
DisableFlagsInUseLine: true,
}
return cmd
}