bee/vendor/github.com/derekparker/delve/pkg/terminal/command.go

1970 lines
51 KiB
Go

// Package terminal implements functions for responding to user
// input and dispatching to appropriate backend commands.
package terminal
import (
"bufio"
"errors"
"fmt"
"go/parser"
"go/scanner"
"io"
"math"
"os"
"os/exec"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"text/tabwriter"
"github.com/cosiner/argv"
"github.com/derekparker/delve/service"
"github.com/derekparker/delve/service/api"
"github.com/derekparker/delve/service/debugger"
)
const optimizedFunctionWarning = "Warning: debugging optimized function"
type cmdPrefix int
const (
noPrefix = cmdPrefix(0)
onPrefix = cmdPrefix(1 << iota)
)
type callContext struct {
Prefix cmdPrefix
Scope api.EvalScope
Breakpoint *api.Breakpoint
}
func (ctx *callContext) scoped() bool {
return ctx.Scope.GoroutineID >= 0 || ctx.Scope.Frame > 0
}
type frameDirection int
const (
frameSet frameDirection = iota
frameUp
frameDown
)
type cmdfunc func(t *Term, ctx callContext, args string) error
type command struct {
aliases []string
builtinAliases []string
allowedPrefixes cmdPrefix
helpMsg string
cmdFn cmdfunc
}
// Returns true if the command string matches one of the aliases for this command
func (c command) match(cmdstr string) bool {
for _, v := range c.aliases {
if v == cmdstr {
return true
}
}
return false
}
// Commands represents the commands for Delve terminal process.
type Commands struct {
cmds []command
lastCmd cmdfunc
client service.Client
frame int // Current frame as set by frame/up/down commands.
}
var (
// LongLoadConfig loads more information:
// * Follows pointers
// * Loads more array values
// * Does not limit struct fields
LongLoadConfig = api.LoadConfig{true, 1, 64, 64, -1}
// ShortLoadConfig loads less information, not following pointers
// and limiting struct fields loaded to 3.
ShortLoadConfig = api.LoadConfig{false, 0, 64, 0, 3}
)
// ByFirstAlias will sort by the first
// alias of a command.
type ByFirstAlias []command
func (a ByFirstAlias) Len() int { return len(a) }
func (a ByFirstAlias) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByFirstAlias) Less(i, j int) bool { return a[i].aliases[0] < a[j].aliases[0] }
// DebugCommands returns a Commands struct with default commands defined.
func DebugCommands(client service.Client) *Commands {
c := &Commands{client: client}
c.cmds = []command{
{aliases: []string{"help", "h"}, cmdFn: c.help, helpMsg: `Prints the help message.
help [command]
Type "help" followed by the name of a command for more information about it.`},
{aliases: []string{"break", "b"}, cmdFn: breakpoint, helpMsg: `Sets a breakpoint.
break [name] <linespec>
See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/locspec.md for the syntax of linespec.
See also: "help on", "help cond" and "help clear"`},
{aliases: []string{"trace", "t"}, cmdFn: tracepoint, helpMsg: `Set tracepoint.
trace [name] <linespec>
A tracepoint is a breakpoint that does not stop the execution of the program, instead when the tracepoint is hit a notification is displayed. See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/locspec.md for the syntax of linespec.
See also: "help on", "help cond" and "help clear"`},
{aliases: []string{"restart", "r"}, cmdFn: restart, helpMsg: `Restart process.
restart [checkpoint]
restart [-noargs] newargv...
For recorded processes restarts from the start or from the specified
checkpoint. For normal processes restarts the process, optionally changing
the arguments. With -noargs, the process starts with an empty commandline.
`},
{aliases: []string{"continue", "c"}, cmdFn: c.cont, helpMsg: "Run until breakpoint or program termination."},
{aliases: []string{"step", "s"}, cmdFn: c.step, helpMsg: "Single step through program."},
{aliases: []string{"step-instruction", "si"}, cmdFn: c.stepInstruction, helpMsg: "Single step a single cpu instruction."},
{aliases: []string{"next", "n"}, cmdFn: c.next, helpMsg: "Step over to next source line."},
{aliases: []string{"stepout"}, cmdFn: c.stepout, helpMsg: "Step out of the current function."},
{aliases: []string{"call"}, cmdFn: c.call, helpMsg: `Resumes process, injecting a function call (EXPERIMENTAL!!!)
Current limitations:
- only pointers to stack-allocated objects can be passed as argument.
- only some automatic type conversions are supported.
- functions can only be called on running goroutines that are not
executing the runtime.
- the current goroutine needs to have at least 256 bytes of free space on
the stack.
- functions can only be called when the goroutine is stopped at a safe
point.
- calling a function will resume execution of all goroutines.
- only supported on linux's native backend.
`},
{aliases: []string{"threads"}, cmdFn: threads, helpMsg: "Print out info for every traced thread."},
{aliases: []string{"thread", "tr"}, cmdFn: thread, helpMsg: `Switch to the specified thread.
thread <id>`},
{aliases: []string{"clear"}, cmdFn: clear, helpMsg: `Deletes breakpoint.
clear <breakpoint name or id>`},
{aliases: []string{"clearall"}, cmdFn: clearAll, helpMsg: `Deletes multiple breakpoints.
clearall [<linespec>]
If called with the linespec argument it will delete all the breakpoints matching the linespec. If linespec is omitted all breakpoints are deleted.`},
{aliases: []string{"goroutines"}, cmdFn: goroutines, helpMsg: `List program goroutines.
goroutines [-u (default: user location)|-r (runtime location)|-g (go statement location)|-s (start location)] [ -t (stack trace)]
Print out info for every goroutine. The flag controls what information is shown along with each goroutine:
-u displays location of topmost stackframe in user code
-r displays location of topmost stackframe (including frames inside private runtime functions)
-g displays location of go instruction that created the goroutine
-s displays location of the start function
-t displays stack trace of goroutine
If no flag is specified the default is -u.`},
{aliases: []string{"goroutine"}, allowedPrefixes: onPrefix, cmdFn: c.goroutine, helpMsg: `Shows or changes current goroutine
goroutine
goroutine <id>
goroutine <id> <command>
Called without arguments it will show information about the current goroutine.
Called with a single argument it will switch to the specified goroutine.
Called with more arguments it will execute a command on the specified goroutine.`},
{aliases: []string{"breakpoints", "bp"}, cmdFn: breakpoints, helpMsg: "Print out info for active breakpoints."},
{aliases: []string{"print", "p"}, allowedPrefixes: onPrefix, cmdFn: printVar, helpMsg: `Evaluate an expression.
[goroutine <n>] [frame <m>] print <expression>
See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/expr.md for a description of supported expressions.`},
{aliases: []string{"whatis"}, cmdFn: whatisCommand, helpMsg: `Prints type of an expression.
whatis <expression>`},
{aliases: []string{"set"}, cmdFn: setVar, helpMsg: `Changes the value of a variable.
[goroutine <n>] [frame <m>] set <variable> = <value>
See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/expr.md for a description of supported expressions. Only numerical variables and pointers can be changed.`},
{aliases: []string{"sources"}, cmdFn: sources, helpMsg: `Print list of source files.
sources [<regex>]
If regex is specified only the source files matching it will be returned.`},
{aliases: []string{"funcs"}, cmdFn: funcs, helpMsg: `Print list of functions.
funcs [<regex>]
If regex is specified only the functions matching it will be returned.`},
{aliases: []string{"types"}, cmdFn: types, helpMsg: `Print list of types
types [<regex>]
If regex is specified only the types matching it will be returned.`},
{aliases: []string{"args"}, allowedPrefixes: onPrefix, cmdFn: args, helpMsg: `Print function arguments.
[goroutine <n>] [frame <m>] args [-v] [<regex>]
If regex is specified only function arguments with a name matching it will be returned. If -v is specified more information about each function argument will be shown.`},
{aliases: []string{"locals"}, allowedPrefixes: onPrefix, cmdFn: locals, helpMsg: `Print local variables.
[goroutine <n>] [frame <m>] locals [-v] [<regex>]
The name of variables that are shadowed in the current scope will be shown in parenthesis.
If regex is specified only local variables with a name matching it will be returned. If -v is specified more information about each local variable will be shown.`},
{aliases: []string{"vars"}, cmdFn: vars, helpMsg: `Print package variables.
vars [-v] [<regex>]
If regex is specified only package variables with a name matching it will be returned. If -v is specified more information about each package variable will be shown.`},
{aliases: []string{"regs"}, cmdFn: regs, helpMsg: `Print contents of CPU registers.
regs [-a]
Argument -a shows more registers.`},
{aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: `Exit the debugger.
exit [-c]
When connected to a headless instance started with the --accept-multiclient, pass -c to resume the execution of the target process before disconnecting.`},
{aliases: []string{"list", "ls", "l"}, cmdFn: listCommand, helpMsg: `Show source code.
[goroutine <n>] [frame <m>] list [<linespec>]
Show source around current point or provided linespec.`},
{aliases: []string{"stack", "bt"}, allowedPrefixes: onPrefix, cmdFn: stackCommand, helpMsg: `Print stack trace.
[goroutine <n>] [frame <m>] stack [<depth>] [-full] [-g] [-s] [-offsets]
-full every stackframe is decorated with the value of its local variables and arguments.
-offsets prints frame offset of each frame
`},
{aliases: []string{"frame"},
cmdFn: func(t *Term, ctx callContext, arg string) error {
return c.frameCommand(t, ctx, arg, frameSet)
},
helpMsg: `Set the current frame, or execute command on a different frame.
frame <m>
frame <m> <command>
The first form sets frame used by subsequent commands such as "print" or "set".
The second form runs the command on the given frame.`},
{aliases: []string{"up"},
cmdFn: func(t *Term, ctx callContext, arg string) error {
return c.frameCommand(t, ctx, arg, frameUp)
},
helpMsg: `Move the current frame up.
up [<m>]
up [<m>] <command>
Move the current frame up by <m>. The second form runs the command on the given frame.`},
{aliases: []string{"down"},
cmdFn: func(t *Term, ctx callContext, arg string) error {
return c.frameCommand(t, ctx, arg, frameDown)
},
helpMsg: `Move the current frame down.
down [<m>]
down [<m>] <command>
Move the current frame down by <m>. The second form runs the command on the given frame.`},
{aliases: []string{"source"}, cmdFn: c.sourceCommand, helpMsg: `Executes a file containing a list of delve commands
source <path>`},
{aliases: []string{"disassemble", "disass"}, cmdFn: disassCommand, helpMsg: `Disassembler.
[goroutine <n>] [frame <m>] disassemble [-a <start> <end>] [-l <locspec>]
If no argument is specified the function being executed in the selected stack frame will be executed.
-a <start> <end> disassembles the specified address range
-l <locspec> disassembles the specified function`},
{aliases: []string{"on"}, cmdFn: c.onCmd, helpMsg: `Executes a command when a breakpoint is hit.
on <breakpoint name or id> <command>.
Supported commands: print, stack and goroutine)`},
{aliases: []string{"condition", "cond"}, cmdFn: conditionCmd, helpMsg: `Set breakpoint condition.
condition <breakpoint name or id> <boolean expression>.
Specifies that the breakpoint or tracepoint should break only if the boolean expression is true.`},
{aliases: []string{"config"}, cmdFn: configureCmd, helpMsg: `Changes configuration parameters.
config -list
Show all configuration parameters.
config -save
Saves the configuration file to disk, overwriting the current configuration file.
config <parameter> <value>
Changes the value of a configuration parameter.
config substitute-path <from> <to>
config substitute-path <from>
Adds or removes a path substitution rule.
config alias <command> <alias>
config alias <alias>
Defines <alias> as an alias to <command> or removes an alias.`},
{aliases: []string{"edit", "ed"}, cmdFn: edit, helpMsg: `Open where you are in $DELVE_EDITOR or $EDITOR
edit [locspec]
If locspec is omitted edit will open the current source file in the editor, otherwise it will open the specified location.`},
}
if client == nil || client.Recorded() {
c.cmds = append(c.cmds, command{
aliases: []string{"rewind", "rw"},
cmdFn: rewind,
helpMsg: "Run backwards until breakpoint or program termination.",
})
c.cmds = append(c.cmds, command{
aliases: []string{"check", "checkpoint"},
cmdFn: checkpoint,
helpMsg: `Creates a checkpoint at the current position.
checkpoint [where]`,
})
c.cmds = append(c.cmds, command{
aliases: []string{"checkpoints"},
cmdFn: checkpoints,
helpMsg: "Print out info for existing checkpoints.",
})
c.cmds = append(c.cmds, command{
aliases: []string{"clear-checkpoint", "clearcheck"},
cmdFn: clearCheckpoint,
helpMsg: `Deletes checkpoint.
clear-checkpoint <id>`,
})
for i := range c.cmds {
v := &c.cmds[i]
if v.match("restart") {
v.helpMsg = `Restart process from a checkpoint or event.
restart [event number or checkpoint id]`
}
}
}
sort.Sort(ByFirstAlias(c.cmds))
return c
}
// Register custom commands. Expects cf to be a func of type cmdfunc,
// returning only an error.
func (c *Commands) Register(cmdstr string, cf cmdfunc, helpMsg string) {
for _, v := range c.cmds {
if v.match(cmdstr) {
v.cmdFn = cf
return
}
}
c.cmds = append(c.cmds, command{aliases: []string{cmdstr}, cmdFn: cf, helpMsg: helpMsg})
}
// Find will look up the command function for the given command input.
// If it cannot find the command it will default to noCmdAvailable().
// If the command is an empty string it will replay the last command.
func (c *Commands) Find(cmdstr string, prefix cmdPrefix) cmdfunc {
// If <enter> use last command, if there was one.
if cmdstr == "" {
if c.lastCmd != nil {
return c.lastCmd
}
return nullCommand
}
for _, v := range c.cmds {
if v.match(cmdstr) {
if prefix != noPrefix && v.allowedPrefixes&prefix == 0 {
continue
}
c.lastCmd = v.cmdFn
return v.cmdFn
}
}
return noCmdAvailable
}
// CallWithContext takes a command and a context that command should be executed in.
func (c *Commands) CallWithContext(cmdstr string, t *Term, ctx callContext) error {
vals := strings.SplitN(strings.TrimSpace(cmdstr), " ", 2)
cmdname := vals[0]
var args string
if len(vals) > 1 {
args = strings.TrimSpace(vals[1])
}
return c.Find(cmdname, ctx.Prefix)(t, ctx, args)
}
// Call takes a command to execute.
func (c *Commands) Call(cmdstr string, t *Term) error {
ctx := callContext{Prefix: noPrefix, Scope: api.EvalScope{GoroutineID: -1, Frame: c.frame}}
return c.CallWithContext(cmdstr, t, ctx)
}
// Merge takes aliases defined in the config struct and merges them with the default aliases.
func (c *Commands) Merge(allAliases map[string][]string) {
for i := range c.cmds {
if c.cmds[i].builtinAliases != nil {
c.cmds[i].aliases = append(c.cmds[i].aliases[:0], c.cmds[i].builtinAliases...)
}
}
for i := range c.cmds {
if aliases, ok := allAliases[c.cmds[i].aliases[0]]; ok {
if c.cmds[i].builtinAliases == nil {
c.cmds[i].builtinAliases = make([]string, len(c.cmds[i].aliases))
copy(c.cmds[i].builtinAliases, c.cmds[i].aliases)
}
c.cmds[i].aliases = append(c.cmds[i].aliases, aliases...)
}
}
}
var noCmdError = errors.New("command not available")
func noCmdAvailable(t *Term, ctx callContext, args string) error {
return noCmdError
}
func nullCommand(t *Term, ctx callContext, args string) error {
return nil
}
func (c *Commands) help(t *Term, ctx callContext, args string) error {
if args != "" {
for _, cmd := range c.cmds {
for _, alias := range cmd.aliases {
if alias == args {
fmt.Println(cmd.helpMsg)
return nil
}
}
}
return noCmdError
}
fmt.Println("The following commands are available:")
w := new(tabwriter.Writer)
w.Init(os.Stdout, 0, 8, 0, '-', 0)
for _, cmd := range c.cmds {
h := cmd.helpMsg
if idx := strings.Index(h, "\n"); idx >= 0 {
h = h[:idx]
}
if len(cmd.aliases) > 1 {
fmt.Fprintf(w, " %s (alias: %s) \t %s\n", cmd.aliases[0], strings.Join(cmd.aliases[1:], " | "), h)
} else {
fmt.Fprintf(w, " %s \t %s\n", cmd.aliases[0], h)
}
}
if err := w.Flush(); err != nil {
return err
}
fmt.Println("Type help followed by a command for full documentation.")
return nil
}
type byThreadID []*api.Thread
func (a byThreadID) Len() int { return len(a) }
func (a byThreadID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byThreadID) Less(i, j int) bool { return a[i].ID < a[j].ID }
func threads(t *Term, ctx callContext, args string) error {
threads, err := t.client.ListThreads()
if err != nil {
return err
}
state, err := t.client.GetState()
if err != nil {
return err
}
sort.Sort(byThreadID(threads))
for _, th := range threads {
prefix := " "
if state.CurrentThread != nil && state.CurrentThread.ID == th.ID {
prefix = "* "
}
if th.Function != nil {
fmt.Printf("%sThread %d at %#v %s:%d %s\n",
prefix, th.ID, th.PC, ShortenFilePath(th.File),
th.Line, th.Function.Name())
} else {
fmt.Printf("%sThread %s\n", prefix, formatThread(th))
}
}
return nil
}
func thread(t *Term, ctx callContext, args string) error {
if len(args) == 0 {
return fmt.Errorf("you must specify a thread")
}
tid, err := strconv.Atoi(args)
if err != nil {
return err
}
oldState, err := t.client.GetState()
if err != nil {
return err
}
newState, err := t.client.SwitchThread(tid)
if err != nil {
return err
}
oldThread := "<none>"
newThread := "<none>"
if oldState.CurrentThread != nil {
oldThread = strconv.Itoa(oldState.CurrentThread.ID)
}
if newState.CurrentThread != nil {
newThread = strconv.Itoa(newState.CurrentThread.ID)
}
fmt.Printf("Switched from %s to %s\n", oldThread, newThread)
return nil
}
type byGoroutineID []*api.Goroutine
func (a byGoroutineID) Len() int { return len(a) }
func (a byGoroutineID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byGoroutineID) Less(i, j int) bool { return a[i].ID < a[j].ID }
func goroutines(t *Term, ctx callContext, argstr string) error {
args := strings.Split(argstr, " ")
var fgl = fglUserCurrent
bPrintStack := false
switch len(args) {
case 0:
// nothing to do
case 1, 2:
for _, arg := range args {
switch arg {
case "-u":
fgl = fglUserCurrent
case "-r":
fgl = fglRuntimeCurrent
case "-g":
fgl = fglGo
case "-s":
fgl = fglStart
case "-t":
bPrintStack = true
case "":
// nothing to do
default:
return fmt.Errorf("wrong argument: '%s'", arg)
}
}
default:
return fmt.Errorf("too many arguments")
}
state, err := t.client.GetState()
if err != nil {
return err
}
gs, err := t.client.ListGoroutines()
if err != nil {
return err
}
sort.Sort(byGoroutineID(gs))
fmt.Printf("[%d goroutines]\n", len(gs))
for _, g := range gs {
prefix := " "
if state.SelectedGoroutine != nil && g.ID == state.SelectedGoroutine.ID {
prefix = "* "
}
fmt.Printf("%sGoroutine %s\n", prefix, formatGoroutine(g, fgl))
if bPrintStack {
stack, err := t.client.Stacktrace(g.ID, 10, false, nil)
if err != nil {
return err
}
printStack(stack, "\t", false)
}
}
return nil
}
func selectedGID(state *api.DebuggerState) int {
if state.SelectedGoroutine == nil {
return 0
}
return state.SelectedGoroutine.ID
}
func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error {
args := strings.SplitN(argstr, " ", 2)
if ctx.Prefix == onPrefix {
if len(args) != 1 || args[0] != "" {
return errors.New("too many arguments to goroutine")
}
ctx.Breakpoint.Goroutine = true
return nil
}
if len(args) == 1 {
if args[0] == "" {
return printscope(t)
}
gid, err := strconv.Atoi(argstr)
if err != nil {
return err
}
oldState, err := t.client.GetState()
if err != nil {
return err
}
newState, err := t.client.SwitchGoroutine(gid)
if err != nil {
return err
}
c.frame = 0
fmt.Printf("Switched from %d to %d (thread %d)\n", selectedGID(oldState), gid, newState.CurrentThread.ID)
return nil
}
var err error
ctx.Scope.GoroutineID, err = strconv.Atoi(args[0])
if err != nil {
return err
}
return c.CallWithContext(args[1], t, ctx)
}
// Handle "frame", "up", "down" commands.
func (c *Commands) frameCommand(t *Term, ctx callContext, argstr string, direction frameDirection) error {
frame := 1
arg := ""
if len(argstr) == 0 {
if direction == frameSet {
return errors.New("not enough arguments")
}
} else {
args := strings.SplitN(argstr, " ", 2)
var err error
if frame, err = strconv.Atoi(args[0]); err != nil {
return err
}
if len(args) > 1 {
arg = args[1]
}
}
switch direction {
case frameUp:
frame = c.frame + frame
case frameDown:
frame = c.frame - frame
}
if len(arg) > 0 {
ctx.Scope.Frame = frame
return c.CallWithContext(arg, t, ctx)
}
if frame < 0 {
return fmt.Errorf("Invalid frame %d", frame)
}
stack, err := t.client.Stacktrace(ctx.Scope.GoroutineID, frame, false, nil)
if err != nil {
return err
}
if frame >= len(stack) {
return fmt.Errorf("Invalid frame %d", frame)
}
c.frame = frame
state, err := t.client.GetState()
if err != nil {
return err
}
printcontext(t, state)
th := stack[frame]
fmt.Printf("Frame %d: %s:%d (PC: %x)\n", frame, ShortenFilePath(th.File), th.Line, th.PC)
printfile(t, th.File, th.Line, true)
return nil
}
func printscope(t *Term) error {
state, err := t.client.GetState()
if err != nil {
return err
}
fmt.Printf("Thread %s\n", formatThread(state.CurrentThread))
if state.SelectedGoroutine != nil {
writeGoroutineLong(os.Stdout, state.SelectedGoroutine, "")
}
return nil
}
func formatThread(th *api.Thread) string {
if th == nil {
return "<nil>"
}
return fmt.Sprintf("%d at %s:%d", th.ID, ShortenFilePath(th.File), th.Line)
}
type formatGoroutineLoc int
const (
fglRuntimeCurrent = formatGoroutineLoc(iota)
fglUserCurrent
fglGo
fglStart
)
func formatLocation(loc api.Location) string {
return fmt.Sprintf("%s:%d %s (%#v)", ShortenFilePath(loc.File), loc.Line, loc.Function.Name(), loc.PC)
}
func formatGoroutine(g *api.Goroutine, fgl formatGoroutineLoc) string {
if g == nil {
return "<nil>"
}
if g.Unreadable != "" {
return fmt.Sprintf("(unreadable %s)", g.Unreadable)
}
var locname string
var loc api.Location
switch fgl {
case fglRuntimeCurrent:
locname = "Runtime"
loc = g.CurrentLoc
case fglUserCurrent:
locname = "User"
loc = g.UserCurrentLoc
case fglGo:
locname = "Go"
loc = g.GoStatementLoc
case fglStart:
locname = "Start"
loc = g.StartLoc
}
thread := ""
if g.ThreadID != 0 {
thread = fmt.Sprintf(" (thread %d)", g.ThreadID)
}
return fmt.Sprintf("%d - %s: %s%s", g.ID, locname, formatLocation(loc), thread)
}
func writeGoroutineLong(w io.Writer, g *api.Goroutine, prefix string) {
fmt.Fprintf(w, "%sGoroutine %d:\n%s\tRuntime: %s\n%s\tUser: %s\n%s\tGo: %s\n%s\tStart: %s\n",
prefix, g.ID,
prefix, formatLocation(g.CurrentLoc),
prefix, formatLocation(g.UserCurrentLoc),
prefix, formatLocation(g.GoStatementLoc),
prefix, formatLocation(g.StartLoc))
}
func parseArgs(args string) ([]string, error) {
if args == "" {
return nil, nil
}
v, err := argv.Argv([]rune(args), argv.ParseEnv(os.Environ()),
func(s []rune, _ map[string]string) ([]rune, error) {
return nil, fmt.Errorf("Backtick not supported in '%s'", string(s))
})
if err != nil {
return nil, err
}
if len(v) != 1 {
return nil, fmt.Errorf("Illegal commandline '%s'", args)
}
return v[0], nil
}
func restart(t *Term, ctx callContext, args string) error {
v, err := parseArgs(args)
if err != nil {
return err
}
var restartPos string
var resetArgs bool
if t.client.Recorded() {
if len(v) > 1 {
return fmt.Errorf("restart: illegal position '%v'", v)
}
if len(v) == 1 {
restartPos = v[0]
v = nil
}
} else if len(v) > 0 {
resetArgs = true
if v[0] == "-noargs" {
if len(v) > 1 {
return fmt.Errorf("restart: -noargs does not take any arg")
}
v = nil
}
}
discarded, err := t.client.RestartFrom(restartPos, resetArgs, v)
if err != nil {
return err
}
if !t.client.Recorded() {
fmt.Println("Process restarted with PID", t.client.ProcessPid())
}
for i := range discarded {
fmt.Printf("Discarded %s at %s: %v\n", formatBreakpointName(discarded[i].Breakpoint, false), formatBreakpointLocation(discarded[i].Breakpoint), discarded[i].Reason)
}
if t.client.Recorded() {
state, err := t.client.GetState()
if err != nil {
return err
}
printcontext(t, state)
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
}
return nil
}
func printcontextNoState(t *Term) {
state, _ := t.client.GetState()
if state == nil || state.CurrentThread == nil {
return
}
printcontext(t, state)
}
func (c *Commands) cont(t *Term, ctx callContext, args string) error {
c.frame = 0
stateChan := t.client.Continue()
var state *api.DebuggerState
for state = range stateChan {
if state.Err != nil {
printcontextNoState(t)
return state.Err
}
printcontext(t, state)
}
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
return nil
}
func continueUntilCompleteNext(t *Term, state *api.DebuggerState, op string) error {
if !state.NextInProgress {
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
return nil
}
for {
fmt.Printf("\tbreakpoint hit during %s, continuing...\n", op)
stateChan := t.client.Continue()
var state *api.DebuggerState
for state = range stateChan {
if state.Err != nil {
printcontextNoState(t)
return state.Err
}
printcontext(t, state)
}
if !state.NextInProgress {
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
return nil
}
}
}
func scopePrefixSwitch(t *Term, ctx callContext) error {
if ctx.Scope.GoroutineID > 0 {
_, err := t.client.SwitchGoroutine(ctx.Scope.GoroutineID)
if err != nil {
return err
}
}
return nil
}
func exitedToError(state *api.DebuggerState, err error) (*api.DebuggerState, error) {
if err == nil && state.Exited {
return nil, fmt.Errorf("Process has exited with status %d", state.ExitStatus)
}
return state, err
}
func (c *Commands) step(t *Term, ctx callContext, args string) error {
if err := scopePrefixSwitch(t, ctx); err != nil {
return err
}
c.frame = 0
state, err := exitedToError(t.client.Step())
if err != nil {
printcontextNoState(t)
return err
}
printcontext(t, state)
return continueUntilCompleteNext(t, state, "step")
}
var notOnFrameZeroErr = errors.New("not on topmost frame")
func (c *Commands) stepInstruction(t *Term, ctx callContext, args string) error {
if err := scopePrefixSwitch(t, ctx); err != nil {
return err
}
if c.frame != 0 {
return notOnFrameZeroErr
}
state, err := exitedToError(t.client.StepInstruction())
if err != nil {
printcontextNoState(t)
return err
}
printcontext(t, state)
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
return nil
}
func (c *Commands) next(t *Term, ctx callContext, args string) error {
if err := scopePrefixSwitch(t, ctx); err != nil {
return err
}
if c.frame != 0 {
return notOnFrameZeroErr
}
state, err := exitedToError(t.client.Next())
if err != nil {
printcontextNoState(t)
return err
}
printcontext(t, state)
return continueUntilCompleteNext(t, state, "next")
}
func (c *Commands) stepout(t *Term, ctx callContext, args string) error {
if err := scopePrefixSwitch(t, ctx); err != nil {
return err
}
if c.frame != 0 {
return notOnFrameZeroErr
}
state, err := exitedToError(t.client.StepOut())
if err != nil {
printcontextNoState(t)
return err
}
printcontext(t, state)
return continueUntilCompleteNext(t, state, "stepout")
}
func (c *Commands) call(t *Term, ctx callContext, args string) error {
if err := scopePrefixSwitch(t, ctx); err != nil {
return err
}
state, err := exitedToError(t.client.Call(args))
c.frame = 0
if err != nil {
printcontextNoState(t)
return err
}
printcontext(t, state)
return continueUntilCompleteNext(t, state, "call")
}
func clear(t *Term, ctx callContext, args string) error {
if len(args) == 0 {
return fmt.Errorf("not enough arguments")
}
id, err := strconv.Atoi(args)
var bp *api.Breakpoint
if err == nil {
bp, err = t.client.ClearBreakpoint(id)
} else {
bp, err = t.client.ClearBreakpointByName(args)
}
if err != nil {
return err
}
fmt.Printf("%s cleared at %s\n", formatBreakpointName(bp, true), formatBreakpointLocation(bp))
return nil
}
func clearAll(t *Term, ctx callContext, args string) error {
breakPoints, err := t.client.ListBreakpoints()
if err != nil {
return err
}
var locPCs map[uint64]struct{}
if args != "" {
locs, err := t.client.FindLocation(api.EvalScope{GoroutineID: -1, Frame: 0}, args)
if err != nil {
return err
}
locPCs = make(map[uint64]struct{})
for _, loc := range locs {
locPCs[loc.PC] = struct{}{}
}
}
for _, bp := range breakPoints {
if locPCs != nil {
if _, ok := locPCs[bp.Addr]; !ok {
continue
}
}
if bp.ID < 0 {
continue
}
_, err := t.client.ClearBreakpoint(bp.ID)
if err != nil {
fmt.Printf("Couldn't delete %s at %s: %s\n", formatBreakpointName(bp, false), formatBreakpointLocation(bp), err)
}
fmt.Printf("%s cleared at %s\n", formatBreakpointName(bp, true), formatBreakpointLocation(bp))
}
return nil
}
// ByID sorts breakpoints by ID.
type ByID []*api.Breakpoint
func (a ByID) Len() int { return len(a) }
func (a ByID) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ByID) Less(i, j int) bool { return a[i].ID < a[j].ID }
func breakpoints(t *Term, ctx callContext, args string) error {
breakPoints, err := t.client.ListBreakpoints()
if err != nil {
return err
}
sort.Sort(ByID(breakPoints))
for _, bp := range breakPoints {
fmt.Printf("%s at %v (%d)\n", formatBreakpointName(bp, true), formatBreakpointLocation(bp), bp.TotalHitCount)
var attrs []string
if bp.Cond != "" {
attrs = append(attrs, fmt.Sprintf("\tcond %s", bp.Cond))
}
if bp.Stacktrace > 0 {
attrs = append(attrs, fmt.Sprintf("\tstack %d", bp.Stacktrace))
}
if bp.Goroutine {
attrs = append(attrs, "\tgoroutine")
}
if bp.LoadArgs != nil {
if *(bp.LoadArgs) == LongLoadConfig {
attrs = append(attrs, "\targs -v")
} else {
attrs = append(attrs, "\targs")
}
}
if bp.LoadLocals != nil {
if *(bp.LoadLocals) == LongLoadConfig {
attrs = append(attrs, "\tlocals -v")
} else {
attrs = append(attrs, "\tlocals")
}
}
for i := range bp.Variables {
attrs = append(attrs, fmt.Sprintf("\tprint %s", bp.Variables[i]))
}
if len(attrs) > 0 {
fmt.Printf("%s\n", strings.Join(attrs, "\n"))
}
}
return nil
}
func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) error {
args := strings.SplitN(argstr, " ", 2)
requestedBp := &api.Breakpoint{}
locspec := ""
switch len(args) {
case 1:
locspec = argstr
case 2:
if api.ValidBreakpointName(args[0]) == nil {
requestedBp.Name = args[0]
locspec = args[1]
} else {
locspec = argstr
}
default:
return fmt.Errorf("address required")
}
requestedBp.Tracepoint = tracepoint
locs, err := t.client.FindLocation(ctx.Scope, locspec)
if err != nil {
if requestedBp.Name == "" {
return err
}
requestedBp.Name = ""
locspec = argstr
var err2 error
locs, err2 = t.client.FindLocation(ctx.Scope, locspec)
if err2 != nil {
return err
}
}
for _, loc := range locs {
requestedBp.Addr = loc.PC
bp, err := t.client.CreateBreakpoint(requestedBp)
if err != nil {
return err
}
fmt.Printf("%s set at %s\n", formatBreakpointName(bp, true), formatBreakpointLocation(bp))
}
return nil
}
func breakpoint(t *Term, ctx callContext, args string) error {
return setBreakpoint(t, ctx, false, args)
}
func tracepoint(t *Term, ctx callContext, args string) error {
return setBreakpoint(t, ctx, true, args)
}
func edit(t *Term, ctx callContext, args string) error {
file, lineno, _, err := getLocation(t, ctx, args, false)
if err != nil {
return err
}
var editor string
if editor = os.Getenv("DELVE_EDITOR"); editor == "" {
if editor = os.Getenv("EDITOR"); editor == "" {
return fmt.Errorf("Neither DELVE_EDITOR or EDITOR is set")
}
}
cmd := exec.Command(editor, fmt.Sprintf("+%d", lineno), file)
return cmd.Run()
}
func printVar(t *Term, ctx callContext, args string) error {
if len(args) == 0 {
return fmt.Errorf("not enough arguments")
}
if ctx.Prefix == onPrefix {
ctx.Breakpoint.Variables = append(ctx.Breakpoint.Variables, args)
return nil
}
val, err := t.client.EvalVariable(ctx.Scope, args, t.loadConfig())
if err != nil {
return err
}
fmt.Println(val.MultilineString(""))
return nil
}
func whatisCommand(t *Term, ctx callContext, args string) error {
if len(args) == 0 {
return fmt.Errorf("not enough arguments")
}
val, err := t.client.EvalVariable(ctx.Scope, args, ShortLoadConfig)
if err != nil {
return err
}
if val.Type != "" {
fmt.Println(val.Type)
}
if val.RealType != val.Type {
fmt.Printf("Real type: %s\n", val.RealType)
}
if val.Kind == reflect.Interface && len(val.Children) > 0 {
fmt.Printf("Concrete type: %s\n", val.Children[0].Type)
}
if t.conf.ShowLocationExpr && val.LocationExpr != "" {
fmt.Printf("location: %s\n", val.LocationExpr)
}
return nil
}
func setVar(t *Term, ctx callContext, args string) error {
// HACK: in go '=' is not an operator, we detect the error and try to recover from it by splitting the input string
_, err := parser.ParseExpr(args)
if err == nil {
return fmt.Errorf("syntax error '=' not found")
}
el, ok := err.(scanner.ErrorList)
if !ok || el[0].Msg != "expected '==', found '='" {
return err
}
lexpr := args[:el[0].Pos.Offset]
rexpr := args[el[0].Pos.Offset+1:]
return t.client.SetVariable(ctx.Scope, lexpr, rexpr)
}
func printFilteredVariables(varType string, vars []api.Variable, filter string, cfg api.LoadConfig) error {
reg, err := regexp.Compile(filter)
if err != nil {
return err
}
match := false
for _, v := range vars {
if reg == nil || reg.Match([]byte(v.Name)) {
match = true
name := v.Name
if v.Flags&api.VariableShadowed != 0 {
name = "(" + name + ")"
}
if cfg == ShortLoadConfig {
fmt.Printf("%s = %s\n", name, v.SinglelineString())
} else {
fmt.Printf("%s = %s\n", name, v.MultilineString(""))
}
}
}
if !match {
fmt.Printf("(no %s)\n", varType)
}
return nil
}
func printSortedStrings(v []string, err error) error {
if err != nil {
return err
}
sort.Strings(v)
for _, d := range v {
fmt.Println(d)
}
return nil
}
func sources(t *Term, ctx callContext, args string) error {
return printSortedStrings(t.client.ListSources(args))
}
func funcs(t *Term, ctx callContext, args string) error {
return printSortedStrings(t.client.ListFunctions(args))
}
func types(t *Term, ctx callContext, args string) error {
return printSortedStrings(t.client.ListTypes(args))
}
func parseVarArguments(args string, t *Term) (filter string, cfg api.LoadConfig) {
if v := strings.SplitN(args, " ", 2); len(v) >= 1 && v[0] == "-v" {
if len(v) == 2 {
return v[1], t.loadConfig()
} else {
return "", t.loadConfig()
}
}
return args, ShortLoadConfig
}
func args(t *Term, ctx callContext, args string) error {
filter, cfg := parseVarArguments(args, t)
if ctx.Prefix == onPrefix {
if filter != "" {
return fmt.Errorf("filter not supported on breakpoint")
}
ctx.Breakpoint.LoadArgs = &cfg
return nil
}
vars, err := t.client.ListFunctionArgs(ctx.Scope, cfg)
if err != nil {
return err
}
return printFilteredVariables("args", vars, filter, cfg)
}
func locals(t *Term, ctx callContext, args string) error {
filter, cfg := parseVarArguments(args, t)
if ctx.Prefix == onPrefix {
if filter != "" {
return fmt.Errorf("filter not supported on breakpoint")
}
ctx.Breakpoint.LoadLocals = &cfg
return nil
}
locals, err := t.client.ListLocalVariables(ctx.Scope, cfg)
if err != nil {
return err
}
return printFilteredVariables("locals", locals, filter, cfg)
}
func vars(t *Term, ctx callContext, args string) error {
filter, cfg := parseVarArguments(args, t)
vars, err := t.client.ListPackageVariables(filter, cfg)
if err != nil {
return err
}
return printFilteredVariables("vars", vars, filter, cfg)
}
func regs(t *Term, ctx callContext, args string) error {
includeFp := false
if args == "-a" {
includeFp = true
}
regs, err := t.client.ListRegisters(0, includeFp)
if err != nil {
return err
}
fmt.Println(regs)
return nil
}
func stackCommand(t *Term, ctx callContext, args string) error {
sa, err := parseStackArgs(args)
if err != nil {
return err
}
if ctx.Prefix == onPrefix {
ctx.Breakpoint.Stacktrace = sa.depth
return nil
}
var cfg *api.LoadConfig
if sa.full {
cfg = &ShortLoadConfig
}
stack, err := t.client.Stacktrace(ctx.Scope.GoroutineID, sa.depth, sa.readDefers, cfg)
if err != nil {
return err
}
printStack(stack, "", sa.offsets)
return nil
}
type stackArgs struct {
depth int
full bool
offsets bool
readDefers bool
}
func parseStackArgs(argstr string) (stackArgs, error) {
r := stackArgs{
depth: 50,
full: false,
}
if argstr != "" {
args := strings.Split(argstr, " ")
for i := range args {
switch args[i] {
case "-full":
r.full = true
case "-offsets":
r.offsets = true
case "-defer":
r.readDefers = true
default:
n, err := strconv.Atoi(args[i])
if err != nil {
return stackArgs{}, fmt.Errorf("depth must be a number")
}
r.depth = n
}
}
}
return r, nil
}
// getLocation returns the current location or the locations specified by the argument.
// getLocation is used to process the argument of list and edit commands.
func getLocation(t *Term, ctx callContext, args string, showContext bool) (file string, lineno int, showarrow bool, err error) {
switch {
case len(args) == 0 && !ctx.scoped():
state, err := t.client.GetState()
if err != nil {
return "", 0, false, err
}
if showContext {
printcontext(t, state)
}
if state.SelectedGoroutine != nil {
return state.SelectedGoroutine.CurrentLoc.File, state.SelectedGoroutine.CurrentLoc.Line, true, nil
}
return state.CurrentThread.File, state.CurrentThread.Line, true, nil
case len(args) == 0 && ctx.scoped():
locs, err := t.client.Stacktrace(ctx.Scope.GoroutineID, ctx.Scope.Frame, false, nil)
if err != nil {
return "", 0, false, err
}
if ctx.Scope.Frame >= len(locs) {
return "", 0, false, fmt.Errorf("Frame %d does not exist in goroutine %d", ctx.Scope.Frame, ctx.Scope.GoroutineID)
}
loc := locs[ctx.Scope.Frame]
gid := ctx.Scope.GoroutineID
if gid < 0 {
state, err := t.client.GetState()
if err != nil {
return "", 0, false, err
}
if state.SelectedGoroutine != nil {
gid = state.SelectedGoroutine.ID
}
}
if showContext {
fmt.Printf("Goroutine %d frame %d at %s:%d (PC: %#x)\n", gid, ctx.Scope.Frame, loc.File, loc.Line, loc.PC)
}
return loc.File, loc.Line, true, nil
default:
locs, err := t.client.FindLocation(ctx.Scope, args)
if err != nil {
return "", 0, false, err
}
if len(locs) > 1 {
return "", 0, false, debugger.AmbiguousLocationError{Location: args, CandidatesLocation: locs}
}
loc := locs[0]
if showContext {
fmt.Printf("Showing %s:%d (PC: %#x)\n", loc.File, loc.Line, loc.PC)
}
return loc.File, loc.Line, false, nil
}
}
func listCommand(t *Term, ctx callContext, args string) error {
file, lineno, showarrow, err := getLocation(t, ctx, args, true)
if err != nil {
return err
}
return printfile(t, file, lineno, showarrow)
}
func (c *Commands) sourceCommand(t *Term, ctx callContext, args string) error {
if len(args) == 0 {
return fmt.Errorf("wrong number of arguments: source <filename>")
}
return c.executeFile(t, args)
}
var disasmUsageError = errors.New("wrong number of arguments: disassemble [-a <start> <end>] [-l <locspec>]")
func disassCommand(t *Term, ctx callContext, args string) error {
var cmd, rest string
if args != "" {
argv := strings.SplitN(args, " ", 2)
if len(argv) != 2 {
return disasmUsageError
}
cmd = argv[0]
rest = argv[1]
}
var disasm api.AsmInstructions
var disasmErr error
switch cmd {
case "":
locs, err := t.client.FindLocation(ctx.Scope, "+0")
if err != nil {
return err
}
disasm, disasmErr = t.client.DisassemblePC(ctx.Scope, locs[0].PC, api.IntelFlavour)
case "-a":
v := strings.SplitN(rest, " ", 2)
if len(v) != 2 {
return disasmUsageError
}
startpc, err := strconv.ParseInt(v[0], 0, 64)
if err != nil {
return fmt.Errorf("wrong argument: %s is not a number", v[0])
}
endpc, err := strconv.ParseInt(v[1], 0, 64)
if err != nil {
return fmt.Errorf("wrong argument: %s is not a number", v[1])
}
disasm, disasmErr = t.client.DisassembleRange(ctx.Scope, uint64(startpc), uint64(endpc), api.IntelFlavour)
case "-l":
locs, err := t.client.FindLocation(ctx.Scope, rest)
if err != nil {
return err
}
if len(locs) != 1 {
return errors.New("expression specifies multiple locations")
}
disasm, disasmErr = t.client.DisassemblePC(ctx.Scope, locs[0].PC, api.IntelFlavour)
default:
return disasmUsageError
}
if disasmErr != nil {
return disasmErr
}
DisasmPrint(disasm, os.Stdout)
return nil
}
func digits(n int) int {
if n <= 0 {
return 1
}
return int(math.Floor(math.Log10(float64(n)))) + 1
}
const stacktraceTruncatedMessage = "(truncated)"
func printStack(stack []api.Stackframe, ind string, offsets bool) {
if len(stack) == 0 {
return
}
extranl := offsets
for i := range stack {
if extranl {
break
}
extranl = extranl || (len(stack[i].Defers) > 0) || (len(stack[i].Arguments) > 0) || (len(stack[i].Locals) > 0)
}
d := digits(len(stack) - 1)
fmtstr := "%s%" + strconv.Itoa(d) + "d 0x%016x in %s\n"
s := ind + strings.Repeat(" ", d+2+len(ind))
for i := range stack {
if stack[i].Err != "" {
fmt.Printf("%serror: %s\n", s, stack[i].Err)
continue
}
fmt.Printf(fmtstr, ind, i, stack[i].PC, stack[i].Function.Name())
fmt.Printf("%sat %s:%d\n", s, ShortenFilePath(stack[i].File), stack[i].Line)
if offsets {
fmt.Printf("%sframe: %+#x frame pointer %+#x\n", s, stack[i].FrameOffset, stack[i].FramePointerOffset)
}
for j, d := range stack[i].Defers {
deferHeader := fmt.Sprintf("%s defer %d: ", s, j)
s2 := strings.Repeat(" ", len(deferHeader))
if d.Unreadable != "" {
fmt.Printf("%s(unreadable defer: %s)\n", deferHeader, d.Unreadable)
continue
}
fmt.Printf("%s%#016x in %s\n", deferHeader, d.DeferredLoc.PC, d.DeferredLoc.Function.Name())
fmt.Printf("%sat %s:%d\n", s2, d.DeferredLoc.File, d.DeferredLoc.Line)
fmt.Printf("%sdeferred by %s at %s:%d\n", s2, d.DeferLoc.Function.Name(), d.DeferLoc.File, d.DeferLoc.Line)
}
for j := range stack[i].Arguments {
fmt.Printf("%s %s = %s\n", s, stack[i].Arguments[j].Name, stack[i].Arguments[j].SinglelineString())
}
for j := range stack[i].Locals {
fmt.Printf("%s %s = %s\n", s, stack[i].Locals[j].Name, stack[i].Locals[j].SinglelineString())
}
if extranl {
fmt.Println()
}
}
if len(stack) > 0 && !stack[len(stack)-1].Bottom {
fmt.Printf("%s"+stacktraceTruncatedMessage+"\n", ind)
}
}
func printcontext(t *Term, state *api.DebuggerState) {
for i := range state.Threads {
if (state.CurrentThread != nil) && (state.Threads[i].ID == state.CurrentThread.ID) {
continue
}
if state.Threads[i].Breakpoint != nil {
printcontextThread(t, state.Threads[i])
}
}
if state.CurrentThread == nil {
fmt.Println("No current thread available")
return
}
var th *api.Thread
if state.SelectedGoroutine == nil {
th = state.CurrentThread
} else {
for i := range state.Threads {
if state.Threads[i].ID == state.SelectedGoroutine.ThreadID {
th = state.Threads[i]
break
}
}
if th == nil {
printcontextLocation(state.SelectedGoroutine.CurrentLoc)
return
}
}
if th.File == "" {
fmt.Printf("Stopped at: 0x%x\n", state.CurrentThread.PC)
t.Println("=>", "no source available")
return
}
printcontextThread(t, th)
if state.When != "" {
fmt.Println(state.When)
}
}
func printcontextLocation(loc api.Location) {
fmt.Printf("> %s() %s:%d (PC: %#v)\n", loc.Function.Name(), ShortenFilePath(loc.File), loc.Line, loc.PC)
if loc.Function != nil && loc.Function.Optimized {
fmt.Println(optimizedFunctionWarning)
}
return
}
func printReturnValues(th *api.Thread) {
if th.ReturnValues == nil {
return
}
fmt.Println("Values returned:")
for _, v := range th.ReturnValues {
fmt.Printf("\t%s: %s\n", v.Name, v.MultilineString("\t"))
}
fmt.Println()
}
func printcontextThread(t *Term, th *api.Thread) {
fn := th.Function
if th.Breakpoint == nil {
printcontextLocation(api.Location{PC: th.PC, File: th.File, Line: th.Line, Function: th.Function})
printReturnValues(th)
return
}
args := ""
if th.BreakpointInfo != nil && th.Breakpoint.LoadArgs != nil && *th.Breakpoint.LoadArgs == ShortLoadConfig {
var arg []string
for _, ar := range th.BreakpointInfo.Arguments {
arg = append(arg, ar.SinglelineString())
}
args = strings.Join(arg, ", ")
}
bpname := ""
if th.Breakpoint.Name != "" {
bpname = fmt.Sprintf("[%s] ", th.Breakpoint.Name)
}
if hitCount, ok := th.Breakpoint.HitCount[strconv.Itoa(th.GoroutineID)]; ok {
fmt.Printf("> %s%s(%s) %s:%d (hits goroutine(%d):%d total:%d) (PC: %#v)\n",
bpname,
fn.Name(),
args,
ShortenFilePath(th.File),
th.Line,
th.GoroutineID,
hitCount,
th.Breakpoint.TotalHitCount,
th.PC)
} else {
fmt.Printf("> %s%s(%s) %s:%d (hits total:%d) (PC: %#v)\n",
bpname,
fn.Name(),
args,
ShortenFilePath(th.File),
th.Line,
th.Breakpoint.TotalHitCount,
th.PC)
}
if th.Function != nil && th.Function.Optimized {
fmt.Println(optimizedFunctionWarning)
}
printReturnValues(th)
if th.BreakpointInfo != nil {
bp := th.Breakpoint
bpi := th.BreakpointInfo
if bpi.Goroutine != nil {
writeGoroutineLong(os.Stdout, bpi.Goroutine, "\t")
}
for _, v := range bpi.Variables {
fmt.Printf("\t%s: %s\n", v.Name, v.MultilineString("\t"))
}
for _, v := range bpi.Locals {
if *bp.LoadLocals == LongLoadConfig {
fmt.Printf("\t%s: %s\n", v.Name, v.MultilineString("\t"))
} else {
fmt.Printf("\t%s: %s\n", v.Name, v.SinglelineString())
}
}
if bp.LoadArgs != nil && *bp.LoadArgs == LongLoadConfig {
for _, v := range bpi.Arguments {
fmt.Printf("\t%s: %s\n", v.Name, v.MultilineString("\t"))
}
}
if bpi.Stacktrace != nil {
fmt.Printf("\tStack:\n")
printStack(bpi.Stacktrace, "\t\t", false)
}
}
}
func printfile(t *Term, filename string, line int, showArrow bool) error {
if filename == "" {
return nil
}
file, err := os.Open(t.substitutePath(filename))
if err != nil {
return err
}
defer file.Close()
fi, _ := file.Stat()
lastModExe := t.client.LastModified()
if fi.ModTime().After(lastModExe) {
fmt.Println("Warning: listing may not match stale executable")
}
buf := bufio.NewScanner(file)
l := line
for i := 1; i < l-5; i++ {
if !buf.Scan() {
return nil
}
}
s := l - 5
if s < 1 {
s = 1
}
for i := s; i <= l+5; i++ {
if !buf.Scan() {
return nil
}
var prefix string
if showArrow {
prefix = " "
if i == l {
prefix = "=>"
}
}
prefix = fmt.Sprintf("%s%4d:\t", prefix, i)
t.Println(prefix, buf.Text())
}
return nil
}
// ExitRequestError is returned when the user
// exits Delve.
type ExitRequestError struct{}
func (ere ExitRequestError) Error() string {
return ""
}
func exitCommand(t *Term, ctx callContext, args string) error {
if args == "-c" {
if !t.client.IsMulticlient() {
return errors.New("not connected to an --accept-multiclient server")
}
t.quitContinue = true
}
return ExitRequestError{}
}
func getBreakpointByIDOrName(t *Term, arg string) (*api.Breakpoint, error) {
if id, err := strconv.Atoi(arg); err == nil {
return t.client.GetBreakpoint(id)
}
return t.client.GetBreakpointByName(arg)
}
func (c *Commands) onCmd(t *Term, ctx callContext, argstr string) error {
args := strings.SplitN(argstr, " ", 2)
if len(args) < 2 {
return errors.New("not enough arguments")
}
bp, err := getBreakpointByIDOrName(t, args[0])
if err != nil {
return err
}
ctx.Prefix = onPrefix
ctx.Breakpoint = bp
err = c.CallWithContext(args[1], t, ctx)
if err != nil {
return err
}
return t.client.AmendBreakpoint(ctx.Breakpoint)
}
func conditionCmd(t *Term, ctx callContext, argstr string) error {
args := strings.SplitN(argstr, " ", 2)
if len(args) < 2 {
return fmt.Errorf("not enough arguments")
}
bp, err := getBreakpointByIDOrName(t, args[0])
if err != nil {
return err
}
bp.Cond = args[1]
return t.client.AmendBreakpoint(bp)
}
// ShortenFilePath take a full file path and attempts to shorten
// it by replacing the current directory to './'.
func ShortenFilePath(fullPath string) string {
workingDir, _ := os.Getwd()
return strings.Replace(fullPath, workingDir, ".", 1)
}
func (c *Commands) executeFile(t *Term, name string) error {
fh, err := os.Open(name)
if err != nil {
return err
}
defer fh.Close()
scanner := bufio.NewScanner(fh)
lineno := 0
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
lineno++
if line == "" || line[0] == '#' {
continue
}
if err := c.Call(line, t); err != nil {
fmt.Printf("%s:%d: %v\n", name, lineno, err)
}
}
return scanner.Err()
}
func rewind(t *Term, ctx callContext, args string) error {
stateChan := t.client.Rewind()
var state *api.DebuggerState
for state = range stateChan {
if state.Err != nil {
return state.Err
}
printcontext(t, state)
}
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
return nil
}
func checkpoint(t *Term, ctx callContext, args string) error {
if args == "" {
state, err := t.client.GetState()
if err != nil {
return err
}
var loc api.Location = api.Location{PC: state.CurrentThread.PC, File: state.CurrentThread.File, Line: state.CurrentThread.Line, Function: state.CurrentThread.Function}
if state.SelectedGoroutine != nil {
loc = state.SelectedGoroutine.CurrentLoc
}
args = fmt.Sprintf("%s() %s:%d (%#x)", loc.Function.Name(), loc.File, loc.Line, loc.PC)
}
cpid, err := t.client.Checkpoint(args)
if err != nil {
return err
}
fmt.Printf("Checkpoint c%d created.\n", cpid)
return nil
}
func checkpoints(t *Term, ctx callContext, args string) error {
cps, err := t.client.ListCheckpoints()
if err != nil {
return err
}
w := new(tabwriter.Writer)
w.Init(os.Stdout, 4, 4, 2, ' ', 0)
fmt.Fprintln(w, "ID\tWhen\tWhere")
for _, cp := range cps {
fmt.Fprintf(w, "c%d\t%s\t%s\n", cp.ID, cp.When, cp.Where)
}
w.Flush()
return nil
}
func clearCheckpoint(t *Term, ctx callContext, args string) error {
if len(args) < 0 {
return errors.New("not enough arguments to clear-checkpoint")
}
if args[0] != 'c' {
return errors.New("clear-checkpoint argument must be a checkpoint ID")
}
id, err := strconv.Atoi(args[1:])
if err != nil {
return errors.New("clear-checkpoint argument must be a checkpoint ID")
}
return t.client.ClearCheckpoint(id)
}
func formatBreakpointName(bp *api.Breakpoint, upcase bool) string {
thing := "breakpoint"
if bp.Tracepoint {
thing = "tracepoint"
}
if upcase {
thing = strings.Title(thing)
}
id := bp.Name
if id == "" {
id = strconv.Itoa(bp.ID)
}
return fmt.Sprintf("%s %s", thing, id)
}
func formatBreakpointLocation(bp *api.Breakpoint) string {
p := ShortenFilePath(bp.File)
if bp.FunctionName != "" {
return fmt.Sprintf("%#v for %s() %s:%d", bp.Addr, bp.FunctionName, p, bp.Line)
}
return fmt.Sprintf("%#v for %s:%d", bp.Addr, p, bp.Line)
}