buildx/dap/variables.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
}