diff --git a/dap/adapter.go b/dap/adapter.go index d45384f01..68f27cdd4 100644 --- a/dap/adapter.go +++ b/dap/adapter.go @@ -216,6 +216,7 @@ func (d *Adapter[C]) newThread(ctx Context, name string) (t *thread) { sourceMap: &d.sourceMap, breakpointMap: d.breakpointMap, idPool: d.idPool, + variables: newVariableReferences(), } d.threads[t.id] = t d.nextThreadID++ @@ -240,6 +241,11 @@ func (d *Adapter[C]) getThread(id int) (t *thread) { func (d *Adapter[C]) deleteThread(ctx Context, t *thread) { d.threadsMu.Lock() + if t := d.threads[t.id]; t != nil { + if t.variables != nil { + t.variables.Reset() + } + } delete(d.threads, t.id) d.threadsMu.Unlock() @@ -252,6 +258,18 @@ func (d *Adapter[C]) deleteThread(ctx Context, t *thread) { } } +func (d *Adapter[T]) getThreadByFrameID(id int) (t *thread) { + d.threadsMu.RLock() + defer d.threadsMu.RUnlock() + + for _, t := range d.threads { + if t.hasFrame(id) { + return t + } + } + return nil +} + type evaluateRequest struct { name string c gateway.Client @@ -330,6 +348,35 @@ func (d *Adapter[C]) StackTrace(c Context, req *dap.StackTraceRequest, resp *dap return nil } +func (d *Adapter[C]) Scopes(c Context, req *dap.ScopesRequest, resp *dap.ScopesResponse) error { + t := d.getThreadByFrameID(req.Arguments.FrameId) + if t == nil { + return errors.Errorf("no such frame id: %d", req.Arguments.FrameId) + } + + resp.Body.Scopes = t.Scopes(req.Arguments.FrameId) + for i, s := range resp.Body.Scopes { + resp.Body.Scopes[i].VariablesReference = (t.id << 24) | s.VariablesReference + } + return nil +} + +func (d *Adapter[C]) Variables(c Context, req *dap.VariablesRequest, resp *dap.VariablesResponse) error { + tid := req.Arguments.VariablesReference >> 24 + + t := d.getThread(tid) + if t == nil { + return errors.Errorf("no such thread: %d", tid) + } + + varRef := req.Arguments.VariablesReference & ((1 << 24) - 1) + resp.Body.Variables = t.Variables(varRef) + for i, ref := range resp.Body.Variables { + resp.Body.Variables[i].VariablesReference = (tid << 24) | ref.VariablesReference + } + return nil +} + func (d *Adapter[C]) Source(c Context, req *dap.SourceRequest, resp *dap.SourceResponse) error { fname := req.Arguments.Source.Path @@ -378,6 +425,8 @@ func (d *Adapter[C]) dapHandler() Handler { Disconnect: d.Disconnect, Threads: d.Threads, StackTrace: d.StackTrace, + Scopes: d.Scopes, + Variables: d.Variables, Source: d.Source, } } diff --git a/dap/handler.go b/dap/handler.go index b9422a383..abe93aa71 100644 --- a/dap/handler.go +++ b/dap/handler.go @@ -55,6 +55,8 @@ type Handler struct { Restart HandlerFunc[*dap.RestartRequest, *dap.RestartResponse] Threads HandlerFunc[*dap.ThreadsRequest, *dap.ThreadsResponse] StackTrace HandlerFunc[*dap.StackTraceRequest, *dap.StackTraceResponse] + Scopes HandlerFunc[*dap.ScopesRequest, *dap.ScopesResponse] + Variables HandlerFunc[*dap.VariablesRequest, *dap.VariablesResponse] Evaluate HandlerFunc[*dap.EvaluateRequest, *dap.EvaluateResponse] Source HandlerFunc[*dap.SourceRequest, *dap.SourceResponse] } diff --git a/dap/server.go b/dap/server.go index 333566483..e7d449334 100644 --- a/dap/server.go +++ b/dap/server.go @@ -125,6 +125,10 @@ func (s *Server) handleMessage(c Context, m dap.Message) (dap.ResponseMessage, e return s.h.Threads.Do(c, req) case *dap.StackTraceRequest: return s.h.StackTrace.Do(c, req) + case *dap.ScopesRequest: + return s.h.Scopes.Do(c, req) + case *dap.VariablesRequest: + return s.h.Variables.Do(c, req) case *dap.EvaluateRequest: return s.h.Evaluate.Do(c, req) case *dap.SourceRequest: diff --git a/dap/thread.go b/dap/thread.go index 887751b59..5e15d4f44 100644 --- a/dap/thread.go +++ b/dap/thread.go @@ -26,6 +26,7 @@ type thread struct { idPool *idPool sourceMap *sourceMap breakpointMap *breakpointMap + variables *variableReferences // Inputs to the evaluate call. c gateway.Client @@ -48,11 +49,10 @@ type thread struct { mu sync.Mutex // Attributes set when a thread is paused. - rCtx *build.ResultHandle - curPos digest.Digest - - // Lazy attributes that are set when a thread is paused. - stackTrace []dap.StackFrame + rCtx *build.ResultHandle + curPos digest.Digest + stackTrace []int32 + frames map[int32]*frame } type region struct { @@ -154,6 +154,7 @@ func (t *thread) pause(c Context, err error, event dap.StoppedEventBody) <-chan } } } + t.collectStackTrace() event.ThreadId = t.id c.C() <- &dap.StoppedEvent{ @@ -178,18 +179,7 @@ func (t *thread) resume(step stepType) { if t.paused == nil { return } - - if t.rCtx != nil { - t.rCtx.Done() - t.rCtx = nil - } - - if t.stackTrace != nil { - for _, frame := range t.stackTrace { - t.idPool.Put(int64(frame.Id)) - } - t.stackTrace = nil - } + t.releaseState() t.paused <- step close(t.paused) @@ -207,10 +197,23 @@ func (t *thread) StackTrace() []dap.StackFrame { return []dap.StackFrame{} } - if t.stackTrace == nil { - t.stackTrace = t.makeStackTrace() + frames := make([]dap.StackFrame, len(t.stackTrace)) + for i, id := range t.stackTrace { + frames[i] = t.frames[id].StackFrame } - return t.stackTrace + return frames +} + +func (t *thread) Scopes(frameID int) []dap.Scope { + t.mu.Lock() + defer t.mu.Unlock() + + frame := t.frames[int32(frameID)] + return frame.Scopes() +} + +func (t *thread) Variables(id int) []dap.Variable { + return t.variables.Get(id) } func (t *thread) getLLBState(ctx Context) error { @@ -502,15 +505,16 @@ func (t *thread) solve(ctx context.Context, target digest.Digest) (gateway.Refer return res.SingleRef() } -func (t *thread) newStackFrame() dap.StackFrame { - return dap.StackFrame{ - Id: int(t.idPool.Get()), +func (t *thread) releaseState() { + if t.rCtx != nil { + t.rCtx.Done() + t.rCtx = nil } + t.stackTrace = nil + t.frames = nil } -func (t *thread) makeStackTrace() []dap.StackFrame { - var frames []dap.StackFrame - +func (t *thread) collectStackTrace() { region := t.regionsByDigest[t.curPos] r := t.regions[region] @@ -519,45 +523,38 @@ func (t *thread) makeStackTrace() []dap.StackFrame { digests = digests[:index+1] } + t.frames = make(map[int32]*frame) for i := len(digests) - 1; i >= 0; i-- { dgst := digests[i] - frame := t.newStackFrame() + frame := &frame{} + frame.Id = int(t.idPool.Get()) + if meta, ok := t.def.Metadata[dgst]; ok { - fillStackFrameMetadata(&frame, meta) + frame.setNameFromMeta(meta) } if loc, ok := t.def.Source.Locations[string(dgst)]; ok { - t.fillStackFrameLocation(&frame, loc) + frame.fillLocation(t.def, loc, t.sourcePath) } - frames = append(frames, frame) + + if op := t.ops[dgst]; op != nil { + frame.fillVarsFromOp(op, t.variables) + } + t.stackTrace = append(t.stackTrace, int32(frame.Id)) + t.frames[int32(frame.Id)] = frame } - return frames } -func fillStackFrameMetadata(frame *dap.StackFrame, meta llb.OpMetadata) { - if name, ok := meta.Description["llb.customname"]; ok { - frame.Name = name - } else if cmd, ok := meta.Description["com.docker.dockerfile.v1.command"]; ok { - frame.Name = cmd - } - // TODO: should we infer the name from somewhere else? -} +func (t *thread) hasFrame(id int) bool { + t.mu.Lock() + defer t.mu.Unlock() -func (t *thread) fillStackFrameLocation(frame *dap.StackFrame, loc *pb.Locations) { - for _, l := range loc.Locations { - for _, r := range l.Ranges { - frame.Line = int(r.Start.Line) - frame.Column = int(r.Start.Character) - frame.EndLine = int(r.End.Line) - frame.EndColumn = int(r.End.Character) - - info := t.def.Source.Infos[l.SourceIndex] - frame.Source = &dap.Source{ - Path: filepath.Join(t.sourcePath, info.Filename), - } - return - } + if t.paused == nil { + return false } + + _, ok := t.frames[int32(id)] + return ok } func pop[S ~[]E, E any](s *S) E { diff --git a/dap/variables.go b/dap/variables.go new file mode 100644 index 000000000..90db1d10b --- /dev/null +++ b/dap/variables.go @@ -0,0 +1,213 @@ +package dap + +import ( + "fmt" + "path/filepath" + "strconv" + "strings" + "sync" + "sync/atomic" + + "github.com/google/go-dap" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/solver/pb" +) + +type frame struct { + dap.StackFrame + scopes []dap.Scope +} + +func (f *frame) setNameFromMeta(meta llb.OpMetadata) { + if name, ok := meta.Description["llb.customname"]; ok { + f.Name = name + } else if cmd, ok := meta.Description["com.docker.dockerfile.v1.command"]; ok { + f.Name = cmd + } + // TODO: should we infer the name from somewhere else? +} + +func (f *frame) fillLocation(def *llb.Definition, loc *pb.Locations, ws string) { + for _, l := range loc.Locations { + for _, r := range l.Ranges { + f.Line = int(r.Start.Line) + f.Column = int(r.Start.Character) + f.EndLine = int(r.End.Line) + f.EndColumn = int(r.End.Character) + + info := def.Source.Infos[l.SourceIndex] + f.Source = &dap.Source{ + Path: filepath.Join(ws, info.Filename), + } + return + } + } +} + +func (f *frame) fillVarsFromOp(op *pb.Op, refs *variableReferences) { + f.scopes = []dap.Scope{ + { + Name: "Arguments", + PresentationHint: "arguments", + VariablesReference: refs.New(func() []dap.Variable { + var vars []dap.Variable + if op.Platform != nil { + vars = append(vars, platformVars(op.Platform, refs)) + } + + switch op := op.Op.(type) { + case *pb.Op_Exec: + vars = append(vars, execOpVars(op.Exec, refs)) + } + return vars + }), + }, + } +} + +func platformVars(platform *pb.Platform, refs *variableReferences) dap.Variable { + return dap.Variable{ + Name: "platform", + Value: fmt.Sprintf("%s/%s", platform.OS, platform.Architecture), + VariablesReference: refs.New(func() []dap.Variable { + vars := []dap.Variable{ + { + Name: "architecture", + Value: platform.Architecture, + }, + { + Name: "os", + Value: platform.OS, + }, + } + + if platform.Variant != "" { + vars = append(vars, dap.Variable{ + Name: "variant", + Value: platform.Variant, + }) + } + + if platform.OSVersion != "" { + vars = append(vars, dap.Variable{ + Name: "osversion", + Value: platform.OSVersion, + }) + } + return vars + }), + } +} + +func execOpVars(exec *pb.ExecOp, refs *variableReferences) dap.Variable { + return dap.Variable{ + Name: "exec", + Value: strings.Join(exec.Meta.Args, " "), + VariablesReference: refs.New(func() []dap.Variable { + vars := []dap.Variable{ + { + Name: "args", + Value: brief(strings.Join(exec.Meta.Args, " ")), + VariablesReference: refs.New(func() []dap.Variable { + vars := make([]dap.Variable, 0, len(exec.Meta.Args)) + for i, arg := range exec.Meta.Args { + vars = append(vars, dap.Variable{ + Name: strconv.Itoa(i), + Value: arg, + }) + } + return vars + }), + }, + { + Name: "env", + Value: brief(strings.Join(exec.Meta.Env, " ")), + VariablesReference: refs.New(func() []dap.Variable { + vars := make([]dap.Variable, 0, len(exec.Meta.Env)) + for _, envstr := range exec.Meta.Env { + parts := strings.SplitN(envstr, "=", 2) + vars = append(vars, dap.Variable{ + Name: parts[0], + Value: parts[1], + }) + } + return vars + }), + }, + } + + if exec.Meta.Cwd != "" { + vars = append(vars, dap.Variable{ + Name: "workdir", + Value: exec.Meta.Cwd, + }) + } + + if exec.Meta.User != "" { + vars = append(vars, dap.Variable{ + Name: "user", + Value: exec.Meta.User, + }) + } + return vars + }), + } +} + +func (f *frame) Scopes() []dap.Scope { + return f.scopes +} + +type variableReferences struct { + refs map[int32]func() []dap.Variable + nextID atomic.Int32 + mask int32 + + mu sync.RWMutex +} + +func newVariableReferences() *variableReferences { + v := new(variableReferences) + v.Reset() + return v +} + +func (v *variableReferences) New(fn func() []dap.Variable) int { + v.mu.Lock() + defer v.mu.Unlock() + + id := v.nextID.Add(1) | v.mask + v.refs[id] = sync.OnceValue(fn) + return int(id) +} + +func (v *variableReferences) Get(id int) []dap.Variable { + v.mu.RLock() + fn := v.refs[int32(id)] + v.mu.RUnlock() + + var vars []dap.Variable + if fn != nil { + vars = fn() + } + + if vars == nil { + vars = []dap.Variable{} + } + return vars +} + +func (v *variableReferences) Reset() { + v.mu.Lock() + defer v.mu.Unlock() + + v.refs = make(map[int32]func() []dap.Variable) + v.nextID.Store(0) +} + +func brief(s string) string { + if len(s) >= 64 { + return s[:60] + " ..." + } + return s +}