Skip to content

Commit 5c27294

Browse files
authored
Merge pull request #3327 from jsternberg/dap-fs-inspect
dap: filesystem inspection when paused on a digest
2 parents 2690ddd + 8e356c3 commit 5c27294

File tree

2 files changed

+214
-22
lines changed

2 files changed

+214
-22
lines changed

dap/thread.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type thread struct {
5050
mu sync.Mutex
5151

5252
// Attributes set when a thread is paused.
53+
cancel context.CancelCauseFunc
5354
rCtx *build.ResultHandle
5455
curPos digest.Digest
5556
stackTrace []int32
@@ -264,7 +265,10 @@ func (t *thread) pause(c Context, ref gateway.Reference, err error, pos *step, e
264265
}
265266
}
266267
}
267-
t.collectStackTrace(pos)
268+
269+
ctx, cancel := context.WithCancelCause(c)
270+
t.collectStackTrace(ctx, pos, ref)
271+
t.cancel = cancel
268272

269273
event.ThreadId = t.id
270274
c.C() <- &dap.StoppedEvent{
@@ -490,20 +494,27 @@ func (t *thread) solve(ctx context.Context, target digest.Digest) (gateway.Refer
490494
}
491495

492496
func (t *thread) releaseState() {
497+
if t.cancel != nil {
498+
t.cancel(context.Canceled)
499+
t.cancel = nil
500+
}
493501
if t.rCtx != nil {
494502
t.rCtx.Done()
495503
t.rCtx = nil
496504
}
505+
for _, f := range t.frames {
506+
f.ResetVars()
507+
}
497508
t.stackTrace = t.stackTrace[:0]
498509
t.variables.Reset()
499510
}
500511

501-
func (t *thread) collectStackTrace(pos *step) {
512+
func (t *thread) collectStackTrace(ctx context.Context, pos *step, ref gateway.Reference) {
502513
for pos != nil {
503514
frame := pos.frame
504-
frame.ExportVars(t.variables)
515+
frame.ExportVars(ctx, ref, t.variables)
505516
t.stackTrace = append(t.stackTrace, int32(frame.Id))
506-
pos = pos.out
517+
pos, ref = pos.out, nil
507518
}
508519
}
509520

dap/variables.go

Lines changed: 199 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
package dap
22

33
import (
4+
"context"
45
"fmt"
6+
"io/fs"
57
"path/filepath"
68
"strconv"
79
"strings"
810
"sync"
911
"sync/atomic"
12+
"time"
13+
"unicode/utf8"
1014

1115
"github.com/google/go-dap"
1216
"github.com/moby/buildkit/client/llb"
17+
gateway "github.com/moby/buildkit/frontend/gateway/client"
1318
"github.com/moby/buildkit/solver/pb"
19+
"github.com/tonistiigi/fsutil/types"
1420
)
1521

1622
type frame struct {
@@ -45,29 +51,34 @@ func (f *frame) fillLocation(def *llb.Definition, loc *pb.Locations, ws string)
4551
}
4652
}
4753

48-
func (f *frame) ExportVars(refs *variableReferences) {
54+
func (f *frame) ExportVars(ctx context.Context, ref gateway.Reference, refs *variableReferences) {
4955
f.fillVarsFromOp(f.op, refs)
56+
if ref != nil {
57+
f.fillVarsFromResult(ctx, ref, refs)
58+
}
59+
}
60+
61+
func (f *frame) ResetVars() {
62+
f.scopes = nil
5063
}
5164

5265
func (f *frame) fillVarsFromOp(op *pb.Op, refs *variableReferences) {
53-
f.scopes = []dap.Scope{
54-
{
55-
Name: "Arguments",
56-
PresentationHint: "arguments",
57-
VariablesReference: refs.New(func() []dap.Variable {
58-
var vars []dap.Variable
59-
if op.Platform != nil {
60-
vars = append(vars, platformVars(op.Platform, refs))
61-
}
66+
f.scopes = append(f.scopes, dap.Scope{
67+
Name: "Arguments",
68+
PresentationHint: "arguments",
69+
VariablesReference: refs.New(func() []dap.Variable {
70+
var vars []dap.Variable
71+
if op.Platform != nil {
72+
vars = append(vars, platformVars(op.Platform, refs))
73+
}
6274

63-
switch op := op.Op.(type) {
64-
case *pb.Op_Exec:
65-
vars = append(vars, execOpVars(op.Exec, refs))
66-
}
67-
return vars
68-
}),
69-
},
70-
}
75+
switch op := op.Op.(type) {
76+
case *pb.Op_Exec:
77+
vars = append(vars, execOpVars(op.Exec, refs))
78+
}
79+
return vars
80+
}),
81+
})
7182
}
7283

7384
func platformVars(platform *pb.Platform, refs *variableReferences) dap.Variable {
@@ -159,6 +170,148 @@ func execOpVars(exec *pb.ExecOp, refs *variableReferences) dap.Variable {
159170
}
160171
}
161172

173+
func (f *frame) fillVarsFromResult(ctx context.Context, ref gateway.Reference, refs *variableReferences) {
174+
f.scopes = append(f.scopes, dap.Scope{
175+
Name: "File Explorer",
176+
PresentationHint: "locals",
177+
VariablesReference: refs.New(func() []dap.Variable {
178+
return fsVars(ctx, ref, "/", refs)
179+
}),
180+
Expensive: true,
181+
})
182+
}
183+
184+
func fsVars(ctx context.Context, ref gateway.Reference, path string, vars *variableReferences) []dap.Variable {
185+
files, err := ref.ReadDir(ctx, gateway.ReadDirRequest{
186+
Path: path,
187+
})
188+
if err != nil {
189+
return []dap.Variable{
190+
{
191+
Name: "error",
192+
Value: err.Error(),
193+
},
194+
}
195+
}
196+
197+
paths := make([]dap.Variable, len(files))
198+
for i, file := range files {
199+
stat := statf(file)
200+
fv := dap.Variable{
201+
Name: file.Path,
202+
}
203+
204+
fullpath := filepath.Join(path, file.Path)
205+
if file.IsDir() {
206+
fv.Name += "/"
207+
fv.VariablesReference = vars.New(func() []dap.Variable {
208+
dvar := dap.Variable{
209+
Name: ".",
210+
Value: statf(file),
211+
VariablesReference: vars.New(func() []dap.Variable {
212+
return statVars(file)
213+
}),
214+
}
215+
return append([]dap.Variable{dvar}, fsVars(ctx, ref, fullpath, vars)...)
216+
})
217+
fv.Value = ""
218+
} else {
219+
fv.Value = stat
220+
fv.VariablesReference = vars.New(func() (dvars []dap.Variable) {
221+
if fs.FileMode(file.Mode).IsRegular() {
222+
// Regular file so display a small blurb of the file.
223+
dvars = append(dvars, fileVars(ctx, ref, fullpath)...)
224+
}
225+
return append(dvars, statVars(file)...)
226+
})
227+
}
228+
paths[i] = fv
229+
}
230+
return paths
231+
}
232+
233+
func statf(st *types.Stat) string {
234+
mode := fs.FileMode(st.Mode)
235+
modTime := time.Unix(0, st.ModTime).UTC()
236+
return fmt.Sprintf("%s %d:%d %s", mode, st.Uid, st.Gid, modTime.Format("Jan 2 15:04:05 2006"))
237+
}
238+
239+
func fileVars(ctx context.Context, ref gateway.Reference, fullpath string) []dap.Variable {
240+
b, err := ref.ReadFile(ctx, gateway.ReadRequest{
241+
Filename: fullpath,
242+
Range: &gateway.FileRange{Length: 512},
243+
})
244+
245+
var (
246+
data string
247+
dataErr error
248+
)
249+
if err != nil {
250+
data = err.Error()
251+
} else if isBinaryData(b) {
252+
data = "binary data"
253+
} else {
254+
if len(b) == 512 {
255+
// Get the remainder of the file.
256+
remaining, err := ref.ReadFile(ctx, gateway.ReadRequest{
257+
Filename: fullpath,
258+
Range: &gateway.FileRange{Offset: 512},
259+
})
260+
if err != nil {
261+
dataErr = err
262+
} else {
263+
b = append(b, remaining...)
264+
}
265+
}
266+
data = string(b)
267+
}
268+
269+
dvars := []dap.Variable{
270+
{
271+
Name: "data",
272+
Value: data,
273+
},
274+
}
275+
if dataErr != nil {
276+
dvars = append(dvars, dap.Variable{
277+
Name: "dataError",
278+
Value: dataErr.Error(),
279+
})
280+
}
281+
return dvars
282+
}
283+
284+
func statVars(st *types.Stat) (vars []dap.Variable) {
285+
if st.Linkname != "" {
286+
vars = append(vars, dap.Variable{
287+
Name: "linkname",
288+
Value: st.Linkname,
289+
})
290+
}
291+
292+
mode := fs.FileMode(st.Mode)
293+
modTime := time.Unix(0, st.ModTime).UTC()
294+
vars = append(vars, []dap.Variable{
295+
{
296+
Name: "mode",
297+
Value: mode.String(),
298+
},
299+
{
300+
Name: "uid",
301+
Value: strconv.FormatUint(uint64(st.Uid), 10),
302+
},
303+
{
304+
Name: "gid",
305+
Value: strconv.FormatUint(uint64(st.Gid), 10),
306+
},
307+
{
308+
Name: "mtime",
309+
Value: modTime.Format("Jan 2 15:04:05 2006"),
310+
},
311+
}...)
312+
return vars
313+
}
314+
162315
func (f *frame) Scopes() []dap.Scope {
163316
if f.scopes == nil {
164317
return []dap.Scope{}
@@ -213,6 +366,34 @@ func (v *variableReferences) Reset() {
213366
v.nextID.Store(0)
214367
}
215368

369+
// isBinaryData uses heuristics to determine if the file
370+
// is binary. Algorithm taken from this blog post:
371+
// https://eli.thegreenplace.net/2011/10/19/perls-guess-if-file-is-text-or-binary-implemented-in-python/
372+
func isBinaryData(b []byte) bool {
373+
odd := 0
374+
for i := 0; i < len(b); i++ {
375+
c := b[i]
376+
if c == 0 {
377+
return true
378+
}
379+
380+
isHighBit := c&128 > 0
381+
if !isHighBit {
382+
if c < 32 && c != '\n' && c != '\t' {
383+
odd++
384+
}
385+
} else {
386+
r, sz := utf8.DecodeRune(b)
387+
if r != utf8.RuneError && sz > 1 {
388+
i += sz - 1
389+
continue
390+
}
391+
odd++
392+
}
393+
}
394+
return float64(odd)/float64(len(b)) > .3
395+
}
396+
216397
func brief(s string) string {
217398
if len(s) >= 64 {
218399
return s[:60] + " ..."

0 commit comments

Comments
 (0)