提交 3be65a4c 编写于 作者: A aarzilli 提交者: Derek Parker

service, terminal: Named breakpoints and breakpoint conditions

Implements #109 and #120
上级 8f85b6cb
......@@ -19,6 +19,7 @@ type Breakpoint struct {
Addr uint64 // Address breakpoint is set for.
OriginalData []byte // If software breakpoint, the data we replace with breakpoint instruction.
Name string // User defined name of the breakpoint
ID int // Monotonically increasing ID.
Temp bool // Whether this is a temp breakpoint (for next'ing).
......
package api
import (
"bytes"
"debug/gosym"
"go/constant"
"go/printer"
"go/token"
"golang.org/x/debug/dwarf"
"reflect"
"strconv"
......@@ -14,6 +17,7 @@ import (
// an api.Breakpoint.
func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
b := &Breakpoint{
Name: bp.Name,
ID: bp.ID,
FunctionName: bp.FunctionName,
File: bp.File,
......@@ -31,6 +35,10 @@ func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
b.HitCount[strconv.Itoa(idx)] = bp.HitCount[idx]
}
var buf bytes.Buffer
printer.Fprint(&buf, token.NewFileSet(), bp.Cond)
b.Cond = buf.String()
return b
}
......
package api
import (
"errors"
"fmt"
"github.com/derekparker/delve/proc"
"reflect"
"strconv"
"unicode"
)
// DebuggerState represents the current context of the debugger.
......@@ -25,6 +29,8 @@ type DebuggerState struct {
type Breakpoint struct {
// ID is a unique identifier for the breakpoint.
ID int `json:"id"`
// User defined name of the breakpoint
Name string `json:"name"`
// Addr is the address of the breakpoint.
Addr uint64 `json:"addr"`
// File is the source file for the breakpoint.
......@@ -35,6 +41,9 @@ type Breakpoint struct {
// may not always be available.
FunctionName string `json:"functionName,omitempty"`
// Breakpoint condition
Cond string
// tracepoint flag
Tracepoint bool `json:"continue"`
// number of stack frames to retrieve
......@@ -49,6 +58,20 @@ type Breakpoint struct {
TotalHitCount uint64 `json:"totalHitCount"`
}
func ValidBreakpointName(name string) error {
if _, err := strconv.Atoi(name); err == nil {
return errors.New("breakpoint name can not be a number")
}
for _, ch := range name {
if !(unicode.IsLetter(ch) || unicode.IsDigit(ch)) {
return fmt.Errorf("invalid character in breakpoint name '%c'", ch)
}
}
return nil
}
// Thread is a thread within the debugged process.
type Thread struct {
// ID is a unique identifier for the thread.
......
......@@ -36,12 +36,19 @@ type Client interface {
// GetBreakpoint gets a breakpoint by ID.
GetBreakpoint(id int) (*api.Breakpoint, error)
// GetBreakpointByName gets a breakpoint by name.
GetBreakpointByName(name string) (*api.Breakpoint, error)
// CreateBreakpoint creates a new breakpoint.
CreateBreakpoint(*api.Breakpoint) (*api.Breakpoint, error)
// ListBreakpoints gets all breakpoints.
ListBreakpoints() ([]*api.Breakpoint, error)
// ClearBreakpoint deletes a breakpoint by ID.
ClearBreakpoint(id int) (*api.Breakpoint, error)
// ClearBreakpointByName deletes a breakpoint by name
ClearBreakpointByName(name string) (*api.Breakpoint, error)
// Allows user to update an existing breakpoint for example to change the information
// retrieved when the breakpoint is hit or to change, add or remove the break condition
AmendBreakpoint(*api.Breakpoint) error
// ListThreads lists all threads.
ListThreads() ([]*api.Thread, error)
......
......@@ -4,6 +4,7 @@ import (
"debug/gosym"
"errors"
"fmt"
"go/parser"
"log"
"path/filepath"
"regexp"
......@@ -100,11 +101,12 @@ func (d *Debugger) Restart() error {
if err != nil {
return fmt.Errorf("could not launch process: %s", err)
}
for addr, bp := range d.process.Breakpoints {
if bp.Temp {
continue
for _, oldBp := range d.Breakpoints() {
newBp, err := p.SetBreakpoint(oldBp.Addr)
if err != nil {
return err
}
if _, err := p.SetBreakpoint(addr); err != nil {
if err := copyBreakpointInfo(newBp, oldBp); err != nil {
return err
}
}
......@@ -150,6 +152,16 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin
addr uint64
err error
)
if requestedBp.Name != "" {
if err = api.ValidBreakpointName(requestedBp.Name); err != nil {
return nil, err
}
if d.FindBreakpointByName(requestedBp.Name) != nil {
return nil, errors.New("breakpoint name already exists")
}
}
switch {
case len(requestedBp.File) > 0:
fileName := requestedBp.File
......@@ -182,16 +194,41 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin
if err != nil {
return nil, err
}
bp.Tracepoint = requestedBp.Tracepoint
bp.Goroutine = requestedBp.Goroutine
bp.Stacktrace = requestedBp.Stacktrace
bp.Variables = requestedBp.Variables
bp.Cond = nil
if err := copyBreakpointInfo(bp, requestedBp); err != nil {
if _, err1 := d.process.ClearBreakpoint(bp.Addr); err1 != nil {
err = fmt.Errorf("error while creating breakpoint: %v, additionally the breakpoint could not be properly rolled back: %v", err, err1)
}
return nil, err
}
createdBp = api.ConvertBreakpoint(bp)
log.Printf("created breakpoint: %#v", createdBp)
return createdBp, nil
}
func (d *Debugger) AmendBreakpoint(amend *api.Breakpoint) error {
original := d.findBreakpoint(amend.ID)
if original == nil {
return fmt.Errorf("no breakpoint with ID %d", amend.ID)
}
if err := api.ValidBreakpointName(amend.Name); err != nil {
return err
}
return copyBreakpointInfo(original, amend)
}
func copyBreakpointInfo(bp *proc.Breakpoint, requested *api.Breakpoint) (err error) {
bp.Name = requested.Name
bp.Tracepoint = requested.Tracepoint
bp.Goroutine = requested.Goroutine
bp.Stacktrace = requested.Stacktrace
bp.Variables = requested.Variables
bp.Cond = nil
if requested.Cond != "" {
bp.Cond, err = parser.ParseExpr(requested.Cond)
}
return err
}
// ClearBreakpoint clears a breakpoint.
func (d *Debugger) ClearBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoint, error) {
var clearedBp *api.Breakpoint
......@@ -218,7 +255,15 @@ func (d *Debugger) Breakpoints() []*api.Breakpoint {
// FindBreakpoint returns the breakpoint specified by 'id'.
func (d *Debugger) FindBreakpoint(id int) *api.Breakpoint {
for _, bp := range d.Breakpoints() {
bp := d.findBreakpoint(id)
if bp == nil {
return nil
}
return api.ConvertBreakpoint(bp)
}
func (d *Debugger) findBreakpoint(id int) *proc.Breakpoint {
for _, bp := range d.process.Breakpoints {
if bp.ID == id {
return bp
}
......@@ -226,6 +271,16 @@ func (d *Debugger) FindBreakpoint(id int) *api.Breakpoint {
return nil
}
// FindBreakpointByName returns the breakpoint specified by 'name'
func (d *Debugger) FindBreakpointByName(name string) *api.Breakpoint {
for _, bp := range d.Breakpoints() {
if bp.Name == name {
return bp
}
}
return nil
}
// Threads returns the threads of the target process.
func (d *Debugger) Threads() ([]*api.Thread, error) {
if d.process.Exited() {
......
......@@ -139,6 +139,12 @@ func (c *RPCClient) GetBreakpoint(id int) (*api.Breakpoint, error) {
return breakpoint, err
}
func (c *RPCClient) GetBreakpointByName(name string) (*api.Breakpoint, error) {
breakpoint := new(api.Breakpoint)
err := c.call("GetBreakpointByName", name, breakpoint)
return breakpoint, err
}
func (c *RPCClient) CreateBreakpoint(breakPoint *api.Breakpoint) (*api.Breakpoint, error) {
newBreakpoint := new(api.Breakpoint)
err := c.call("CreateBreakpoint", breakPoint, &newBreakpoint)
......@@ -157,6 +163,17 @@ func (c *RPCClient) ClearBreakpoint(id int) (*api.Breakpoint, error) {
return bp, err
}
func (c *RPCClient) ClearBreakpointByName(name string) (*api.Breakpoint, error) {
bp := new(api.Breakpoint)
err := c.call("ClearBreakpointByName", name, bp)
return bp, err
}
func (c *RPCClient) AmendBreakpoint(bp *api.Breakpoint) error {
err := c.call("AmendBreakpoint", bp, nil)
return err
}
func (c *RPCClient) ListThreads() ([]*api.Thread, error) {
var threads []*api.Thread
err := c.call("ListThreads", nil, &threads)
......
......@@ -121,6 +121,15 @@ func (s *RPCServer) GetBreakpoint(id int, breakpoint *api.Breakpoint) error {
return nil
}
func (s *RPCServer) GetBreakpointByName(name string, breakpoint *api.Breakpoint) error {
bp := s.debugger.FindBreakpointByName(name)
if bp == nil {
return fmt.Errorf("no breakpoint with name %s", name)
}
*breakpoint = *bp
return nil
}
type StacktraceGoroutineArgs struct {
Id int
Depth int
......@@ -163,6 +172,24 @@ func (s *RPCServer) ClearBreakpoint(id int, breakpoint *api.Breakpoint) error {
return nil
}
func (s *RPCServer) ClearBreakpointByName(name string, breakpoint *api.Breakpoint) error {
bp := s.debugger.FindBreakpointByName(name)
if bp == nil {
return fmt.Errorf("no breakpoint with name %s", name)
}
deleted, err := s.debugger.ClearBreakpoint(bp)
if err != nil {
return err
}
*breakpoint = *deleted
return nil
}
func (s *RPCServer) AmendBreakpoint(amend *api.Breakpoint, unused *int) error {
*unused = 0
return s.debugger.AmendBreakpoint(amend)
}
func (s *RPCServer) ListThreads(arg interface{}, threads *[]*api.Thread) (err error) {
*threads, err = s.debugger.Threads()
return err
......
......@@ -97,6 +97,35 @@ func TestRestart_afterExit(t *testing.T) {
})
}
func TestRestart_breakpointPreservation(t *testing.T) {
withTestClient("continuetestprog", t, func(c service.Client) {
_, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.main", Line: 1, Name: "firstbreakpoint", Tracepoint: true})
assertNoError(err, t, "CreateBreakpoint()")
stateCh := c.Continue()
state := <- stateCh
if state.CurrentThread.Breakpoint.Name != "firstbreakpoint" || !state.CurrentThread.Breakpoint.Tracepoint {
t.Fatalf("Wrong breakpoint: %#v\n", state.CurrentThread.Breakpoint)
}
state = <- stateCh
if !state.Exited {
t.Fatal("Did not exit after first tracepoint")
}
t.Log("Restart")
c.Restart()
stateCh = c.Continue()
state = <- stateCh
if state.CurrentThread.Breakpoint.Name != "firstbreakpoint" || !state.CurrentThread.Breakpoint.Tracepoint {
t.Fatalf("Wrong breakpoint (after restart): %#v\n", state.CurrentThread.Breakpoint)
}
state = <- stateCh
if !state.Exited {
t.Fatal("Did not exit after first tracepoint (after restart)")
}
})
}
func TestRestart_duringStop(t *testing.T) {
withTestClient("continuetestprog", t, func(c service.Client) {
origPid := c.ProcessPid()
......@@ -966,3 +995,33 @@ func TestNegativeStackDepthBug(t *testing.T) {
assertError(err, t, "Stacktrace()")
})
}
func TestClientServer_CondBreakpoint(t *testing.T) {
withTestClient("parallel_next", t, func(c service.Client) {
bp, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.sayhi", Line: 1})
assertNoError(err, t, "CreateBreakpoint()")
bp.Cond = "n == 7"
assertNoError(c.AmendBreakpoint(bp), t, "AmendBreakpoint() 1")
bp, err = c.GetBreakpoint(bp.ID)
assertNoError(err, t, "GetBreakpoint() 1")
bp.Variables = append(bp.Variables, "n")
assertNoError(c.AmendBreakpoint(bp), t, "AmendBreakpoint() 2")
bp, err = c.GetBreakpoint(bp.ID)
assertNoError(err, t, "GetBreakpoint() 2")
if bp.Cond == "" {
t.Fatalf("No condition set on breakpoint %#v", bp)
}
if len(bp.Variables) != 1 {
t.Fatalf("Wrong number of expressions to evaluate on breakpoint %#v", bp)
}
state := <-c.Continue()
assertNoError(state.Err, t, "Continue()")
nvar, err := c.EvalVariable(api.EvalScope{-1, 0}, "n")
assertNoError(err, t, "EvalVariable()")
if nvar.SinglelineString() != "7" {
t.Fatalf("Stopped on wrong goroutine %s\n", nvar.Value)
}
})
}
......@@ -57,7 +57,7 @@ func DebugCommands(client service.Client) *Commands {
c.cmds = []command{
{aliases: []string{"help"}, cmdFn: c.help, helpMsg: "Prints the help message."},
{aliases: []string{"break", "b"}, cmdFn: breakpoint, helpMsg: "break <linespec> [-stack <n>|-goroutine|<variable name>]*"},
{aliases: []string{"break", "b"}, cmdFn: breakpoint, helpMsg: "break [name] <linespec>"},
{aliases: []string{"trace", "t"}, cmdFn: tracepoint, helpMsg: "Set tracepoint, takes the same arguments as break."},
{aliases: []string{"restart", "r"}, cmdFn: restart, helpMsg: "Restart process."},
{aliases: []string{"continue", "c"}, cmdFn: cont, helpMsg: "Run until breakpoint or program termination."},
......@@ -85,6 +85,8 @@ func DebugCommands(client service.Client) *Commands {
{aliases: []string{"frame"}, cmdFn: frame, helpMsg: "Sets current stack frame (0 is the top of the stack)"},
{aliases: []string{"source"}, cmdFn: c.sourceCommand, helpMsg: "Executes a file containing a list of delve commands"},
{aliases: []string{"disassemble", "disass"}, cmdFn: g0f0(disassCommand), helpMsg: "Displays disassembly of specific function or address range: disassemble [-a <start> <end>] [-l <locspec>]"},
{aliases: []string{"on"}, cmdFn: onCmd, helpMsg: "on <breakpoint name or id> <command>. Executes command when the specified breakpoint is hit (supported commands: print <expression>, stack [<depth>] [-full] and goroutine)"},
{aliases: []string{"condition", "cond"}, cmdFn: conditionCmd, helpMsg: "cond <breakpoint name or id> <boolean expression>. Specifies that the breakpoint or tracepoint should break only if the boolean expression is true."},
}
return c
......@@ -499,14 +501,16 @@ func clear(t *Term, args string) error {
return fmt.Errorf("not enough arguments")
}
id, err := strconv.Atoi(args)
if err != nil {
return err
var bp *api.Breakpoint
if err == nil {
bp, err = t.client.ClearBreakpoint(id)
} else {
bp, err = t.client.ClearBreakpointByName(args)
}
bp, err := t.client.ClearBreakpoint(id)
if err != nil {
return err
}
fmt.Printf("Breakpoint %d cleared at %#v for %s %s:%d\n", bp.ID, bp.Addr, bp.FunctionName, ShortenFilePath(bp.File), bp.Line)
fmt.Printf("%s cleared at %s\n", formatBreakpointName(bp, true), formatBreakpointLocation(bp))
return nil
}
......@@ -537,9 +541,9 @@ func clearAll(t *Term, args string) error {
_, err := t.client.ClearBreakpoint(bp.ID)
if err != nil {
fmt.Printf("Couldn't delete breakpoint %d at %#v %s:%d: %s\n", bp.ID, bp.Addr, ShortenFilePath(bp.File), bp.Line, err)
fmt.Printf("Couldn't delete %s at %s: %s\n", formatBreakpointName(bp, false), formatBreakpointLocation(bp), err)
}
fmt.Printf("Breakpoint %d cleared at %#v for %s %s:%d\n", bp.ID, bp.Addr, bp.FunctionName, ShortenFilePath(bp.File), bp.Line)
fmt.Printf("%s cleared at %s\n", formatBreakpointName(bp, true), formatBreakpointLocation(bp))
}
return nil
}
......@@ -558,61 +562,60 @@ func breakpoints(t *Term, args string) error {
}
sort.Sort(ByID(breakPoints))
for _, bp := range breakPoints {
thing := "Breakpoint"
if bp.Tracepoint {
thing = "Tracepoint"
}
fmt.Printf("%s %d at %#v %s:%d (%d)\n", thing, bp.ID, bp.Addr, ShortenFilePath(bp.File), bp.Line, bp.TotalHitCount)
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, "-stack")
attrs = append(attrs, strconv.Itoa(bp.Stacktrace))
attrs = append(attrs, fmt.Sprintf("\tstack %d", bp.Stacktrace))
}
if bp.Goroutine {
attrs = append(attrs, "-goroutine")
attrs = append(attrs, "\tgoroutine")
}
for i := range bp.Variables {
attrs = append(attrs, bp.Variables[i])
attrs = append(attrs, fmt.Sprintf("\tprint %s", bp.Variables[i]))
}
if len(attrs) > 0 {
fmt.Printf("\t%s\n", strings.Join(attrs, " "))
fmt.Printf("%s\n", strings.Join(attrs, "\n"))
}
}
return nil
}
func setBreakpoint(t *Term, tracepoint bool, argstr string) error {
args := strings.Split(argstr, " ")
if len(args) < 1 {
return fmt.Errorf("address required, specify either a function name or <file:line>")
}
requestedBp := &api.Breakpoint{}
args := strings.SplitN(argstr, " ", 2)
for i := 1; i < len(args); i++ {
switch args[i] {
case "-stack":
i++
n, err := strconv.Atoi(args[i])
if err != nil {
return fmt.Errorf("argument of -stack must be a number")
}
requestedBp.Stacktrace = n
case "-goroutine":
requestedBp.Goroutine = true
default:
requestedBp.Variables = append(requestedBp.Variables, args[i])
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(api.EvalScope{GoroutineID: -1, Frame: 0}, args[0])
locs, err := t.client.FindLocation(api.EvalScope{GoroutineID: -1, Frame: 0}, locspec)
if err != nil {
return err
}
thing := "Breakpoint"
if tracepoint {
thing = "Tracepoint"
if requestedBp.Name == "" {
return err
}
requestedBp.Name = ""
locspec = argstr
var err2 error
locs, err2 = t.client.FindLocation(api.EvalScope{-1, 0}, locspec)
if err2 != nil {
return err
}
}
for _, loc := range locs {
requestedBp.Addr = loc.PC
......@@ -622,7 +625,7 @@ func setBreakpoint(t *Term, tracepoint bool, argstr string) error {
return err
}
fmt.Printf("%s %d set at %#v for %s %s:%d\n", thing, bp.ID, bp.Addr, bp.FunctionName, ShortenFilePath(bp.File), bp.Line)
fmt.Printf("%s set at %s\n", formatBreakpointName(bp, true), formatBreakpointLocation(bp))
}
return nil
}
......@@ -960,8 +963,14 @@ func printcontextThread(t *Term, th *api.Thread) {
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:%d (hits goroutine(%d):%d total:%d) (PC: %#v)\n",
fmt.Printf("> %s%s(%s) %s:%d (hits goroutine(%d):%d total:%d) (PC: %#v)\n",
bpname,
fn.Name,
args,
ShortenFilePath(th.File),
......@@ -971,7 +980,8 @@ func printcontextThread(t *Term, th *api.Thread) {
th.Breakpoint.TotalHitCount,
th.PC)
} else {
fmt.Printf("> %s(%s) %s:%d (hits total:%d) (PC: %#v)\n",
fmt.Printf("> %s%s(%s) %s:%d (hits total:%d) (PC: %#v)\n",
bpname,
fn.Name,
args,
ShortenFilePath(th.File),
......@@ -1053,6 +1063,65 @@ func exitCommand(t *Term, args string) error {
return ExitRequestError{}
}
func getBreakpointByIDOrName(t *Term, arg string) (bp *api.Breakpoint, err error) {
if id, err := strconv.Atoi(arg); err == nil {
bp, err = t.client.GetBreakpoint(id)
} else {
bp, err = t.client.GetBreakpointByName(arg)
}
return
}
func onCmd(t *Term, argstr string) error {
args := strings.SplitN(argstr, " ", 3)
if len(args) < 2 {
return fmt.Errorf("not enough arguments")
}
bp, err := getBreakpointByIDOrName(t, args[0])
if err != nil {
return err
}
switch args[1] {
case "p", "print":
if len(args) < 3 {
return fmt.Errorf("not enough arguments")
}
bp.Variables = append(bp.Variables, args[2])
case "stack", "bt":
depth, _, err := parseStackArgs(args[2])
if err != nil {
return err
}
bp.Stacktrace = depth
case "goroutine":
if len(args) != 2 {
return fmt.Errorf("too many arguments")
}
bp.Goroutine = true
}
return t.client.AmendBreakpoint(bp)
}
func conditionCmd(t *Term, 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 {
......@@ -1088,3 +1157,27 @@ func (c *Commands) executeFile(t *Term, name string) error {
return scanner.Err()
}
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)
} else {
return fmt.Sprintf("%#v for %s:%d", bp.Addr, p, bp.Line)
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册