Skip to content

Commit e75bc33

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 <jonathan.sternberg@docker.com>
1 parent ffdd0bf commit e75bc33

File tree

5 files changed

+258
-7
lines changed

5 files changed

+258
-7
lines changed

commands/dap.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@ package commands
22

33
import (
44
"context"
5+
<<<<<<< HEAD
6+
=======
7+
"io"
8+
"net"
9+
"os"
10+
>>>>>>> cc003efc (dap: support evaluate request to invoke a container)
511

12+
"github.com/containerd/console"
613
"github.com/docker/buildx/dap"
714
"github.com/docker/buildx/util/cobrautil"
815
"github.com/docker/buildx/util/ioset"
916
"github.com/docker/buildx/util/progress"
17+
"github.com/docker/cli/cli"
1018
"github.com/docker/cli/cli/command"
1119
"github.com/pkg/errors"
1220
"github.com/spf13/cobra"
@@ -28,6 +36,8 @@ func dapCmd(dockerCli command.Cli, rootOpts *rootOptions) *cobra.Command {
2836
dapBuildCmd := buildCmd(dockerCli, rootOpts, &options)
2937
dapBuildCmd.Args = cobra.RangeArgs(0, 1)
3038
cmd.AddCommand(dapBuildCmd)
39+
40+
cmd.AddCommand(dapAttachCmd())
3141
return cmd
3242
}
3343

@@ -79,3 +89,39 @@ func (d *adapterProtocolDebugger) Stop() error {
7989
defer d.conn.Close()
8090
return d.Adapter.Stop()
8191
}
92+
93+
func dapAttachCmd() *cobra.Command {
94+
cmd := &cobra.Command{
95+
Use: "attach PATH",
96+
Short: "Attach to a container created by the dap evaluate request",
97+
Args: cli.ExactArgs(1),
98+
Hidden: true,
99+
RunE: func(cmd *cobra.Command, args []string) error {
100+
c, err := console.ConsoleFromFile(os.Stdout)
101+
if err != nil {
102+
return err
103+
}
104+
105+
if err := c.SetRaw(); err != nil {
106+
return err
107+
}
108+
109+
conn, err := net.Dial("unix", args[0])
110+
if err != nil {
111+
return err
112+
}
113+
114+
fwd := ioset.NewSingleForwarder()
115+
fwd.SetReader(os.Stdin)
116+
fwd.SetWriter(conn, func() io.WriteCloser {
117+
return conn
118+
})
119+
120+
if _, err := io.Copy(os.Stdout, conn); err != nil && !errors.Is(err, io.EOF) {
121+
return err
122+
}
123+
return nil
124+
},
125+
}
126+
return cmd
127+
}

dap/adapter.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type Adapter[T any] struct {
2828
threads map[int]*thread
2929
threadsMu sync.RWMutex
3030
nextThreadID int
31+
32+
supportsExec bool
3133
}
3234

3335
func New[T any](cfg *build.InvokeConfig) *Adapter[T] {
@@ -92,6 +94,9 @@ func (d *Adapter[T]) Stop() error {
9294
func (d *Adapter[T]) Initialize(c Context, req *dap.InitializeRequest, resp *dap.InitializeResponse) error {
9395
close(d.initialized)
9496

97+
// Set parameters based on passed client capabilities.
98+
d.supportsExec = req.Arguments.SupportsRunInTerminalRequest
99+
95100
// Set capabilities.
96101
resp.Body.SupportsConfigurationDoneRequest = true
97102
return nil
@@ -221,6 +226,20 @@ func (d *Adapter[T]) getThread(id int) (t *thread) {
221226
return t
222227
}
223228

229+
func (d *Adapter[T]) getCurrentThread() (t *thread) {
230+
d.threadsMu.Lock()
231+
defer d.threadsMu.Unlock()
232+
233+
for _, thread := range d.threads {
234+
if thread.isPaused() {
235+
if t == nil || thread.id < t.id {
236+
t = thread
237+
}
238+
}
239+
}
240+
return t
241+
}
242+
224243
func (d *Adapter[T]) deleteThread(ctx Context, t *thread) {
225244
d.threadsMu.Lock()
226245
delete(d.threads, t.id)
@@ -346,6 +365,7 @@ func (d *Adapter[T]) dapHandler() Handler {
346365
Disconnect: d.Disconnect,
347366
Threads: d.Threads,
348367
StackTrace: d.StackTrace,
368+
Evaluate: d.Evaluate,
349369
}
350370
}
351371

@@ -422,6 +442,13 @@ func (t *thread) Resume(c Context) {
422442
t.paused = nil
423443
}
424444

445+
func (t *thread[T]) isPaused() bool {
446+
t.mu.Lock()
447+
defer t.mu.Unlock()
448+
449+
return t.paused != nil
450+
}
451+
425452
// TODO: return a suitable stack frame for the thread.
426453
// For now, just returns nothing.
427454
func (t *thread) StackFrames() []dap.StackFrame {

dap/eval.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
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+
)
16+
17+
func (d *Adapter[T]) Evaluate(ctx Context, req *dap.EvaluateRequest, resp *dap.EvaluateResponse) error {
18+
if req.Arguments.Context != "repl" {
19+
return errors.Errorf("unsupported evaluate context: %s", req.Arguments.Context)
20+
}
21+
22+
args, err := shlex.Split(req.Arguments.Expression)
23+
if err != nil {
24+
return errors.Wrapf(err, "cannot parse expression")
25+
}
26+
27+
if len(args) == 0 {
28+
return nil
29+
}
30+
31+
switch arg0 := args[0]; arg0 {
32+
case "exec":
33+
if !d.supportsExec {
34+
return errors.New("dap client does not support runInTerminalRequest")
35+
}
36+
37+
t := d.getCurrentThread()
38+
if t == nil {
39+
return errors.New("no paused thread for exec command")
40+
}
41+
42+
argv := args[1:]
43+
if err := t.Exec(ctx, argv, resp); err != nil {
44+
return err
45+
}
46+
return nil
47+
default:
48+
return errors.Errorf("unknown evalute command: %q", arg0)
49+
}
50+
}
51+
52+
func (t *thread[T]) Exec(ctx Context, args []string, eresp *dap.EvaluateResponse) (retErr error) {
53+
cfg := &build.InvokeConfig{Tty: true}
54+
if len(cfg.Entrypoint) == 0 && len(cfg.Cmd) == 0 {
55+
cfg.Entrypoint = []string{"/bin/sh"} // launch shell by default
56+
cfg.Cmd = []string{}
57+
cfg.NoCmd = false
58+
}
59+
60+
ctr, err := build.NewContainer(ctx, t.rCtx, cfg)
61+
if err != nil {
62+
return err
63+
}
64+
defer func() {
65+
if retErr != nil {
66+
ctr.Cancel()
67+
}
68+
}()
69+
70+
dir, err := os.MkdirTemp("", "buildx-dap-exec")
71+
if err != nil {
72+
return err
73+
}
74+
defer func() {
75+
if retErr != nil {
76+
os.RemoveAll(dir)
77+
}
78+
}()
79+
80+
socketPath := filepath.Join(dir, "s.sock")
81+
l, err := net.Listen("unix", socketPath)
82+
if err != nil {
83+
return err
84+
}
85+
86+
go func() {
87+
defer os.RemoveAll(dir)
88+
t.runExec(l, ctr, cfg)
89+
}()
90+
91+
// TODO: this should work in standalone mode too.
92+
docker := os.Getenv(metadata.ReexecEnvvar)
93+
req := &dap.RunInTerminalRequest{
94+
Request: dap.Request{
95+
Command: "runInTerminal",
96+
},
97+
Arguments: dap.RunInTerminalRequestArguments{
98+
Kind: "integrated",
99+
Args: []string{docker, "buildx", "dap", "attach", socketPath},
100+
Env: map[string]any{
101+
"BUILDX_EXPERIMENTAL": "1",
102+
},
103+
},
104+
}
105+
106+
resp := ctx.Request(req)
107+
if !resp.GetResponse().Success {
108+
return errors.New(resp.GetResponse().Message)
109+
}
110+
111+
eresp.Body.Result = fmt.Sprintf("Started process attached to %s.", socketPath)
112+
return nil
113+
}
114+
115+
func (t *thread[T]) runExec(l net.Listener, ctr *build.Container, cfg *build.InvokeConfig) {
116+
defer l.Close()
117+
defer ctr.Cancel()
118+
119+
conn, err := l.Accept()
120+
if err != nil {
121+
return
122+
}
123+
defer conn.Close()
124+
125+
// start a background goroutine to politely refuse any subsequent connections.
126+
go func() {
127+
for {
128+
conn, err := l.Accept()
129+
if err != nil {
130+
return
131+
}
132+
fmt.Fprint(conn, "Error: Already connected to exec instance.")
133+
conn.Close()
134+
}
135+
}()
136+
ctr.Exec(context.Background(), cfg, conn, conn, conn)
137+
}

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)