Skip to content

Commit 3453f3b

Browse files
committed
dap: support evaluate request to invoke a container
Supports using the `evaluate` request in REPL mode to start a container with the `exec` command. Presently doesn't support any arguments. This improves the dap server so it is capable of sending reverse requests and receiving the response. It also adds a hidden command `dap attach` that attaches to the socket created by `evaluate`. This requires the client to support `runInTerminal`. Likely needs some additional work to make sure resources are cleaned up cleanly especially when the build is unpaused or terminated, but it should work as a decent base. Signed-off-by: Jonathan A. Sternberg <[email protected]>
1 parent d5b9564 commit 3453f3b

File tree

6 files changed

+280
-17
lines changed

6 files changed

+280
-17
lines changed

commands/dap.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ package commands
22

33
import (
44
"context"
5+
"io"
6+
"net"
7+
"os"
58

9+
"github.com/containerd/console"
610
"github.com/docker/buildx/dap"
711
"github.com/docker/buildx/dap/common"
812
"github.com/docker/buildx/util/cobrautil"
913
"github.com/docker/buildx/util/ioset"
1014
"github.com/docker/buildx/util/progress"
15+
"github.com/docker/cli/cli"
1116
"github.com/docker/cli/cli/command"
1217
"github.com/pkg/errors"
1318
"github.com/spf13/cobra"
@@ -24,6 +29,8 @@ func dapCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
2429
dapBuildCmd := buildCmd(dockerCli, rootOpts, &options)
2530
dapBuildCmd.Args = cobra.RangeArgs(0, 1)
2631
cmd.AddCommand(dapBuildCmd)
32+
33+
cmd.AddCommand(dapAttachCmd())
2734
return cmd
2835
}
2936

@@ -71,3 +78,39 @@ func (d *adapterProtocolDebugger) Stop() error {
7178
defer d.conn.Close()
7279
return d.Adapter.Stop()
7380
}
81+
82+
func dapAttachCmd() *cobra.Command {
83+
cmd := &cobra.Command{
84+
Use: "attach PATH",
85+
Short: "Attach to a container created by the dap evaluate request",
86+
Args: cli.ExactArgs(1),
87+
Hidden: true,
88+
RunE: func(cmd *cobra.Command, args []string) error {
89+
c, err := console.ConsoleFromFile(os.Stdout)
90+
if err != nil {
91+
return err
92+
}
93+
94+
if err := c.SetRaw(); err != nil {
95+
return err
96+
}
97+
98+
conn, err := net.Dial("unix", args[0])
99+
if err != nil {
100+
return err
101+
}
102+
103+
fwd := ioset.NewSingleForwarder()
104+
fwd.SetReader(os.Stdin)
105+
fwd.SetWriter(conn, func() io.WriteCloser {
106+
return conn
107+
})
108+
109+
if _, err := io.Copy(os.Stdout, conn); err != nil && !errors.Is(err, io.EOF) {
110+
return err
111+
}
112+
return nil
113+
},
114+
}
115+
return cmd
116+
}

