This improves location resolution for duplicate digests by making some assumptions about the file structure to determine a "best match" when there are multiple possible locations. It uses the next operation (that this digest is the input for) to determine a location. If the location of the current operation is before the next operation, this location is preferred. If there are multiple locations that happen before the next operation, the one closest to the next operation (aka later in the file) is used instead. This resolves the most common case of multiple identical `FROM` statements without adding more code to the frontends themselves. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
439 lines
9.5 KiB
Go
439 lines
9.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, next *step) {
|
|
for _, l := range loc.Locations {
|
|
for _, r := range l.Ranges {
|
|
if next != nil && f.Line != 0 {
|
|
// We have location information. See if the new location
|
|
// information matches with our location better.
|
|
if !betterLocation(r, f, next) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
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),
|
|
}
|
|
|
|
// If we do not have a next operation, then we don't have
|
|
// any information to make a determination about the "best" fit
|
|
// that happens at the beginning of this section. Exit early.
|
|
if next == nil {
|
|
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
|
|
}
|
|
|
|
func betterLocation(r *pb.Range, f *frame, next *step) bool {
|
|
// Ideal guess is one that is before the next frame.
|
|
if int(r.Start.Line) <= next.frame.Line {
|
|
// And is later than our current guess.
|
|
if int(r.Start.Line) > f.Line {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// We're after the next frame so this is a bad guess.
|
|
// Was our original one even worse?
|
|
if int(r.Start.Line) < f.Line {
|
|
// Yes it was. We'll consider this a better location.
|
|
return true
|
|
}
|
|
|
|
// Doesn't seem to be a better location.
|
|
return false
|
|
}
|