diff --git a/dap/thread.go b/dap/thread.go index 42a65c8f1..47efbe2ba 100644 --- a/dap/thread.go +++ b/dap/thread.go @@ -15,6 +15,7 @@ import ( "github.com/moby/buildkit/solver/pb" "github.com/opencontainers/go-digest" "github.com/pkg/errors" + "golang.org/x/sync/errgroup" ) type thread struct { @@ -73,16 +74,16 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen } var ( - ref gateway.Reference - mounts map[string]gateway.Reference - next = t.entrypoint - err error + k string + refs map[string]gateway.Reference + next = t.entrypoint + err error ) for next != nil { event := t.needsDebug(next, action, err) if event.Reason != "" { select { - case action = <-t.pause(ctx, ref, err, mounts, next, event): + case action = <-t.pause(ctx, k, refs, err, next, event): // do nothing here case <-ctx.Done(): return context.Cause(ctx) @@ -96,7 +97,7 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen if action == stepContinue { t.setBreakpoints(ctx) } - ref, next, mounts, err = t.seekNext(ctx, next, action) + k, next, refs, err = t.seekNext(ctx, next, action) } return nil } @@ -121,8 +122,8 @@ func (t *thread) init(ctx Context, c gateway.Client, ref gateway.Reference, meta } type step struct { - // dgst holds the digest that should be resolved by this step. - // If this is empty, no digest should be resolved. + // dgst holds the digest associated with this step. This is used for + // breakpoint resolution. dgst digest.Digest // in holds the next target when step in is used. @@ -136,6 +137,9 @@ type step struct { // frame will hold the stack frame associated with this step. frame *frame + + // parent holds the index of the parent step. + parent int } func (t *thread) createProgram() error { @@ -143,71 +147,99 @@ func (t *thread) createProgram() error { // Create the entrypoint by using the last node. // We will build on top of that. - head := &step{ - dgst: t.head, - frame: t.getStackFrame(t.head, nil), - } - t.entrypoint = t.createBranch(head) + t.entrypoint = t.createBranch(t.head, nil) return nil } -func (t *thread) createBranch(last *step) (first *step) { - first = last - for first.dgst != "" { - prev := &step{ - // set to first temporarily until we determine - // if there are other inputs. - in: first, - // always first - next: first, - // exit point always matches the one set on first - out: first.out, - // always set to the same as next which is always first - frame: t.getStackFrame(first.dgst, first), +func (t *thread) createBranch(dgst digest.Digest, exitpoint *step) (entrypoint *step) { + // Construct the final two steps in this branch. The final steps + // both point to the same line. The difference between them is one + // step is before the execution of the digest and the other is + // after the execution of that digest. + returnpoint := &step{ + in: exitpoint, + next: exitpoint, + out: exitpoint, + parent: -1, + } + + entrypoint = &step{ + dgst: dgst, + in: returnpoint, + next: returnpoint, + out: exitpoint, + frame: t.getStackFrame(dgst, nil), + parent: -1, + } + + // Create a pseudo-frame and attach it to the return point. + // This is mostly used for getting the correct inputs utilized + // by this frame. + // + // We don't save this frame or assign it a unique ID as it should + // never be returned. + returnpoint.frame = &frame{ + StackFrame: entrypoint.frame.StackFrame, + op: &pb.Op{ + Inputs: []*pb.Input{ + {Digest: string(dgst), Index: 0}, + }, + }, + } + + for { + // Construct the input step for this digest based on the inputs. + op := t.ops[entrypoint.dgst] + if len(op.Inputs) == 0 { + return entrypoint } - op := t.ops[first.dgst] - if len(op.Inputs) > 0 { - parent := t.determineParent(op) - for i := len(op.Inputs) - 1; i >= 0; i-- { - if i == parent { - // Skip the direct parent. - continue - } - inp := op.Inputs[i] - - // Create a pseudo-step that acts as an exit point for this - // branch. This step exists so this branch has a place to go - // after it has finished that will advance to the next - // instruction. - exit := &step{ - in: prev.in, - next: prev.next, - out: prev.out, - frame: prev.frame, - } - - head := &step{ - dgst: digest.Digest(inp.Digest), - in: exit, - next: exit, - out: exit, - frame: t.getStackFrame(digest.Digest(inp.Digest), nil), - } - prev.in = t.createBranch(head) + entrypoint.parent = t.determineParent(op) + for i := len(op.Inputs) - 1; i >= 0; i-- { + if i == entrypoint.parent { + // Skip the direct parent. + continue } + + // When we find inputs that aren't the direct parent, + // we want to add them as a step before the current step. + // We have to do a few things when inserting this. + // + // 1. We move the digest from the old entrypoint to this node. + // This is so the breakpoint happens before these inputs + // are evaluated. + // 2. We keep the next/out pointers the same but redirect in + // to point to the new branch. + // 3. The direct parent is excluded from this logic. We handle + // that later. + inp := op.Inputs[i] + + head := *entrypoint + entrypoint.dgst = "" - // Set the digest of the parent input on the first step associated - // with this step if it exists. - if parent >= 0 { - prev.dgst = digest.Digest(op.Inputs[parent].Digest) - } + // Create the routine associated with this input. + // Associate it with the entrypoint in step. + head.in = t.createBranch(digest.Digest(inp.Digest), entrypoint) + entrypoint = &head + } + + // If we have no direct parent, return the current entrypoint + // as the beginning. + if entrypoint.parent < 0 { + return entrypoint } - // New first is the step we just created. - first = prev + // Create a new step that refers to the direct parent. + head := &step{ + dgst: digest.Digest(op.Inputs[entrypoint.parent].Digest), + in: entrypoint, + next: entrypoint, + out: entrypoint.out, + parent: -1, + } + head.frame = t.getStackFrame(head.dgst, entrypoint) + entrypoint = head } - return first } func (t *thread) getStackFrame(dgst digest.Digest, next *step) *frame { @@ -274,18 +306,16 @@ func (t *thread) needsDebug(cur *step, step stepType, err error) (e dap.StoppedE } else if cur != nil { if step != stepContinue { e.Reason = "step" - } else if next := cur.in; next != nil { - if id, ok := t.bps[next.dgst]; ok { - e.Reason = "breakpoint" - e.Description = "Paused on breakpoint" - e.HitBreakpointIds = []int{id} - } + } else if id, ok := t.bps[cur.dgst]; ok { + e.Reason = "breakpoint" + e.Description = "Paused on breakpoint" + e.HitBreakpointIds = []int{id} } } return } -func (t *thread) pause(c Context, ref gateway.Reference, err error, mounts map[string]gateway.Reference, pos *step, event dap.StoppedEventBody) <-chan stepType { +func (t *thread) pause(c Context, k string, refs map[string]gateway.Reference, err error, pos *step, event dap.StoppedEventBody) <-chan stepType { t.mu.Lock() defer t.mu.Unlock() @@ -295,10 +325,12 @@ func (t *thread) pause(c Context, ref gateway.Reference, err error, mounts map[s t.paused = make(chan stepType, 1) ctx, cancel := context.WithCancelCause(c) - t.collectStackTrace(ctx, pos, mounts) + t.collectStackTrace(ctx, pos, refs) t.cancel = cancel - if ref != nil || err != nil { + // Used for exec. Only works if there was an error or if the step returns + // a root mount. + if ref, ok := refs[k]; ok || err != nil { t.prepareResultHandle(c, ref, err) } @@ -412,7 +444,7 @@ func (t *thread) getLLBState(ctx Context) error { dgst := digest.FromBytes(dt) var op pb.Op - if err := op.UnmarshalVT(dt); err != nil { + if err := op.Unmarshal(dt); err != nil { return err } t.ops[dgst] = &op @@ -426,7 +458,7 @@ func (t *thread) setBreakpoints(ctx Context) { t.bps = t.breakpointMap.Intersect(ctx, t.def.Source, t.sourcePath) } -func (t *thread) seekNext(ctx Context, from *step, action stepType) (gateway.Reference, *step, map[string]gateway.Reference, error) { +func (t *thread) seekNext(ctx Context, from *step, action stepType) (string, *step, map[string]gateway.Reference, error) { // If we're at the end, return no digest to signal that // we should conclude debugging. var target *step @@ -443,31 +475,26 @@ func (t *thread) seekNext(ctx Context, from *step, action stepType) (gateway.Ref return t.seek(ctx, target) } -func (t *thread) seek(ctx Context, target *step) (ref gateway.Reference, result *step, mounts map[string]gateway.Reference, err error) { - if target != nil { - if target.dgst != "" { - ref, err = t.solve(ctx, target.dgst, 0) - if err != nil { - return ref, nil, nil, err - } - } +func (t *thread) seek(ctx Context, target *step) (k string, result *step, mounts map[string]gateway.Reference, err error) { + k = "/" + var refs map[string]gateway.Reference + if target != nil { + k, refs, err = t.solveInputs(ctx, target) + if err != nil { + return "", nil, nil, err + } result = target } else { - ref = t.ref + refs = map[string]gateway.Reference{"/": t.ref} } - if ref != nil { - if err = ref.Evaluate(ctx); err != nil { - result, mounts = t.rewind(ctx, err) - if result == nil { - return nil, nil, nil, err - } - } else { - mounts = map[string]gateway.Reference{"/": ref} + if len(refs) > 0 { + if err := t.evaluateRefs(ctx, refs); err != nil { + return t.rewind(ctx, err) } } - return ref, result, mounts, err + return k, result, refs, nil } func (t *thread) continueDigest(from *step) *step { @@ -487,28 +514,74 @@ func (t *thread) continueDigest(from *step) *step { next := func(s *step) *step { cur := s.in for cur != nil { - next := cur.in - if next != nil && isBreakpoint(next.dgst) { + if isBreakpoint(cur.dgst) { return cur } - cur = next + cur = cur.in } return nil } return next(from) } -func (t *thread) solve(ctx context.Context, target digest.Digest, index int64) (gateway.Reference, error) { - if target == t.head { +func (t *thread) solveInputs(ctx context.Context, target *step) (string, map[string]gateway.Reference, error) { + if target == nil || target.frame.op == nil { + return "", nil, nil + } + op := target.frame.op + + var root string + refs := make(map[string]gateway.Reference) + for i, input := range op.Inputs { + k := t.determineInputName(op, input) + if _, ok := refs[k]; ok || k == "" { + continue + } + + if i == target.parent { + root = k + } + + ref, err := t.solve(ctx, input) + if err != nil { + return "", nil, err + } + refs[k] = ref + } + return root, refs, nil +} + +func (t *thread) determineInputName(op *pb.Op, input *pb.Input) string { + switch op := op.Op.(type) { + case *pb.Op_Exec: + for _, m := range op.Exec.Mounts { + if m.Input >= 0 && m.Input == input.Index { + return m.Dest + } + } + } + return input.Digest +} + +func (t *thread) evaluateRefs(ctx context.Context, refs map[string]gateway.Reference) error { + eg, _ := errgroup.WithContext(ctx) + for _, ref := range refs { + eg.Go(func() error { + return ref.Evaluate(ctx) + }) + } + return eg.Wait() +} + +func (t *thread) solve(ctx context.Context, input *pb.Input) (gateway.Reference, error) { + if input.Digest == string(t.head) { return t.ref, nil } head := &pb.Op{ - Inputs: []*pb.Input{ - {Digest: string(target), Index: index}, - }, + Inputs: []*pb.Input{input}, } - dt, err := head.MarshalVT() + dt, err := head.Marshal() if err != nil { return nil, err } @@ -565,47 +638,37 @@ func (t *thread) hasFrame(id int) bool { return ok } -func (t *thread) rewind(ctx context.Context, err error) (result *step, mounts map[string]gateway.Reference) { +func (t *thread) rewind(ctx Context, inErr error) (k string, result *step, mounts map[string]gateway.Reference, retErr error) { var solveErr *errdefs.SolveError - if !errors.As(err, &solveErr) { + if !errors.As(inErr, &solveErr) { // If this is not a solve error, do not return the // reference and target step. - return nil, nil + return "", nil, nil, inErr } - dt, err := solveErr.Op.MarshalVT() + dt, err := solveErr.Op.Marshal() if err != nil { - return nil, nil + return "", nil, nil, err } // Find the error digest. errDgst := digest.FromBytes(dt) - // Iterate from the first step to find the one - // we failed on. + // Iterate from the first step to find the one we failed on. result = t.entrypoint - for result != nil { - next := result.in - if next != nil && next.dgst == errDgst { - break - } - result = next + for result != nil && result.dgst != errDgst { + result = result.in } - if exec, ok := solveErr.Op.GetOp().(*pb.Op_Exec); ok { - mounts = make(map[string]gateway.Reference, len(exec.Exec.Mounts)) - for _, m := range exec.Exec.Mounts { - if m.Input < 0 { - continue - } - - // TODO: Use the correct input index for this. - input := solveErr.Op.Inputs[m.Input] - target, index := digest.Digest(input.Digest), input.Index - if ref, err := t.solve(ctx, target, index); err == nil { - mounts[m.Dest] = ref - } - } + if result == nil { + return "", nil, nil, inErr } - return result, mounts + + // Seek to this step. This should succeed because otherwise + // we wouldn't have been able to even fail on it to begin with. + k, result, mounts, err = t.seek(ctx, result) + if err != nil { + return k, result, mounts, err + } + return k, result, mounts, inErr } diff --git a/dap/variables.go b/dap/variables.go index 69959a307..8c19857ab 100644 --- a/dap/variables.go +++ b/dap/variables.go @@ -4,8 +4,11 @@ import ( "context" "fmt" "io/fs" + "maps" "path" "path/filepath" + "slices" + "sort" "strconv" "strings" "sync" @@ -191,18 +194,29 @@ func (f *frame) fillVarsFromResult(ctx context.Context, mounts map[string]gatewa Name: "File Explorer", PresentationHint: "locals", VariablesReference: refs.New(func() []dap.Variable { - return fsVars(ctx, mounts, "/", refs) + return fileExplorer(ctx, mounts, refs) }), Expensive: true, }) } -func fsVars(ctx context.Context, mounts map[string]gateway.Reference, path string, vars *variableReferences) []dap.Variable { - path, ref := lookupPath(path, mounts) - if ref == nil { - return nil - } +func fileExplorer(ctx context.Context, mounts map[string]gateway.Reference, vars *variableReferences) []dap.Variable { + keys := slices.Collect(maps.Keys(mounts)) + sort.Strings(keys) + outVars := make([]dap.Variable, 0, len(keys)) + for _, k := range keys { + outVars = append(outVars, dap.Variable{ + Name: k, + VariablesReference: vars.New(func() []dap.Variable { + return fsVars(ctx, mounts[k], "/", vars) + }), + }) + } + return outVars +} + +func fsVars(ctx context.Context, ref gateway.Reference, path string, vars *variableReferences) []dap.Variable { files, err := ref.ReadDir(ctx, gateway.ReadDirRequest{ Path: path, }) @@ -233,7 +247,7 @@ func fsVars(ctx context.Context, mounts map[string]gateway.Reference, path strin return statVars(file) }), } - return append([]dap.Variable{dvar}, fsVars(ctx, mounts, fullpath, vars)...) + return append([]dap.Variable{dvar}, fsVars(ctx, ref, fullpath, vars)...) }) fv.Value = "" } else { @@ -441,15 +455,3 @@ func betterLocation(r *pb.Range, f *frame, next *step) bool { // Doesn't seem to be a better location. return false } - -func lookupPath(path string, mounts map[string]gateway.Reference) (remainder string, ref gateway.Reference) { - var prefix string - for p, r := range mounts { - if len(p) > len(prefix) && strings.HasPrefix(path, p) { - prefix = p - remainder, _ = filepath.Rel(prefix, path) - ref = r - } - } - return "/" + remainder, ref -}