When the builder loads a dockerfile, it does it by using the base name of the dockerfile path and only loads the innermost directory. This means that the source name we're looking for is the base name and not the full relative path. Update the set breakpoints functionality so it takes this into account. Fixes scenarios where DAP is used with a dockerfile nested in the context. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
405 lines
8.5 KiB
Go
405 lines
8.5 KiB
Go
package dap
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/google/go-dap"
|
|
"github.com/moby/buildkit/client/llb"
|
|
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/tonistiigi/fsutil/types"
|
|
)
|
|
|
|
type frame struct {
|
|
dap.StackFrame
|
|
op *pb.Op
|
|
scopes []dap.Scope
|
|
}
|
|
|
|
func (f *frame) setNameFromMeta(meta llb.OpMetadata) {
|
|
if name, ok := meta.Description["llb.customname"]; ok {
|
|
f.Name = name
|
|
} else if cmd, ok := meta.Description["com.docker.dockerfile.v1.command"]; ok {
|
|
f.Name = cmd
|
|
}
|
|
// TODO: should we infer the name from somewhere else?
|
|
}
|
|
|
|
func (f *frame) fillLocation(def *llb.Definition, loc *pb.Locations, ws string) {
|
|
for _, l := range loc.Locations {
|
|
for _, r := range l.Ranges {
|
|
f.Line = int(r.Start.Line)
|
|
f.Column = int(r.Start.Character)
|
|
f.EndLine = int(r.End.Line)
|
|
f.EndColumn = int(r.End.Character)
|
|
|
|
info := def.Source.Infos[l.SourceIndex]
|
|
f.Source = &dap.Source{
|
|
Name: path.Base(info.Filename),
|
|
Path: filepath.Join(ws, info.Filename),
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (f *frame) ExportVars(ctx context.Context, ref gateway.Reference, refs *variableReferences) {
|
|
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) {
|
|
f.scopes = append(f.scopes, dap.Scope{
|
|
Name: "Arguments",
|
|
PresentationHint: "arguments",
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
var vars []dap.Variable
|
|
if op.Platform != nil {
|
|
vars = append(vars, platformVars(op.Platform, refs))
|
|
}
|
|
|
|
switch op := op.Op.(type) {
|
|
case *pb.Op_Exec:
|
|
vars = append(vars, execOpVars(op.Exec, refs))
|
|
}
|
|
return vars
|
|
}),
|
|
})
|
|
}
|
|
|
|
func platformVars(platform *pb.Platform, refs *variableReferences) dap.Variable {
|
|
return dap.Variable{
|
|
Name: "platform",
|
|
Value: fmt.Sprintf("%s/%s", platform.OS, platform.Architecture),
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
vars := []dap.Variable{
|
|
{
|
|
Name: "architecture",
|
|
Value: platform.Architecture,
|
|
},
|
|
{
|
|
Name: "os",
|
|
Value: platform.OS,
|
|
},
|
|
}
|
|
|
|
if platform.Variant != "" {
|
|
vars = append(vars, dap.Variable{
|
|
Name: "variant",
|
|
Value: platform.Variant,
|
|
})
|
|
}
|
|
|
|
if platform.OSVersion != "" {
|
|
vars = append(vars, dap.Variable{
|
|
Name: "osversion",
|
|
Value: platform.OSVersion,
|
|
})
|
|
}
|
|
return vars
|
|
}),
|
|
}
|
|
}
|
|
|
|
func execOpVars(exec *pb.ExecOp, refs *variableReferences) dap.Variable {
|
|
return dap.Variable{
|
|
Name: "exec",
|
|
Value: strings.Join(exec.Meta.Args, " "),
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
vars := []dap.Variable{
|
|
{
|
|
Name: "args",
|
|
Value: brief(strings.Join(exec.Meta.Args, " ")),
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
vars := make([]dap.Variable, 0, len(exec.Meta.Args))
|
|
for i, arg := range exec.Meta.Args {
|
|
vars = append(vars, dap.Variable{
|
|
Name: strconv.Itoa(i),
|
|
Value: arg,
|
|
})
|
|
}
|
|
return vars
|
|
}),
|
|
},
|
|
{
|
|
Name: "env",
|
|
Value: brief(strings.Join(exec.Meta.Env, " ")),
|
|
VariablesReference: refs.New(func() []dap.Variable {
|
|
vars := make([]dap.Variable, 0, len(exec.Meta.Env))
|
|
for _, envstr := range exec.Meta.Env {
|
|
parts := strings.SplitN(envstr, "=", 2)
|
|
vars = append(vars, dap.Variable{
|
|
Name: parts[0],
|
|
Value: parts[1],
|
|
})
|
|
}
|
|
return vars
|
|
}),
|
|
},
|
|
}
|
|
|
|
if exec.Meta.Cwd != "" {
|
|
vars = append(vars, dap.Variable{
|
|
Name: "workdir",
|
|
Value: exec.Meta.Cwd,
|
|
})
|
|
}
|
|
|
|
if exec.Meta.User != "" {
|
|
vars = append(vars, dap.Variable{
|
|
Name: "user",
|
|
Value: exec.Meta.User,
|
|
})
|
|
}
|
|
return vars
|
|
}),
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if f.scopes == nil {
|
|
return []dap.Scope{}
|
|
}
|
|
return f.scopes
|
|
}
|
|
|
|
type variableReferences struct {
|
|
refs map[int32]func() []dap.Variable
|
|
nextID atomic.Int32
|
|
mask int32
|
|
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func newVariableReferences() *variableReferences {
|
|
v := new(variableReferences)
|
|
v.Reset()
|
|
return v
|
|
}
|
|
|
|
func (v *variableReferences) New(fn func() []dap.Variable) int {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
id := v.nextID.Add(1) | v.mask
|
|
v.refs[id] = sync.OnceValue(fn)
|
|
return int(id)
|
|
}
|
|
|
|
func (v *variableReferences) Get(id int) []dap.Variable {
|
|
v.mu.RLock()
|
|
fn := v.refs[int32(id)]
|
|
v.mu.RUnlock()
|
|
|
|
var vars []dap.Variable
|
|
if fn != nil {
|
|
vars = fn()
|
|
}
|
|
|
|
if vars == nil {
|
|
vars = []dap.Variable{}
|
|
}
|
|
return vars
|
|
}
|
|
|
|
func (v *variableReferences) Reset() {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
v.refs = make(map[int32]func() []dap.Variable)
|
|
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 {
|
|
if len(s) >= 64 {
|
|
return s[:60] + " ..."
|
|
}
|
|
return s
|
|
}
|