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 }