dap/adapter.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type Adapter[C LaunchConfig] struct {
3030
initialized chan struct{}
3131
started chan launchResponse[C]
3232
configuration chan struct{}
33+
supportsExec bool
3334

3435
evaluateReqCh chan *evaluateRequest
3536

@@ -104,6 +105,9 @@ func (d *Adapter[C]) Stop() error {
104105
func (d *Adapter[C]) Initialize(c Context, req *dap.InitializeRequest, resp *dap.InitializeResponse) error {
105106
close(d.initialized)
106107

108+
// Set parameters based on passed client capabilities.
109+
d.supportsExec = req.Arguments.SupportsRunInTerminalRequest
110+
107111
// Set capabilities.
108112
resp.Body.SupportsConfigurationDoneRequest = true
109113
return nil
@@ -262,6 +266,20 @@ func (d *Adapter[C]) getThread(id int) (t *thread) {
262266
return t
263267
}
264268

269+
func (d *Adapter[C]) getFirstThread() (t *thread) {
270+
d.threadsMu.Lock()
271+
defer d.threadsMu.Unlock()
272+
273+
for _, thread := range d.threads {
274+
if thread.isPaused() {
275+
if t == nil || thread.id < t.id {
276+
t = thread
277+
}
278+
}
279+
}
280+
return t
281+
}
282+
265283
func (d *Adapter[C]) deleteThread(ctx Context, t *thread) {
266284
d.threadsMu.Lock()
267285
if t := d.threads[t.id]; t != nil {
@@ -454,6 +472,7 @@ func (d *Adapter[C]) dapHandler() Handler {
454472
StackTrace: d.StackTrace,
455473
Scopes: d.Scopes,
456474
Variables: d.Variables,
475+
Evaluate: d.Evaluate,
457476
Source: d.Source,
458477
}
459478
}

dap/eval.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package dap
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/docker/buildx/build"
11+
"github.com/docker/cli/cli-plugins/metadata"
12+
"github.com/google/go-dap"
13+
"github.com/google/shlex"
14+
"github.com/pkg/errors"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
func (d *Adapter[C]) Evaluate(ctx Context, req *dap.EvaluateRequest, resp *dap.EvaluateResponse) error {
19+
if req.Arguments.Context != "repl" {
20+
return errors.Errorf("unsupported evaluate context: %s", req.Arguments.Context)
21+
}
22+
23+
args, err := shlex.Split(req.Arguments.Expression)
24+
if err != nil {
25+
return errors.Wrapf(err, "cannot parse expression")
26+
}
27+
28+
if len(args) == 0 {
29+
return nil
30+
}
31+
32+
var t *thread
33+
if req.Arguments.FrameId > 0 {
34+
if t = d.getThreadByFrameID(req.Arguments.FrameId); t == nil {
35+
return errors.Errorf("no thread with frame id %d", req.Arguments.FrameId)
36+
}
37+
} else {
38+
if t = d.getFirstThread(); t == nil {
39+
return errors.New("no paused thread")
40+
}
41+
}
42+
43+
cmd := d.replCommands(ctx, t, resp)
44+
cmd.SetArgs(args)
45+
cmd.SetErr(d.Out())
46+
if err := cmd.Execute(); err != nil {
47+
fmt.Fprintf(d.Out(), "ERROR: %+v\n", err)
48+
}
49+
return nil
50+
}
51+
52+
func (d *Adapter[C]) replCommands(ctx Context, t *thread, resp *dap.EvaluateResponse) *cobra.Command {
53+
rootCmd := &cobra.Command{}
54+
55+
execCmd := &cobra.Command{
56+
Use: "exec",
57+
RunE: func(cmd *cobra.Command, args []string) error {
58+
if !d.supportsExec {
59+
return errors.New("cannot exec without runInTerminal client capability")
60+
}
61+
return t.Exec(ctx, args, resp)
62+
},
63+
}
64+
rootCmd.AddCommand(execCmd)
65+
return rootCmd
66+
}
67+
68+
func (t *thread) Exec(ctx Context, args []string, eresp *dap.EvaluateResponse) (retErr error) {
69+
cfg := &build.InvokeConfig{Tty: true}
70+
if len(cfg.Entrypoint) == 0 && len(cfg.Cmd) == 0 {
71+
cfg.Entrypoint = []string{"/bin/sh"} // launch shell by default
72+
cfg.Cmd = []string{}
73+
cfg.NoCmd = false
74+
}
75+
76+
ctr, err := build.NewContainer(ctx, t.rCtx, cfg)
77+
if err != nil {
78+
return err
79+
}
80+
defer func() {
81+
if retErr != nil {
82+
ctr.Cancel()
83+
}
84+
}()
85+
86+
dir, err := os.MkdirTemp("", "buildx-dap-exec")
87+
if err != nil {
88+
return err
89+
}
90+
defer func() {
91+
if retErr != nil {
92+
os.RemoveAll(dir)
93+
}
94+
}()
95+
96+
socketPath := filepath.Join(dir, "s.sock")
97+
l, err := net.Listen("unix", socketPath)
98+
if err != nil {
99+
return err
100+
}
101+
102+
go func() {
103+
defer os.RemoveAll(dir)
104+
t.runExec(l, ctr, cfg)
105+
}()
106+
107+
// TODO: this should work in standalone mode too.
108+
docker := os.Getenv(metadata.ReexecEnvvar)
109+
req := &dap.RunInTerminalRequest{
110+
Request: dap.Request{
111+
Command: "runInTerminal",
112+
},
113+
Arguments: dap.RunInTerminalRequestArguments{
114+
Kind: "integrated",
115+
Args: []string{docker, "buildx", "dap", "attach", socketPath},
116+
Env: map[string]any{
117+
"BUILDX_EXPERIMENTAL": "1",
118+
},
119+
},
120+
}
121+
122+
resp := ctx.Request(req)
123+
if !resp.GetResponse().Success {
124+
return errors.New(resp.GetResponse().Message)
125+
}
126+
127+
eresp.Body.Result = fmt.Sprintf("Started process attached to %s.", socketPath)
128+
return nil
129+
}
130+
131+
func (t *thread) runExec(l net.Listener, ctr *build.Container, cfg *build.InvokeConfig) {
132+
defer l.Close()
133+
defer ctr.Cancel()
134+
135+
conn, err := l.Accept()
136+
if err != nil {
137+
return
138+
}
139+
defer conn.Close()
140+
141+
// start a background goroutine to politely refuse any subsequent connections.
142+
go func() {
143+
for {
144+
conn, err := l.Accept()
145+
if err != nil {
146+
return
147+
}
148+
fmt.Fprint(conn, "Error: Already connected to exec instance.")
149+
conn.Close()
150+
}
151+
}()
152+
ctr.Exec(context.Background(), cfg, conn, conn, conn)
153+
}

dap/handler.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ type Context interface {
1212
context.Context
1313
C() chan<- dap.Message
1414
Go(f func(c Context)) bool
15+
Request(req dap.RequestMessage) dap.ResponseMessage
1516
}
1617

1718
type dispatchContext struct {
@@ -28,6 +29,14 @@ func (c *dispatchContext) Go(f func(c Context)) bool {
2829
return c.srv.Go(f)
2930
}
3031

32+
func (c *dispatchContext) Request(req dap.RequestMessage) dap.ResponseMessage {
33+
respCh := make(chan dap.ResponseMessage, 1)
34+
c.srv.doRequest(c, req, func(c Context, resp dap.ResponseMessage) {
35+
respCh <- resp
36+
})
37+
return <-respCh
38+
}
39+
3140
type HandlerFunc[Req dap.RequestMessage, Resp dap.ResponseMessage] func(c Context, req Req, resp Resp) error
3241

3342
func (h HandlerFunc[Req, Resp]) Do(c Context, req Req) (resp Resp, err error) {

0 commit comments

Comments
 (0)