Merge pull request #3327 from jsternberg/dap-fs-inspect
dap: filesystem inspection when paused on a digest
This commit is contained in:
+15
-4
@@ -50,6 +50,7 @@ type thread struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
// Attributes set when a thread is paused.
|
// Attributes set when a thread is paused.
|
||||||
|
cancel context.CancelCauseFunc
|
||||||
rCtx *build.ResultHandle
|
rCtx *build.ResultHandle
|
||||||
curPos digest.Digest
|
curPos digest.Digest
|
||||||
stackTrace []int32
|
stackTrace []int32
|
||||||
@@ -264,7 +265,10 @@ func (t *thread) pause(c Context, ref gateway.Reference, err error, pos *step, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.collectStackTrace(pos)
|
|
||||||
|
ctx, cancel := context.WithCancelCause(c)
|
||||||
|
t.collectStackTrace(ctx, pos, ref)
|
||||||
|
t.cancel = cancel
|
||||||
|
|
||||||
event.ThreadId = t.id
|
event.ThreadId = t.id
|
||||||
c.C() <- &dap.StoppedEvent{
|
c.C() <- &dap.StoppedEvent{
|
||||||
@@ -490,20 +494,27 @@ func (t *thread) solve(ctx context.Context, target digest.Digest) (gateway.Refer
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *thread) releaseState() {
|
func (t *thread) releaseState() {
|
||||||
|
if t.cancel != nil {
|
||||||
|
t.cancel(context.Canceled)
|
||||||
|
t.cancel = nil
|
||||||
|
}
|
||||||
if t.rCtx != nil {
|
if t.rCtx != nil {
|
||||||
t.rCtx.Done()
|
t.rCtx.Done()
|
||||||
t.rCtx = nil
|
t.rCtx = nil
|
||||||
}
|
}
|
||||||
|
for _, f := range t.frames {
|
||||||
|
f.ResetVars()
|
||||||
|
}
|
||||||
t.stackTrace = t.stackTrace[:0]
|
t.stackTrace = t.stackTrace[:0]
|
||||||
t.variables.Reset()
|
t.variables.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *thread) collectStackTrace(pos *step) {
|
func (t *thread) collectStackTrace(ctx context.Context, pos *step, ref gateway.Reference) {
|
||||||
for pos != nil {
|
for pos != nil {
|
||||||
frame := pos.frame
|
frame := pos.frame
|
||||||
frame.ExportVars(t.variables)
|
frame.ExportVars(ctx, ref, t.variables)
|
||||||
t.stackTrace = append(t.stackTrace, int32(frame.Id))
|
t.stackTrace = append(t.stackTrace, int32(frame.Id))
|
||||||
pos = pos.out
|
pos, ref = pos.out, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+199
-18
@@ -1,16 +1,22 @@
|
|||||||
package dap
|
package dap
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/google/go-dap"
|
"github.com/google/go-dap"
|
||||||
"github.com/moby/buildkit/client/llb"
|
"github.com/moby/buildkit/client/llb"
|
||||||
|
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
||||||
"github.com/moby/buildkit/solver/pb"
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
"github.com/tonistiigi/fsutil/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type frame struct {
|
type frame struct {
|
||||||
@@ -45,29 +51,34 @@ func (f *frame) fillLocation(def *llb.Definition, loc *pb.Locations, ws string)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *frame) ExportVars(refs *variableReferences) {
|
func (f *frame) ExportVars(ctx context.Context, ref gateway.Reference, refs *variableReferences) {
|
||||||
f.fillVarsFromOp(f.op, refs)
|
f.fillVarsFromOp(f.op, refs)
|
||||||
|
if ref != nil {
|
||||||
|
f.fillVarsFromResult(ctx, ref, refs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *frame) ResetVars() {
|
||||||
|
f.scopes = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *frame) fillVarsFromOp(op *pb.Op, refs *variableReferences) {
|
func (f *frame) fillVarsFromOp(op *pb.Op, refs *variableReferences) {
|
||||||
f.scopes = []dap.Scope{
|
f.scopes = append(f.scopes, dap.Scope{
|
||||||
{
|
Name: "Arguments",
|
||||||
Name: "Arguments",
|
PresentationHint: "arguments",
|
||||||
PresentationHint: "arguments",
|
VariablesReference: refs.New(func() []dap.Variable {
|
||||||
VariablesReference: refs.New(func() []dap.Variable {
|
var vars []dap.Variable
|
||||||
var vars []dap.Variable
|
if op.Platform != nil {
|
||||||
if op.Platform != nil {
|
vars = append(vars, platformVars(op.Platform, refs))
|
||||||
vars = append(vars, platformVars(op.Platform, refs))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
switch op := op.Op.(type) {
|
switch op := op.Op.(type) {
|
||||||
case *pb.Op_Exec:
|
case *pb.Op_Exec:
|
||||||
vars = append(vars, execOpVars(op.Exec, refs))
|
vars = append(vars, execOpVars(op.Exec, refs))
|
||||||
}
|
}
|
||||||
return vars
|
return vars
|
||||||
}),
|
}),
|
||||||
},
|
})
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func platformVars(platform *pb.Platform, refs *variableReferences) dap.Variable {
|
func platformVars(platform *pb.Platform, refs *variableReferences) dap.Variable {
|
||||||
@@ -159,6 +170,148 @@ func execOpVars(exec *pb.ExecOp, refs *variableReferences) dap.Variable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *frame) fillVarsFromResult(ctx context.Context, ref 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)
|
||||||
|
}),
|
||||||
|
Expensive: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func fsVars(ctx context.Context, ref gateway.Reference, path string, vars *variableReferences) []dap.Variable {
|
||||||
|
files, err := ref.ReadDir(ctx, gateway.ReadDirRequest{
|
||||||
|
Path: path,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return []dap.Variable{
|
||||||
|
{
|
||||||
|
Name: "error",
|
||||||
|
Value: err.Error(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := make([]dap.Variable, len(files))
|
||||||
|
for i, file := range files {
|
||||||
|
stat := statf(file)
|
||||||
|
fv := dap.Variable{
|
||||||
|
Name: file.Path,
|
||||||
|
}
|
||||||
|
|
||||||
|
fullpath := filepath.Join(path, file.Path)
|
||||||
|
if file.IsDir() {
|
||||||
|
fv.Name += "/"
|
||||||
|
fv.VariablesReference = vars.New(func() []dap.Variable {
|
||||||
|
dvar := dap.Variable{
|
||||||
|
Name: ".",
|
||||||
|
Value: statf(file),
|
||||||
|
VariablesReference: vars.New(func() []dap.Variable {
|
||||||
|
return statVars(file)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
return append([]dap.Variable{dvar}, fsVars(ctx, ref, fullpath, vars)...)
|
||||||
|
})
|
||||||
|
fv.Value = ""
|
||||||
|
} else {
|
||||||
|
fv.Value = stat
|
||||||
|
fv.VariablesReference = vars.New(func() (dvars []dap.Variable) {
|
||||||
|
if fs.FileMode(file.Mode).IsRegular() {
|
||||||
|
// Regular file so display a small blurb of the file.
|
||||||
|
dvars = append(dvars, fileVars(ctx, ref, fullpath)...)
|
||||||
|
}
|
||||||
|
return append(dvars, statVars(file)...)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
paths[i] = fv
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
func statf(st *types.Stat) string {
|
||||||
|
mode := fs.FileMode(st.Mode)
|
||||||
|
modTime := time.Unix(0, st.ModTime).UTC()
|
||||||
|
return fmt.Sprintf("%s %d:%d %s", mode, st.Uid, st.Gid, modTime.Format("Jan 2 15:04:05 2006"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileVars(ctx context.Context, ref gateway.Reference, fullpath string) []dap.Variable {
|
||||||
|
b, err := ref.ReadFile(ctx, gateway.ReadRequest{
|
||||||
|
Filename: fullpath,
|
||||||
|
Range: &gateway.FileRange{Length: 512},
|
||||||
|
})
|
||||||
|
|
||||||
|
var (
|
||||||
|
data string
|
||||||
|
dataErr error
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
data = err.Error()
|
||||||
|
} else if isBinaryData(b) {
|
||||||
|
data = "binary data"
|
||||||
|
} else {
|
||||||
|
if len(b) == 512 {
|
||||||
|
// Get the remainder of the file.
|
||||||
|
remaining, err := ref.ReadFile(ctx, gateway.ReadRequest{
|
||||||
|
Filename: fullpath,
|
||||||
|
Range: &gateway.FileRange{Offset: 512},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
dataErr = err
|
||||||
|
} else {
|
||||||
|
b = append(b, remaining...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
dvars := []dap.Variable{
|
||||||
|
{
|
||||||
|
Name: "data",
|
||||||
|
Value: data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if dataErr != nil {
|
||||||
|
dvars = append(dvars, dap.Variable{
|
||||||
|
Name: "dataError",
|
||||||
|
Value: dataErr.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return dvars
|
||||||
|
}
|
||||||
|
|
||||||
|
func statVars(st *types.Stat) (vars []dap.Variable) {
|
||||||
|
if st.Linkname != "" {
|
||||||
|
vars = append(vars, dap.Variable{
|
||||||
|
Name: "linkname",
|
||||||
|
Value: st.Linkname,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := fs.FileMode(st.Mode)
|
||||||
|
modTime := time.Unix(0, st.ModTime).UTC()
|
||||||
|
vars = append(vars, []dap.Variable{
|
||||||
|
{
|
||||||
|
Name: "mode",
|
||||||
|
Value: mode.String(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "uid",
|
||||||
|
Value: strconv.FormatUint(uint64(st.Uid), 10),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "gid",
|
||||||
|
Value: strconv.FormatUint(uint64(st.Gid), 10),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "mtime",
|
||||||
|
Value: modTime.Format("Jan 2 15:04:05 2006"),
|
||||||
|
},
|
||||||
|
}...)
|
||||||
|
return vars
|
||||||
|
}
|
||||||
|
|
||||||
func (f *frame) Scopes() []dap.Scope {
|
func (f *frame) Scopes() []dap.Scope {
|
||||||
if f.scopes == nil {
|
if f.scopes == nil {
|
||||||
return []dap.Scope{}
|
return []dap.Scope{}
|
||||||
@@ -213,6 +366,34 @@ func (v *variableReferences) Reset() {
|
|||||||
v.nextID.Store(0)
|
v.nextID.Store(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBinaryData uses heuristics to determine if the file
|
||||||
|
// is binary. Algorithm taken from this blog post:
|
||||||
|
// https://eli.thegreenplace.net/2011/10/19/perls-guess-if-file-is-text-or-binary-implemented-in-python/
|
||||||
|
func isBinaryData(b []byte) bool {
|
||||||
|
odd := 0
|
||||||
|
for i := 0; i < len(b); i++ {
|
||||||
|
c := b[i]
|
||||||
|
if c == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
isHighBit := c&128 > 0
|
||||||
|
if !isHighBit {
|
||||||
|
if c < 32 && c != '\n' && c != '\t' {
|
||||||
|
odd++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
r, sz := utf8.DecodeRune(b)
|
||||||
|
if r != utf8.RuneError && sz > 1 {
|
||||||
|
i += sz - 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
odd++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return float64(odd)/float64(len(b)) > .3
|
||||||
|
}
|
||||||
|
|
||||||
func brief(s string) string {
|
func brief(s string) string {
|
||||||
if len(s) >= 64 {
|
if len(s) >= 64 {
|
||||||
return s[:60] + " ..."
|
return s[:60] + " ..."
|
||||||
|
|||||||
Reference in New Issue
Block a user