From 1886e232c5008d9a6345da752d17d67bdad17cfa Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Mon, 14 Jul 2025 10:29:54 -0500 Subject: [PATCH] dap: implement variable references Implement variable references to inspect the state of a stack frame. Variable reference ids are composed of two sections. A thread mask that is the first 8 bytes and the remainder is an increasing number that gets reset each time a thread is resumed. This allows the adapter to know which thread to delegate the variables request to and allows the variable references to still remain confined to each thread. An int32 is used for this because variable references need to be in the range of (0, 2^32). At the moment, only the platform variables and some of the exec operations for an operation. These are labeled as "arguments" to the stack frame. Signed-off-by: Jonathan A. Sternberg --- dap/adapter.go | 49 +++++++++++ dap/handler.go | 2 + dap/server.go | 4 + dap/thread.go | 103 +++++++++++------------ dap/variables.go | 213 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 318 insertions(+), 53 deletions(-) create mode 100644 dap/variables.go 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 +}