Merge pull request #3307 from jsternberg/dap-variables

dap: implement variable references
This commit is contained in:
Tõnis Tiigi
2025-07-14 09:54:48 -07:00
committed by GitHub
5 changed files with 318 additions and 53 deletions
+49
View File
@@ -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,
}
}
+2
View File
@@ -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]
}
+4
View File
@@ -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:
+50 -53
View File
@@ -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 {
+213
View File
@@ -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
}