From 82aff3f18ad19c8ba190abd45a8689609dcffb50 Mon Sep 17 00:00:00 2001 From: Yasushi Saito Date: Thu, 22 Mar 2018 10:02:15 -0700 Subject: [PATCH] Extend the "frame" command to set the current frame. (#1110) * Extend the "frame" command to set the current frame. Command frame 3 sets up so that subsequent "print", "set", "whatis" command will operate on frame 3. frame 3 print foo continues to work. Added "up", "down". They move the current frame up or down. Implementation note: This changes removes "scopePrefix" mode from the terminal/command.go and instead have the command examine the goroutine/frame value to see if it is invoked in a scoped context. * Rename Command.Frame -> Command.frame. --- _fixtures/goroutinestackprog.go | 3 + pkg/proc/proc_test.go | 2 +- pkg/proc/variable_test.go | 2 +- pkg/terminal/command.go | 184 ++++++++++++++++++++++---------- pkg/terminal/command_test.go | 24 ++++- 5 files changed, 151 insertions(+), 64 deletions(-) diff --git a/_fixtures/goroutinestackprog.go b/_fixtures/goroutinestackprog.go index bb4de350..a1ad515c 100644 --- a/_fixtures/goroutinestackprog.go +++ b/_fixtures/goroutinestackprog.go @@ -9,7 +9,10 @@ func agoroutine(started chan<- struct{}, done chan<- struct{}, i int) { done <- struct{}{} } +var dummy int + func stacktraceme() { + dummy++ return } diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 88b2312d..c9feb096 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -896,7 +896,7 @@ func stackMatch(stack []loc, locations []proc.Stackframe, skipRuntime bool) bool } func TestStacktraceGoroutine(t *testing.T) { - mainStack := []loc{{13, "main.stacktraceme"}, {26, "main.main"}} + mainStack := []loc{{14, "main.stacktraceme"}, {29, "main.main"}} agoroutineStacks := [][]loc{ {{8, "main.agoroutine"}}, {{9, "main.agoroutine"}}, diff --git a/pkg/proc/variable_test.go b/pkg/proc/variable_test.go index 892ed096..631f5b75 100644 --- a/pkg/proc/variable_test.go +++ b/pkg/proc/variable_test.go @@ -32,7 +32,7 @@ func TestGoroutineCreationLocation(t *testing.T) { if filepath.Base(createdLocation.File) != "goroutinestackprog.go" { t.Fatalf("goroutine creation file incorrect: %s", filepath.Base(createdLocation.File)) } - if createdLocation.Line != 20 { + if createdLocation.Line != 23 { t.Fatalf("goroutine creation line incorrect: %v", createdLocation.Line) } } diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 8515f5a5..a5d71d20 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -29,9 +29,8 @@ const optimizedFunctionWarning = "Warning: debugging optimized function" type cmdPrefix int const ( - noPrefix = cmdPrefix(0) - scopePrefix = cmdPrefix(1 << iota) - onPrefix + noPrefix = cmdPrefix(0) + onPrefix = cmdPrefix(1 << iota) ) type callContext struct { @@ -40,6 +39,18 @@ type callContext struct { 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 { @@ -62,9 +73,10 @@ func (c command) match(cmdstr string) bool { // Commands represents the commands for Delve terminal process. type Commands struct { - cmds []command - lastCmd cmdfunc - client service.Client + cmds []command + lastCmd cmdfunc + client service.Client + frame int // Current frame as set by frame/up/down commands. } var ( @@ -111,11 +123,11 @@ See also: "help on", "help cond" and "help clear"`}, 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: cont, helpMsg: "Run until breakpoint or program termination."}, - {aliases: []string{"step", "s"}, allowedPrefixes: scopePrefix, cmdFn: step, helpMsg: "Single step through program."}, - {aliases: []string{"step-instruction", "si"}, allowedPrefixes: scopePrefix, cmdFn: stepInstruction, helpMsg: "Single step a single cpu instruction."}, - {aliases: []string{"next", "n"}, allowedPrefixes: scopePrefix, cmdFn: next, helpMsg: "Step over to next source line."}, - {aliases: []string{"stepout"}, allowedPrefixes: scopePrefix, cmdFn: stepout, helpMsg: "Step out of the current function."}, + {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{"threads"}, cmdFn: threads, helpMsg: "Print out info for every traced thread."}, {aliases: []string{"thread", "tr"}, cmdFn: thread, helpMsg: `Switch to the specified thread. @@ -139,7 +151,7 @@ Print out info for every goroutine. The flag controls what information is shown -g displays location of go instruction that created the goroutine If no flag is specified the default is -u.`}, - {aliases: []string{"goroutine"}, allowedPrefixes: onPrefix | scopePrefix, cmdFn: c.goroutine, helpMsg: `Shows or changes current goroutine + {aliases: []string{"goroutine"}, allowedPrefixes: onPrefix, cmdFn: c.goroutine, helpMsg: `Shows or changes current goroutine goroutine goroutine @@ -149,15 +161,15 @@ 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 | scopePrefix, cmdFn: printVar, helpMsg: `Evaluate an expression. + {aliases: []string{"print", "p"}, allowedPrefixes: onPrefix, cmdFn: printVar, helpMsg: `Evaluate an expression. [goroutine ] [frame ] print See $GOPATH/src/github.com/derekparker/delve/Documentation/cli/expr.md for a description of supported expressions.`}, - {aliases: []string{"whatis"}, allowedPrefixes: scopePrefix, cmdFn: whatisCommand, helpMsg: `Prints type of an expression. + {aliases: []string{"whatis"}, cmdFn: whatisCommand, helpMsg: `Prints type of an expression. whatis .`}, - {aliases: []string{"set"}, allowedPrefixes: scopePrefix, cmdFn: setVar, helpMsg: `Changes the value of a variable. + {aliases: []string{"set"}, cmdFn: setVar, helpMsg: `Changes the value of a variable. [goroutine ] [frame ] set = @@ -177,12 +189,12 @@ If regex is specified only the functions matching it will be returned.`}, types [] If regex is specified only the types matching it will be returned.`}, - {aliases: []string{"args"}, allowedPrefixes: scopePrefix | onPrefix, cmdFn: args, helpMsg: `Print function arguments. + {aliases: []string{"args"}, allowedPrefixes: onPrefix, cmdFn: args, helpMsg: `Print function arguments. [goroutine ] [frame ] args [-v] [] 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: scopePrefix | onPrefix, cmdFn: locals, helpMsg: `Print local variables. + {aliases: []string{"locals"}, allowedPrefixes: onPrefix, cmdFn: locals, helpMsg: `Print local variables. [goroutine ] [frame ] locals [-v] [] @@ -200,25 +212,53 @@ If regex is specified only package variables with a name matching it will be ret Argument -a shows more registers.`}, {aliases: []string{"exit", "quit", "q"}, cmdFn: exitCommand, helpMsg: "Exit the debugger."}, - {aliases: []string{"list", "ls", "l"}, allowedPrefixes: scopePrefix, cmdFn: listCommand, helpMsg: `Show source code. + {aliases: []string{"list", "ls", "l"}, cmdFn: listCommand, helpMsg: `Show source code. [goroutine ] [frame ] list [] Show source around current point or provided linespec.`}, - {aliases: []string{"stack", "bt"}, allowedPrefixes: scopePrefix | onPrefix, cmdFn: stackCommand, helpMsg: `Print stack trace. + {aliases: []string{"stack", "bt"}, allowedPrefixes: onPrefix, cmdFn: stackCommand, helpMsg: `Print stack trace. [goroutine ] [frame ] stack [] [-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"}, allowedPrefixes: scopePrefix, cmdFn: c.frame, helpMsg: `Executes command on a different frame. - - 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 + frame + +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 [] + up [] + +Move the current frame up by . 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 [] + down [] + +Move the current frame down by . 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 `}, - {aliases: []string{"disassemble", "disass"}, allowedPrefixes: scopePrefix, cmdFn: disassCommand, helpMsg: `Disassembler. + {aliases: []string{"disassemble", "disass"}, cmdFn: disassCommand, helpMsg: `Disassembler. [goroutine ] [frame ] disassemble [-a ] [-l ] @@ -349,7 +389,7 @@ func (c *Commands) CallWithContext(cmdstr string, t *Term, ctx callContext) erro } func (c *Commands) Call(cmdstr string, t *Term) error { - ctx := callContext{Prefix: noPrefix, Scope: api.EvalScope{GoroutineID: -1, Frame: 0}} + ctx := callContext{Prefix: noPrefix, Scope: api.EvalScope{GoroutineID: -1, Frame: c.frame}} return c.CallWithContext(cmdstr, t, ctx) } @@ -544,9 +584,6 @@ func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error { } if len(args) == 1 { - if ctx.Prefix == scopePrefix { - return errors.New("no command passed to goroutine") - } if args[0] == "" { return printscope(t) } @@ -563,13 +600,12 @@ func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error { 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.Prefix = scopePrefix ctx.Scope.GoroutineID, err = strconv.Atoi(args[0]) if err != nil { return err @@ -577,21 +613,54 @@ func (c *Commands) goroutine(t *Term, ctx callContext, argstr string) error { return c.CallWithContext(args[1], t, ctx) } -func (c *Commands) frame(t *Term, ctx callContext, args string) error { - v := strings.SplitN(args, " ", 2) - - switch len(v) { - case 0, 1: - return errors.New("not enough arguments") +// 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] + } } - - var err error - ctx.Prefix = scopePrefix - ctx.Scope.Frame, err = strconv.Atoi(v[0]) + 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, nil) if err != nil { return err } - return c.CallWithContext(v[1], t, ctx) + 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 { @@ -730,7 +799,8 @@ func printfileNoState(t *Term) { } } -func cont(t *Term, ctx callContext, args string) error { +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 { @@ -768,12 +838,6 @@ func continueUntilCompleteNext(t *Term, state *api.DebuggerState, op string) err } func scopePrefixSwitch(t *Term, ctx callContext) error { - if ctx.Prefix != scopePrefix { - return nil - } - if ctx.Scope.Frame != 0 { - return errors.New("frame prefix not accepted") - } if ctx.Scope.GoroutineID > 0 { _, err := t.client.SwitchGoroutine(ctx.Scope.GoroutineID) if err != nil { @@ -790,10 +854,11 @@ func exitedToError(state *api.DebuggerState, err error) (*api.DebuggerState, err return state, err } -func step(t *Term, ctx callContext, args string) error { +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 { printfileNoState(t) @@ -803,11 +868,12 @@ func step(t *Term, ctx callContext, args string) error { return continueUntilCompleteNext(t, state, "step") } -func stepInstruction(t *Term, ctx callContext, args string) error { +func (c *Commands) stepInstruction(t *Term, ctx callContext, args string) error { if err := scopePrefixSwitch(t, ctx); err != nil { return err } state, err := exitedToError(t.client.StepInstruction()) + c.frame = 0 if err != nil { printfileNoState(t) return err @@ -817,11 +883,12 @@ func stepInstruction(t *Term, ctx callContext, args string) error { return nil } -func next(t *Term, ctx callContext, args string) error { +func (c *Commands) next(t *Term, ctx callContext, args string) error { if err := scopePrefixSwitch(t, ctx); err != nil { return err } state, err := exitedToError(t.client.Next()) + c.frame = 0 if err != nil { printfileNoState(t) return err @@ -830,11 +897,12 @@ func next(t *Term, ctx callContext, args string) error { return continueUntilCompleteNext(t, state, "next") } -func stepout(t *Term, ctx callContext, args string) error { +func (c *Commands) stepout(t *Term, ctx callContext, args string) error { if err := scopePrefixSwitch(t, ctx); err != nil { return err } state, err := exitedToError(t.client.StepOut()) + c.frame = 0 if err != nil { printfileNoState(t) return err @@ -949,7 +1017,7 @@ func breakpoints(t *Term, ctx callContext, args string) error { return nil } -func setBreakpoint(t *Term, tracepoint bool, argstr string) error { +func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) error { args := strings.SplitN(argstr, " ", 2) requestedBp := &api.Breakpoint{} @@ -969,7 +1037,7 @@ func setBreakpoint(t *Term, tracepoint bool, argstr string) error { } requestedBp.Tracepoint = tracepoint - locs, err := t.client.FindLocation(api.EvalScope{GoroutineID: -1, Frame: 0}, locspec) + locs, err := t.client.FindLocation(ctx.Scope, locspec) if err != nil { if requestedBp.Name == "" { return err @@ -977,7 +1045,7 @@ func setBreakpoint(t *Term, tracepoint bool, argstr string) error { requestedBp.Name = "" locspec = argstr var err2 error - locs, err2 = t.client.FindLocation(api.EvalScope{GoroutineID: -1, Frame: 0}, locspec) + locs, err2 = t.client.FindLocation(ctx.Scope, locspec) if err2 != nil { return err } @@ -996,11 +1064,11 @@ func setBreakpoint(t *Term, tracepoint bool, argstr string) error { } func breakpoint(t *Term, ctx callContext, args string) error { - return setBreakpoint(t, false, args) + return setBreakpoint(t, ctx, false, args) } func tracepoint(t *Term, ctx callContext, args string) error { - return setBreakpoint(t, true, args) + return setBreakpoint(t, ctx, true, args) } func printVar(t *Term, ctx callContext, args string) error { @@ -1228,7 +1296,7 @@ func parseStackArgs(argstr string) (stackArgs, error) { func listCommand(t *Term, ctx callContext, args string) error { switch { - case len(args) == 0 && ctx.Prefix != scopePrefix: + case len(args) == 0 && !ctx.scoped(): state, err := t.client.GetState() if err != nil { return err @@ -1236,7 +1304,7 @@ func listCommand(t *Term, ctx callContext, args string) error { printcontext(t, state) return printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true) - case len(args) == 0 && ctx.Prefix == scopePrefix: + case len(args) == 0 && ctx.scoped(): locs, err := t.client.Stacktrace(ctx.Scope.GoroutineID, ctx.Scope.Frame, nil) if err != nil { return err diff --git a/pkg/terminal/command_test.go b/pkg/terminal/command_test.go index d6bb1717..63b1d599 100644 --- a/pkg/terminal/command_test.go +++ b/pkg/terminal/command_test.go @@ -343,10 +343,6 @@ func TestScopePrefix(t *testing.T) { term.MustExec("c") term.AssertExecError("frame", "not enough arguments") - term.AssertExecError("frame 1", "not enough arguments") - term.AssertExecError("frame 1 goroutines", "command not available") - term.AssertExecError("frame 1 goroutine", "no command passed to goroutine") - term.AssertExecError(fmt.Sprintf("frame 1 goroutine %d", curgid), "no command passed to goroutine") term.AssertExecError(fmt.Sprintf("goroutine %d frame 10 locals", curgid), fmt.Sprintf("Frame 10 does not exist in goroutine %d", curgid)) term.AssertExecError("goroutine 9000 locals", "Unknown goroutine 9000") @@ -356,6 +352,26 @@ func TestScopePrefix(t *testing.T) { term.AssertExec("frame 3 print n", "1\n") term.AssertExec("frame 4 print n", "0\n") term.AssertExecError("frame 5 print n", "could not find symbol value for n") + + term.MustExec("frame 2") + term.AssertExec("print n", "2\n") + term.MustExec("frame 4") + term.AssertExec("print n", "0\n") + term.MustExec("down") + term.AssertExec("print n", "1\n") + term.MustExec("down 2") + term.AssertExec("print n", "3\n") + term.AssertExecError("down 2", "Invalid frame -1") + term.AssertExec("print n", "3\n") + term.MustExec("up 2") + term.AssertExec("print n", "1\n") + term.AssertExecError("up 100", "Invalid frame 103") + term.AssertExec("print n", "1\n") + + term.MustExec("step") + term.AssertExecError("print n", "could not find symbol value for n") + term.MustExec("frame 2") + term.AssertExec("print n", "2\n") }) } -- GitLab