monitor: support step-by-step breakpoint debugger
This commit adds a set of commands to monitor for enabling breakpoint debugger. This is implemented based on the walker utility for step-by-step LLB inspection. For each vertex and breakpoint, monitor calls Solve API so the user can enter to the debugger container on each vertex for inspection. User can enter to the breakpoint debugger mode by --invoke=debug-step flag. Signed-off-by: Kohei Tokunaga <ktokunaga.mail@gmail.com>pull/1656/head
parent
b1ae24df65
commit
bcf21dee44
@ -0,0 +1,43 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/buildx/monitor/types"
|
||||
"github.com/docker/buildx/util/walker"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type BreakCmd struct {
|
||||
m types.Monitor
|
||||
}
|
||||
|
||||
func NewBreakCmd(m types.Monitor) types.Command {
|
||||
return &BreakCmd{m}
|
||||
}
|
||||
|
||||
func (cm *BreakCmd) Info() types.CommandInfo {
|
||||
return types.CommandInfo{
|
||||
Name: "break",
|
||||
HelpMessage: "sets a breakpoint",
|
||||
HelpMessageLong: `
|
||||
Usage:
|
||||
break LINE
|
||||
|
||||
LINE is a line number to set a breakpoint.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *BreakCmd) Exec(ctx context.Context, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return errors.Errorf("break: specify line")
|
||||
}
|
||||
line, err := strconv.ParseInt(args[1], 10, 64)
|
||||
if err != nil {
|
||||
return errors.Errorf("break: invalid line number: %q: %v", args[1], err)
|
||||
}
|
||||
cm.m.GetWalkerController().Breakpoints().Add("", walker.NewLineBreakpoint(line))
|
||||
return nil
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/buildx/monitor/types"
|
||||
"github.com/docker/buildx/util/walker"
|
||||
)
|
||||
|
||||
type BreakpointsCmd struct {
|
||||
m types.Monitor
|
||||
}
|
||||
|
||||
func NewBreakpointsCmd(m types.Monitor) types.Command {
|
||||
return &BreakpointsCmd{m}
|
||||
}
|
||||
|
||||
func (cm *BreakpointsCmd) Info() types.CommandInfo {
|
||||
return types.CommandInfo{
|
||||
Name: "breakpoints",
|
||||
HelpMessage: "lists registered breakpoints",
|
||||
HelpMessageLong: `
|
||||
Usage:
|
||||
breakpoints
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *BreakpointsCmd) Exec(ctx context.Context, args []string) error {
|
||||
cm.m.GetWalkerController().Breakpoints().ForEach(func(key string, bp walker.Breakpoint) bool {
|
||||
fmt.Printf("%s %s\n", key, bp.String())
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/buildx/monitor/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ClearCmd struct {
|
||||
m types.Monitor
|
||||
}
|
||||
|
||||
func NewClearCmd(m types.Monitor) types.Command {
|
||||
return &ClearCmd{m}
|
||||
}
|
||||
|
||||
func (cm *ClearCmd) Info() types.CommandInfo {
|
||||
return types.CommandInfo{
|
||||
Name: "clear",
|
||||
HelpMessage: "clears a breakpoint",
|
||||
HelpMessageLong: `
|
||||
Usage:
|
||||
clear KEY
|
||||
|
||||
KEY is the name of the breakpoint.
|
||||
Use "breakpoints" command to list keys of the breakpoints.
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ClearCmd) Exec(ctx context.Context, args []string) error {
|
||||
if len(args) < 2 {
|
||||
return errors.Errorf("clear: specify breakpoint key")
|
||||
}
|
||||
cm.m.GetWalkerController().Breakpoints().Clear(args[1])
|
||||
return nil
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/buildx/monitor/types"
|
||||
"github.com/docker/buildx/monitor/utils"
|
||||
)
|
||||
|
||||
type ClearallCmd struct {
|
||||
m types.Monitor
|
||||
}
|
||||
|
||||
func NewClearallCmd(m types.Monitor) types.Command {
|
||||
return &ClearallCmd{m}
|
||||
}
|
||||
|
||||
func (cm *ClearallCmd) Info() types.CommandInfo {
|
||||
return types.CommandInfo{
|
||||
Name: "clearall",
|
||||
HelpMessage: "clears all breakpoints",
|
||||
HelpMessageLong: `
|
||||
Usage:
|
||||
clearall
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ClearallCmd) Exec(ctx context.Context, args []string) error {
|
||||
utils.SetDefaultBreakpoints(cm.m.GetWalkerController().Breakpoints())
|
||||
return nil
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/buildx/monitor/types"
|
||||
)
|
||||
|
||||
type ContinueCmd struct {
|
||||
m types.Monitor
|
||||
}
|
||||
|
||||
func NewContinueCmd(m types.Monitor) types.Command {
|
||||
return &ContinueCmd{m}
|
||||
}
|
||||
|
||||
func (cm *ContinueCmd) Info() types.CommandInfo {
|
||||
return types.CommandInfo{
|
||||
Name: "continue",
|
||||
HelpMessage: "resumes the build until the next breakpoint",
|
||||
HelpMessageLong: `
|
||||
Usage:
|
||||
continue
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ContinueCmd) Exec(ctx context.Context, args []string) error {
|
||||
wc := cm.m.GetWalkerController()
|
||||
wc.Continue()
|
||||
if (len(args) >= 2 && args[1] == "init") || !wc.IsStarted() {
|
||||
wc.WalkCancel() // Cancel current walking (needed especially for "init" option)
|
||||
if err := wc.StartWalk(); err != nil {
|
||||
fmt.Printf("failed to walk LLB: %v\n", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/buildx/monitor/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type NextCmd struct {
|
||||
m types.Monitor
|
||||
}
|
||||
|
||||
func NewNextCmd(m types.Monitor) types.Command {
|
||||
return &NextCmd{m}
|
||||
}
|
||||
|
||||
func (cm *NextCmd) Info() types.CommandInfo {
|
||||
return types.CommandInfo{
|
||||
Name: "next",
|
||||
HelpMessage: "resumes the build until the next vertex",
|
||||
HelpMessageLong: `
|
||||
Usage:
|
||||
next
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *NextCmd) Exec(ctx context.Context, args []string) error {
|
||||
if err := cm.m.GetWalkerController().Next(); err != nil {
|
||||
return errors.Errorf("next: %s : If walker isn't runnig, might need to run \"continue\" command first", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/docker/buildx/monitor/types"
|
||||
monitorutils "github.com/docker/buildx/monitor/utils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type ShowCmd struct {
|
||||
m types.Monitor
|
||||
stdout io.WriteCloser
|
||||
}
|
||||
|
||||
func NewShowCmd(m types.Monitor, stdout io.WriteCloser) types.Command {
|
||||
return &ShowCmd{m, stdout}
|
||||
}
|
||||
|
||||
func (cm *ShowCmd) Info() types.CommandInfo {
|
||||
return types.CommandInfo{
|
||||
Name: "show",
|
||||
HelpMessage: "shows the debugging Dockerfile with breakpoint information",
|
||||
HelpMessageLong: `
|
||||
Usage:
|
||||
show
|
||||
`,
|
||||
}
|
||||
}
|
||||
|
||||
func (cm *ShowCmd) Exec(ctx context.Context, args []string) error {
|
||||
st := cm.m.GetWalkerController().Inspect()
|
||||
if len(st.Definition.Source.Infos) != 1 {
|
||||
return errors.Errorf("list: multiple sources isn't supported")
|
||||
}
|
||||
monitorutils.PrintLines(cm.stdout, st.Definition.Source.Infos[0], st.Cursors, cm.m.GetWalkerController().Breakpoints(), 0, 0, true)
|
||||
return nil
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/docker/buildx/controller/control"
|
||||
controllerapi "github.com/docker/buildx/controller/pb"
|
||||
"github.com/docker/buildx/monitor/types"
|
||||
"github.com/docker/buildx/util/progress"
|
||||
"github.com/docker/buildx/util/walker"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
solverpb "github.com/moby/buildkit/solver/pb"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func IsProcessID(ctx context.Context, c control.BuildxController, curRef, ref string) (bool, error) {
|
||||
infos, err := c.ListProcesses(ctx, curRef)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, p := range infos {
|
||||
if p.ProcessID == ref {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func PrintLines(w io.Writer, source *solverpb.SourceInfo, positions []solverpb.Range, bps *walker.Breakpoints, before, after int, all bool) {
|
||||
fmt.Fprintf(w, "Filename: %q\n", source.Filename)
|
||||
scanner := bufio.NewScanner(bytes.NewReader(source.Data))
|
||||
lastLinePrinted := false
|
||||
firstPrint := true
|
||||
for i := 1; scanner.Scan(); i++ {
|
||||
print := false
|
||||
target := false
|
||||
if len(positions) == 0 {
|
||||
print = true
|
||||
} else {
|
||||
for _, r := range positions {
|
||||
if all || int(r.Start.Line)-before <= i && i <= int(r.End.Line)+after {
|
||||
print = true
|
||||
if int(r.Start.Line) <= i && i <= int(r.End.Line) {
|
||||
target = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !print {
|
||||
lastLinePrinted = false
|
||||
continue
|
||||
}
|
||||
if !lastLinePrinted && !firstPrint {
|
||||
fmt.Fprintln(w, "----------------")
|
||||
}
|
||||
|
||||
prefix := " "
|
||||
bps.ForEach(func(key string, b walker.Breakpoint) bool {
|
||||
if b.IsMarked(int64(i)) {
|
||||
prefix = "*"
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
prefix2 := " "
|
||||
if target {
|
||||
prefix2 = "=>"
|
||||
}
|
||||
fmt.Fprintln(w, prefix+prefix2+fmt.Sprintf("%4d| ", i)+scanner.Text())
|
||||
lastLinePrinted = true
|
||||
firstPrint = false
|
||||
}
|
||||
}
|
||||
|
||||
func IsSameDefinition(a *solverpb.Definition, b *solverpb.Definition) bool {
|
||||
ctx := context.TODO()
|
||||
opA, err := llb.NewDefinitionOp(a)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
dgstA, _, _, _, err := llb.NewState(opA).Output().Vertex(ctx, nil).Marshal(ctx, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
opB, err := llb.NewDefinitionOp(b)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
dgstB, _, _, _, err := llb.NewState(opB).Output().Vertex(ctx, nil).Marshal(ctx, nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return dgstA.String() == dgstB.String()
|
||||
}
|
||||
|
||||
func SetDefaultBreakpoints(bps *walker.Breakpoints) {
|
||||
bps.ClearAll()
|
||||
bps.Add("stopOnEntry", walker.NewStopOnEntryBreakpoint()) // always enabled
|
||||
bps.Add("stopOnErr", walker.NewOnErrorBreakpoint())
|
||||
}
|
||||
|
||||
func NewWalkerController(m types.Monitor, stdout io.WriteCloser, invokeConfig controllerapi.InvokeConfig, progress *progress.Printer, def *solverpb.Definition) *walker.Controller {
|
||||
bps := walker.NewBreakpoints()
|
||||
SetDefaultBreakpoints(bps)
|
||||
return walker.NewController(def, bps, func(ctx context.Context, bCtx *walker.BreakContext) error {
|
||||
var keys []string
|
||||
for k := range bCtx.Hits {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
fmt.Fprintf(stdout, "Break at %+v\n", keys)
|
||||
PrintLines(stdout, bCtx.Definition.Source.Infos[0], bCtx.Cursors, bCtx.Breakpoints, 0, 0, true)
|
||||
m.Rollback(ctx, invokeConfig)
|
||||
return nil
|
||||
}, func(ctx context.Context, st llb.State) error {
|
||||
d, err := st.Marshal(ctx)
|
||||
if err != nil {
|
||||
return errors.Errorf("solve: failed to marshal definition: %v", err)
|
||||
}
|
||||
progress.Unpause()
|
||||
err = m.Solve(ctx, m.AttachedSessionID(), d.ToPB(), progress)
|
||||
progress.Pause()
|
||||
if err != nil {
|
||||
fmt.Fprintf(stdout, "failed during walk: %v\n", err)
|
||||
}
|
||||
return err
|
||||
}, func(err error) {
|
||||
if err == nil {
|
||||
fmt.Fprintf(stdout, "walker finished\n")
|
||||
} else {
|
||||
fmt.Fprintf(stdout, "walker finished with error %v\n", err)
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue