|
1 | 1 | package dap |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
4 | 5 | "fmt" |
| 6 | + "io/fs" |
5 | 7 | "path/filepath" |
6 | 8 | "strconv" |
7 | 9 | "strings" |
8 | 10 | "sync" |
9 | 11 | "sync/atomic" |
| 12 | + "time" |
| 13 | + "unicode/utf8" |
10 | 14 |
|
11 | 15 | "github.com/google/go-dap" |
12 | 16 | "github.com/moby/buildkit/client/llb" |
| 17 | + gateway "github.com/moby/buildkit/frontend/gateway/client" |
13 | 18 | "github.com/moby/buildkit/solver/pb" |
| 19 | + "github.com/tonistiigi/fsutil/types" |
14 | 20 | ) |
15 | 21 |
|
16 | 22 | type frame struct { |
@@ -45,29 +51,34 @@ func (f *frame) fillLocation(def *llb.Definition, loc *pb.Locations, ws string) |
45 | 51 | } |
46 | 52 | } |
47 | 53 |
|
48 | | -func (f *frame) ExportVars(refs *variableReferences) { |
| 54 | +func (f *frame) ExportVars(ctx context.Context, ref gateway.Reference, refs *variableReferences) { |
49 | 55 | f.fillVarsFromOp(f.op, refs) |
| 56 | + if ref != nil { |
| 57 | + f.fillVarsFromResult(ctx, ref, refs) |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +func (f *frame) ResetVars() { |
| 62 | + f.scopes = nil |
50 | 63 | } |
51 | 64 |
|
52 | 65 | func (f *frame) fillVarsFromOp(op *pb.Op, refs *variableReferences) { |
53 | | - f.scopes = []dap.Scope{ |
54 | | - { |
55 | | - Name: "Arguments", |
56 | | - PresentationHint: "arguments", |
57 | | - VariablesReference: refs.New(func() []dap.Variable { |
58 | | - var vars []dap.Variable |
59 | | - if op.Platform != nil { |
60 | | - vars = append(vars, platformVars(op.Platform, refs)) |
61 | | - } |
| 66 | + f.scopes = append(f.scopes, dap.Scope{ |
| 67 | + Name: "Arguments", |
| 68 | + PresentationHint: "arguments", |
| 69 | + VariablesReference: refs.New(func() []dap.Variable { |
| 70 | + var vars []dap.Variable |
| 71 | + if op.Platform != nil { |
| 72 | + vars = append(vars, platformVars(op.Platform, refs)) |
| 73 | + } |
62 | 74 |
|
63 | | - switch op := op.Op.(type) { |
64 | | - case *pb.Op_Exec: |
65 | | - vars = append(vars, execOpVars(op.Exec, refs)) |
66 | | - } |
67 | | - return vars |
68 | | - }), |
69 | | - }, |
70 | | - } |
| 75 | + switch op := op.Op.(type) { |
| 76 | + case *pb.Op_Exec: |
| 77 | + vars = append(vars, execOpVars(op.Exec, refs)) |
| 78 | + } |
| 79 | + return vars |
| 80 | + }), |
| 81 | + }) |
71 | 82 | } |
72 | 83 |
|
73 | 84 | func platformVars(platform *pb.Platform, refs *variableReferences) dap.Variable { |
@@ -159,6 +170,148 @@ func execOpVars(exec *pb.ExecOp, refs *variableReferences) dap.Variable { |
159 | 170 | } |
160 | 171 | } |
161 | 172 |
|
| 173 | +func (f *frame) fillVarsFromResult(ctx context.Context, ref gateway.Reference, refs *variableReferences) { |
| 174 | + f.scopes = append(f.scopes, dap.Scope{ |
| 175 | + Name: "File Explorer", |
| 176 | + PresentationHint: "locals", |
| 177 | + VariablesReference: refs.New(func() []dap.Variable { |
| 178 | + return fsVars(ctx, ref, "/", refs) |
| 179 | + }), |
| 180 | + Expensive: true, |
| 181 | + }) |
| 182 | +} |
| 183 | + |
| 184 | +func fsVars(ctx context.Context, ref gateway.Reference, path string, vars *variableReferences) []dap.Variable { |
| 185 | + files, err := ref.ReadDir(ctx, gateway.ReadDirRequest{ |
| 186 | + Path: path, |
| 187 | + }) |
| 188 | + if err != nil { |
| 189 | + return []dap.Variable{ |
| 190 | + { |
| 191 | + Name: "error", |
| 192 | + Value: err.Error(), |
| 193 | + }, |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + paths := make([]dap.Variable, len(files)) |
| 198 | + for i, file := range files { |
| 199 | + stat := statf(file) |
| 200 | + fv := dap.Variable{ |
| 201 | + Name: file.Path, |
| 202 | + } |
| 203 | + |
| 204 | + fullpath := filepath.Join(path, file.Path) |
| 205 | + if file.IsDir() { |
| 206 | + fv.Name += "/" |
| 207 | + fv.VariablesReference = vars.New(func() []dap.Variable { |
| 208 | + dvar := dap.Variable{ |
| 209 | + Name: ".", |
| 210 | + Value: statf(file), |
| 211 | + VariablesReference: vars.New(func() []dap.Variable { |
| 212 | + return statVars(file) |
| 213 | + }), |
| 214 | + } |
| 215 | + return append([]dap.Variable{dvar}, fsVars(ctx, ref, fullpath, vars)...) |
| 216 | + }) |
| 217 | + fv.Value = "" |
| 218 | + } else { |
| 219 | + fv.Value = stat |
| 220 | + fv.VariablesReference = vars.New(func() (dvars []dap.Variable) { |
| 221 | + if fs.FileMode(file.Mode).IsRegular() { |
| 222 | + // Regular file so display a small blurb of the file. |
| 223 | + dvars = append(dvars, fileVars(ctx, ref, fullpath)...) |
| 224 | + } |
| 225 | + return append(dvars, statVars(file)...) |
| 226 | + }) |
| 227 | + } |
| 228 | + paths[i] = fv |
| 229 | + } |
| 230 | + return paths |
| 231 | +} |
| 232 | + |
| 233 | +func statf(st *types.Stat) string { |
| 234 | + mode := fs.FileMode(st.Mode) |
| 235 | + modTime := time.Unix(0, st.ModTime).UTC() |
| 236 | + return fmt.Sprintf("%s %d:%d %s", mode, st.Uid, st.Gid, modTime.Format("Jan 2 15:04:05 2006")) |
| 237 | +} |
| 238 | + |
| 239 | +func fileVars(ctx context.Context, ref gateway.Reference, fullpath string) []dap.Variable { |
| 240 | + b, err := ref.ReadFile(ctx, gateway.ReadRequest{ |
| 241 | + Filename: fullpath, |
| 242 | + Range: &gateway.FileRange{Length: 512}, |
| 243 | + }) |
| 244 | + |
| 245 | + var ( |
| 246 | + data string |
| 247 | + dataErr error |
| 248 | + ) |
| 249 | + if err != nil { |
| 250 | + data = err.Error() |
| 251 | + } else if isBinaryData(b) { |
| 252 | + data = "binary data" |
| 253 | + } else { |
| 254 | + if len(b) == 512 { |
| 255 | + // Get the remainder of the file. |
| 256 | + remaining, err := ref.ReadFile(ctx, gateway.ReadRequest{ |
| 257 | + Filename: fullpath, |
| 258 | + Range: &gateway.FileRange{Offset: 512}, |
| 259 | + }) |
| 260 | + if err != nil { |
| 261 | + dataErr = err |
| 262 | + } else { |
| 263 | + b = append(b, remaining...) |
| 264 | + } |
| 265 | + } |
| 266 | + data = string(b) |
| 267 | + } |
| 268 | + |
| 269 | + dvars := []dap.Variable{ |
| 270 | + { |
| 271 | + Name: "data", |
| 272 | + Value: data, |
| 273 | + }, |
| 274 | + } |
| 275 | + if dataErr != nil { |
| 276 | + dvars = append(dvars, dap.Variable{ |
| 277 | + Name: "dataError", |
| 278 | + Value: dataErr.Error(), |
| 279 | + }) |
| 280 | + } |
| 281 | + return dvars |
| 282 | +} |
| 283 | + |
| 284 | +func statVars(st *types.Stat) (vars []dap.Variable) { |
| 285 | + if st.Linkname != "" { |
| 286 | + vars = append(vars, dap.Variable{ |
| 287 | + Name: "linkname", |
| 288 | + Value: st.Linkname, |
| 289 | + }) |
| 290 | + } |
| 291 | + |
| 292 | + mode := fs.FileMode(st.Mode) |
| 293 | + modTime := time.Unix(0, st.ModTime).UTC() |
| 294 | + vars = append(vars, []dap.Variable{ |
| 295 | + { |
| 296 | + Name: "mode", |
| 297 | + Value: mode.String(), |
| 298 | + }, |
| 299 | + { |
| 300 | + Name: "uid", |
| 301 | + Value: strconv.FormatUint(uint64(st.Uid), 10), |
| 302 | + }, |
| 303 | + { |
| 304 | + Name: "gid", |
| 305 | + Value: strconv.FormatUint(uint64(st.Gid), 10), |
| 306 | + }, |
| 307 | + { |
| 308 | + Name: "mtime", |
| 309 | + Value: modTime.Format("Jan 2 15:04:05 2006"), |
| 310 | + }, |
| 311 | + }...) |
| 312 | + return vars |
| 313 | +} |
| 314 | + |
162 | 315 | func (f *frame) Scopes() []dap.Scope { |
163 | 316 | if f.scopes == nil { |
164 | 317 | return []dap.Scope{} |
@@ -213,6 +366,34 @@ func (v *variableReferences) Reset() { |
213 | 366 | v.nextID.Store(0) |
214 | 367 | } |
215 | 368 |
|
| 369 | +// isBinaryData uses heuristics to determine if the file |
| 370 | +// is binary. Algorithm taken from this blog post: |
| 371 | +// https://eli.thegreenplace.net/2011/10/19/perls-guess-if-file-is-text-or-binary-implemented-in-python/ |
| 372 | +func isBinaryData(b []byte) bool { |
| 373 | + odd := 0 |
| 374 | + for i := 0; i < len(b); i++ { |
| 375 | + c := b[i] |
| 376 | + if c == 0 { |
| 377 | + return true |
| 378 | + } |
| 379 | + |
| 380 | + isHighBit := c&128 > 0 |
| 381 | + if !isHighBit { |
| 382 | + if c < 32 && c != '\n' && c != '\t' { |
| 383 | + odd++ |
| 384 | + } |
| 385 | + } else { |
| 386 | + r, sz := utf8.DecodeRune(b) |
| 387 | + if r != utf8.RuneError && sz > 1 { |
| 388 | + i += sz - 1 |
| 389 | + continue |
| 390 | + } |
| 391 | + odd++ |
| 392 | + } |
| 393 | + } |
| 394 | + return float64(odd)/float64(len(b)) > .3 |
| 395 | +} |
| 396 | + |
216 | 397 | func brief(s string) string { |
217 | 398 | if len(s) >= 64 { |
218 | 399 | return s[:60] + " ..." |
|
0 commit comments