mirror of https://github.com/docker/buildx.git
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
|
|
}
|