From 0dddf0a7b8aa840acbde0502d1e4687daf164be0 Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Mon, 14 Jul 2025 09:19:17 -0500 Subject: [PATCH] dap: implement first pass at breakpoints Implement the first iteration of breakpoints. When a breakpoint is set, it starts unverified. When a thread begins evaluation, it tries to see if a breakpoint corresponds to one of the parsed instructions and will verify it. Breakpoints work when continue is used. At the current moment, setting breakpoints while a thread is currently running doesn't work. Breakpoints are rechecked each time execution is about to restart. Signed-off-by: Jonathan A. Sternberg --- dap/adapter.go | 130 +++++++++++++++++++++++++++++++++++++++++++------ dap/thread.go | 98 ++++++++++++++++++++++++++----------- 2 files changed, 186 insertions(+), 42 deletions(-) diff --git a/dap/adapter.go b/dap/adapter.go index ce5991ba0..d45384f01 100644 --- a/dap/adapter.go +++ b/dap/adapter.go @@ -7,6 +7,8 @@ import ( "fmt" "io" "path" + "path/filepath" + "slices" "sync" "sync/atomic" @@ -14,6 +16,8 @@ import ( "github.com/docker/buildx/dap/common" "github.com/google/go-dap" gateway "github.com/moby/buildkit/frontend/gateway/client" + "github.com/moby/buildkit/solver/pb" + "github.com/opencontainers/go-digest" "github.com/pkg/errors" "golang.org/x/sync/errgroup" ) @@ -33,8 +37,9 @@ type Adapter[C LaunchConfig] struct { threadsMu sync.RWMutex nextThreadID int - sourceMap sourceMap - idPool *idPool + breakpointMap *breakpointMap + sourceMap sourceMap + idPool *idPool } func New[C LaunchConfig]() *Adapter[C] { @@ -45,6 +50,7 @@ func New[C LaunchConfig]() *Adapter[C] { evaluateReqCh: make(chan *evaluateRequest), threads: make(map[int]*thread), nextThreadID: 1, + breakpointMap: newBreakpointMap(), idPool: new(idPool), } d.srv = NewServer(d.dapHandler()) @@ -151,14 +157,7 @@ func (d *Adapter[C]) Next(c Context, req *dap.NextRequest, resp *dap.NextRespons } func (d *Adapter[C]) SetBreakpoints(c Context, req *dap.SetBreakpointsRequest, resp *dap.SetBreakpointsResponse) error { - // TODO: implement breakpoints - for range req.Arguments.Breakpoints { - // Fail to create all breakpoints that were requested. - resp.Body.Breakpoints = append(resp.Body.Breakpoints, dap.Breakpoint{ - Verified: false, - Message: "breakpoints unsupported", - }) - } + resp.Body.Breakpoints = d.breakpointMap.Set(req.Arguments.Source.Path, req.Arguments.Breakpoints) return nil } @@ -212,10 +211,11 @@ func (d *Adapter[C]) newThread(ctx Context, name string) (t *thread) { d.threadsMu.Lock() id := d.nextThreadID t = &thread{ - id: id, - name: name, - sourceMap: &d.sourceMap, - idPool: d.idPool, + id: id, + name: name, + sourceMap: &d.sourceMap, + breakpointMap: d.breakpointMap, + idPool: d.idPool, } d.threads[t.id] = t d.nextThreadID++ @@ -468,3 +468,105 @@ func (s *sourceMap) Get(fname string) ([]byte, bool) { } return v.([]byte), true } + +type breakpointMap struct { + byPath map[string][]dap.Breakpoint + mu sync.RWMutex + + nextID atomic.Int64 +} + +func newBreakpointMap() *breakpointMap { + return &breakpointMap{ + byPath: make(map[string][]dap.Breakpoint), + } +} + +func (b *breakpointMap) Set(fname string, sbps []dap.SourceBreakpoint) (breakpoints []dap.Breakpoint) { + b.mu.Lock() + defer b.mu.Unlock() + + prev := b.byPath[fname] + for _, sbp := range sbps { + index := slices.IndexFunc(prev, func(e dap.Breakpoint) bool { + return sbp.Line >= e.Line && sbp.Line <= e.EndLine && sbp.Column >= e.Column && sbp.Column <= e.EndColumn + }) + + var bp dap.Breakpoint + if index >= 0 { + bp = prev[index] + } else { + bp = dap.Breakpoint{ + Id: int(b.nextID.Add(1)), + Line: sbp.Line, + EndLine: sbp.Line, + Column: sbp.Column, + EndColumn: sbp.Column, + } + } + breakpoints = append(breakpoints, bp) + } + b.byPath[fname] = breakpoints + return breakpoints +} + +func (b *breakpointMap) Intersect(ctx Context, src *pb.Source, ws string) map[digest.Digest]int { + b.mu.Lock() + defer b.mu.Unlock() + + digests := make(map[digest.Digest]int) + + for dgst, locs := range src.Locations { + if id := b.intersect(ctx, src, locs, ws); id > 0 { + digests[digest.Digest(dgst)] = id + } + } + return digests +} + +func (b *breakpointMap) intersect(ctx Context, src *pb.Source, locs *pb.Locations, ws string) int { + overlaps := func(r *pb.Range, bp *dap.Breakpoint) bool { + return r.Start.Line <= int32(bp.Line) && r.Start.Character <= int32(bp.Column) && r.End.Line >= int32(bp.EndLine) && r.End.Character >= int32(bp.EndColumn) + } + + for _, loc := range locs.Locations { + if len(loc.Ranges) == 0 { + continue + } + r := loc.Ranges[0] + + info := src.Infos[loc.SourceIndex] + fname := filepath.Join(ws, info.Filename) + + bps := b.byPath[fname] + if len(bps) == 0 { + // No breakpoints for this file. + continue + } + + for i, bp := range bps { + if !overlaps(r, &bp) { + continue + } + + if !bp.Verified { + bp.Line = int(r.Start.Line) + bp.EndLine = int(r.End.Line) + bp.Column = int(r.Start.Character) + bp.EndColumn = int(r.End.Character) + bp.Verified = true + + ctx.C() <- &dap.BreakpointEvent{ + Event: dap.Event{Event: "breakpoint"}, + Body: dap.BreakpointEventBody{ + Reason: "changed", + Breakpoint: bp, + }, + } + bps[i] = bp + } + return bp.Id + } + } + return 0 +} diff --git a/dap/thread.go b/dap/thread.go index 18ee91f18..887751b59 100644 --- a/dap/thread.go +++ b/dap/thread.go @@ -23,8 +23,9 @@ type thread struct { name string // Persistent state from the adapter. - idPool *idPool - sourceMap *sourceMap + idPool *idPool + sourceMap *sourceMap + breakpointMap *breakpointMap // Inputs to the evaluate call. c gateway.Client @@ -36,6 +37,7 @@ type thread struct { def *llb.Definition ops map[digest.Digest]*pb.Op head digest.Digest + bps map[digest.Digest]int // Runtime state for the evaluate call. regions []*region @@ -80,15 +82,18 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, ref gateway.Reference, } for { + if step == stepContinue { + t.setBreakpoints(ctx) + } pos, err := t.seekNext(ctx, step) - reason, desc := t.needsDebug(pos, step, err) - if reason == "" { + event := t.needsDebug(pos, step, err) + if event.Reason == "" { return err } select { - case step = <-t.pause(ctx, err, reason, desc): + case step = <-t.pause(ctx, err, event): case <-ctx.Done(): return context.Cause(ctx) } @@ -100,7 +105,11 @@ func (t *thread) init(ctx Context, c gateway.Client, ref gateway.Reference, meta t.ref = ref t.meta = meta t.sourcePath = inputs.ContextPath - return t.createRegions(ctx) + + if err := t.getLLBState(ctx); err != nil { + return err + } + return t.createRegions() } func (t *thread) reset() { @@ -111,17 +120,23 @@ func (t *thread) reset() { t.ops = nil } -func (t *thread) needsDebug(target digest.Digest, step stepType, err error) (reason, desc string) { +func (t *thread) needsDebug(target digest.Digest, step stepType, err error) (e dap.StoppedEventBody) { if err != nil { - reason = "exception" - desc = "Encountered an error during result evaluation" - } else if target != "" && step == stepNext { - reason = "step" + e.Reason = "exception" + e.Description = "Encountered an error during result evaluation" + } else if step == stepNext && target != "" { + e.Reason = "step" + } else if step == stepContinue { + if id, ok := t.bps[target]; ok { + e.Reason = "breakpoint" + e.Description = "Paused on breakpoint" + e.HitBreakpointIds = []int{id} + } } return } -func (t *thread) pause(c Context, err error, reason, desc string) <-chan stepType { +func (t *thread) pause(c Context, err error, event dap.StoppedEventBody) <-chan stepType { t.mu.Lock() defer t.mu.Unlock() @@ -140,13 +155,10 @@ func (t *thread) pause(c Context, err error, reason, desc string) <-chan stepTyp } } + event.ThreadId = t.id c.C() <- &dap.StoppedEvent{ Event: dap.Event{Event: "stopped"}, - Body: dap.StoppedEventBody{ - Reason: reason, - Description: desc, - ThreadId: t.id, - }, + Body: event, } return t.paused } @@ -232,6 +244,10 @@ func (t *thread) getLLBState(ctx Context) error { return err } +func (t *thread) setBreakpoints(ctx Context) { + t.bps = t.breakpointMap.Intersect(ctx, t.def.Source, t.sourcePath) +} + func (t *thread) findBacklinks() map[digest.Digest]map[digest.Digest]struct{} { backlinks := make(map[digest.Digest]map[digest.Digest]struct{}) for dgst := range t.ops { @@ -249,11 +265,7 @@ func (t *thread) findBacklinks() map[digest.Digest]map[digest.Digest]struct{} { return backlinks } -func (t *thread) createRegions(ctx Context) error { - if err := t.getLLBState(ctx); err != nil { - return err - } - +func (t *thread) createRegions() error { // Find the links going from inputs to their outputs. // This isn't represented in the LLB graph but we need it to ensure // an op only has one child and whether we are allowed to visit a node. @@ -363,8 +375,11 @@ func (t *thread) seekNext(ctx Context, step stepType) (digest.Digest, error) { } target := t.head - if step == stepNext { - target = t.nextDigest() + switch step { + case stepNext: + target = t.nextDigest(nil) + case stepContinue: + target = t.continueDigest() } if target == "" { @@ -394,11 +409,27 @@ func (t *thread) seek(ctx Context, target digest.Digest) (digest.Digest, error) return t.curPos, err } -func (t *thread) nextDigest() digest.Digest { +func (t *thread) nextDigest(fn func(digest.Digest) bool) digest.Digest { + isValid := func(dgst digest.Digest) bool { + // Skip this digest because it has no locations in the source file. + if loc, ok := t.def.Source.Locations[string(dgst)]; !ok || len(loc.Locations) == 0 { + return false + } + + // If a custom function has been set for validation, use it. + return fn == nil || fn(dgst) + } + // If we have no position, automatically select the first step. if t.curPos == "" { r := t.regions[len(t.regions)-1] - return r.digests[0] + if isValid(r.digests[0]) { + return r.digests[0] + } + + // We cannot use the first position. Treat the first position as our + // current position so we can iterate. + t.curPos = r.digests[0] } // Look up the region associated with our current position. @@ -426,8 +457,7 @@ func (t *thread) nextDigest() digest.Digest { } next := r.digests[i] - if loc, ok := t.def.Source.Locations[string(next)]; !ok || len(loc.Locations) == 0 { - // Skip this digest because it has no locations in the source file. + if !isValid(next) { i++ continue } @@ -435,6 +465,18 @@ func (t *thread) nextDigest() digest.Digest { } } +func (t *thread) continueDigest() digest.Digest { + if len(t.bps) == 0 { + return t.head + } + + isValid := func(dgst digest.Digest) bool { + _, ok := t.bps[dgst] + return ok + } + return t.nextDigest(isValid) +} + func (t *thread) solve(ctx context.Context, target digest.Digest) (gateway.Reference, error) { if target == t.head { return t.ref, nil