dap: collect references for inputs to ensure the file explorer works on an error

The debug adapter will now look at the error and collect the input
mounts from the exec operation and translate them to a gateway reference
to be able to access the filesystem in the file explorer.

When we are attempting to find the real operation that failed and
succeed, we now re-solve the dependencies (which should already have
been solved correctly to begin with) so that we can have the proper
reference and can access the filesystem contents.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
This commit is contained in:
Jonathan A. Sternberg
2025-09-17 15:58:28 -05:00
parent e540b7af14
commit 17c2e1dc35
2 changed files with 94 additions and 55 deletions
+70 -48
View File
@@ -49,7 +49,6 @@ type thread struct {
// Attributes set when a thread is paused.
cancel context.CancelCauseFunc // invoked when the thread is resumed
rCtx *build.ResultHandle
curPos digest.Digest
stackTrace []int32
}
@@ -74,15 +73,16 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen
}
var (
ref gateway.Reference
next = t.entrypoint
err error
ref gateway.Reference
mounts 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, next, event):
case action = <-t.pause(ctx, ref, err, mounts, next, event):
// do nothing here
case <-ctx.Done():
return context.Cause(ctx)
@@ -96,7 +96,7 @@ func (t *thread) Evaluate(ctx Context, c gateway.Client, headRef gateway.Referen
if action == stepContinue {
t.setBreakpoints(ctx)
}
ref, next, err = t.seekNext(ctx, next, action)
ref, next, mounts, err = t.seekNext(ctx, next, action)
}
return nil
}
@@ -285,26 +285,17 @@ func (t *thread) needsDebug(cur *step, step stepType, err error) (e dap.StoppedE
return
}
func (t *thread) pause(c Context, ref gateway.Reference, err error, pos *step, event dap.StoppedEventBody) <-chan stepType {
func (t *thread) pause(c Context, ref gateway.Reference, err error, mounts map[string]gateway.Reference, pos *step, event dap.StoppedEventBody) <-chan stepType {
t.mu.Lock()
defer t.mu.Unlock()
if t.paused != nil {
return t.paused
}
t.paused = make(chan stepType, 1)
if err != nil {
var solveErr *errdefs.SolveError
if errors.As(err, &solveErr) {
if dt, err := solveErr.Op.MarshalVT(); err == nil {
t.curPos = digest.FromBytes(dt)
}
}
}
ctx, cancel := context.WithCancelCause(c)
t.collectStackTrace(ctx, pos, ref)
t.collectStackTrace(ctx, pos, mounts)
t.cancel = cancel
if ref != nil || err != nil {
@@ -435,7 +426,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, error) {
func (t *thread) seekNext(ctx Context, from *step, action stepType) (gateway.Reference, *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
@@ -452,12 +443,12 @@ 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, err error) {
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)
ref, err = t.solve(ctx, target.dgst, 0)
if err != nil {
return ref, nil, err
return ref, nil, nil, err
}
}
@@ -468,31 +459,15 @@ func (t *thread) seek(ctx Context, target *step) (ref gateway.Reference, result
if ref != nil {
if err = ref.Evaluate(ctx); err != nil {
// If this is not a solve error, do not return the
// reference and target step.
var solveErr *errdefs.SolveError
if errors.As(err, &solveErr) {
if dt, err := solveErr.Op.MarshalVT(); err == nil {
// Find the error digest.
errDgst := digest.FromBytes(dt)
// 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
}
}
} else {
return nil, nil, err
result, mounts = t.rewind(ctx, err)
if result == nil {
return nil, nil, nil, err
}
} else {
mounts = map[string]gateway.Reference{"/": ref}
}
}
return ref, result, err
return ref, result, mounts, err
}
func (t *thread) continueDigest(from *step) *step {
@@ -523,13 +498,15 @@ func (t *thread) continueDigest(from *step) *step {
return next(from)
}
func (t *thread) solve(ctx context.Context, target digest.Digest) (gateway.Reference, error) {
func (t *thread) solve(ctx context.Context, target digest.Digest, index int64) (gateway.Reference, error) {
if target == t.head {
return t.ref, nil
}
head := &pb.Op{
Inputs: []*pb.Input{{Digest: string(target)}},
Inputs: []*pb.Input{
{Digest: string(target), Index: index},
},
}
dt, err := head.MarshalVT()
if err != nil {
@@ -567,12 +544,12 @@ func (t *thread) releaseState() {
t.variables.Reset()
}
func (t *thread) collectStackTrace(ctx context.Context, pos *step, ref gateway.Reference) {
func (t *thread) collectStackTrace(ctx context.Context, pos *step, mounts map[string]gateway.Reference) {
for pos != nil {
frame := pos.frame
frame.ExportVars(ctx, ref, t.variables)
frame.ExportVars(ctx, mounts, t.variables)
t.stackTrace = append(t.stackTrace, int32(frame.Id))
pos, ref = pos.out, nil
pos, mounts = pos.out, nil
}
}
@@ -587,3 +564,48 @@ func (t *thread) hasFrame(id int) bool {
_, ok := t.frames[int32(id)]
return ok
}
func (t *thread) rewind(ctx context.Context, err error) (result *step, mounts map[string]gateway.Reference) {
var solveErr *errdefs.SolveError
if !errors.As(err, &solveErr) {
// If this is not a solve error, do not return the
// reference and target step.
return nil, nil
}
dt, err := solveErr.Op.MarshalVT()
if err != nil {
return nil, nil
}
// Find the error digest.
errDgst := digest.FromBytes(dt)
// 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
}
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
}
}
}
return result, mounts
}
+24 -7
View File
@@ -67,10 +67,10 @@ func (f *frame) fillLocation(def *llb.Definition, loc *pb.Locations, ws string,
}
}
func (f *frame) ExportVars(ctx context.Context, ref gateway.Reference, refs *variableReferences) {
func (f *frame) ExportVars(ctx context.Context, mounts map[string]gateway.Reference, refs *variableReferences) {
f.fillVarsFromOp(f.op, refs)
if ref != nil {
f.fillVarsFromResult(ctx, ref, refs)
if len(mounts) > 0 {
f.fillVarsFromResult(ctx, mounts, refs)
}
}
@@ -186,18 +186,23 @@ func execOpVars(exec *pb.ExecOp, refs *variableReferences) dap.Variable {
}
}
func (f *frame) fillVarsFromResult(ctx context.Context, ref gateway.Reference, refs *variableReferences) {
func (f *frame) fillVarsFromResult(ctx context.Context, mounts map[string]gateway.Reference, refs *variableReferences) {
f.scopes = append(f.scopes, dap.Scope{
Name: "File Explorer",
PresentationHint: "locals",
VariablesReference: refs.New(func() []dap.Variable {
return fsVars(ctx, ref, "/", refs)
return fsVars(ctx, mounts, "/", refs)
}),
Expensive: true,
})
}
func fsVars(ctx context.Context, ref gateway.Reference, path string, vars *variableReferences) []dap.Variable {
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
}
files, err := ref.ReadDir(ctx, gateway.ReadDirRequest{
Path: path,
})
@@ -228,7 +233,7 @@ func fsVars(ctx context.Context, ref gateway.Reference, path string, vars *varia
return statVars(file)
}),
}
return append([]dap.Variable{dvar}, fsVars(ctx, ref, fullpath, vars)...)
return append([]dap.Variable{dvar}, fsVars(ctx, mounts, fullpath, vars)...)
})
fv.Value = ""
} else {
@@ -436,3 +441,15 @@ 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, p)
ref = r
}
}
return "/" + remainder, ref
}