terminal,service: add API and commands for watchpoints (#2488)

Adds API calls and terminal commands to set watchpoints.
This commit is contained in:
Alessandro Arzilli 2021-05-20 19:04:02 +02:00 committed by GitHub
parent 370ce5c01c
commit 4f11320e4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 196 additions and 8 deletions

@ -34,6 +34,7 @@ Command | Description
[on](#on) | Executes a command when a breakpoint is hit.
[toggle](#toggle) | Toggles on or off a breakpoint.
[trace](#trace) | Set tracepoint.
[watch](#watch) | Set watchpoint.
## Viewing program variables and memory
@ -581,6 +582,24 @@ Print package variables.
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.
## watch
Set watchpoint.
watch [-r|-w|-rw] <expr>
-r stops when the memory location is read
-w stops when the memory location is written
-rw stops when the memory location is read or written
The memory location is specified with the same expression language used by 'print', for example:
watch v
will watch the address of variable 'v'.
See also: "help print".
## whatis
Prints type of an expression.

@ -26,6 +26,7 @@ clear_breakpoint(Id, Name) | Equivalent to API call [ClearBreakpoint](https://go
clear_checkpoint(ID) | Equivalent to API call [ClearCheckpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ClearCheckpoint)
raw_command(Name, ThreadID, GoroutineID, ReturnInfoLoadConfig, Expr, UnsafeCall) | Equivalent to API call [Command](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Command)
create_breakpoint(Breakpoint) | Equivalent to API call [CreateBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CreateBreakpoint)
create_watchpoint(Scope, Expr, Type) | Equivalent to API call [CreateWatchpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CreateWatchpoint)
detach(Kill) | Equivalent to API call [Detach](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Detach)
disassemble(Scope, StartPC, EndPC, Flavour) | Equivalent to API call [Disassemble](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Disassemble)
dump_cancel() | Equivalent to API call [DumpCancel](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.DumpCancel)

@ -35,6 +35,7 @@ type Breakpoint struct {
Name string // User defined name of the breakpoint
LogicalID int // ID of the logical breakpoint that owns this physical breakpoint
WatchExpr string
WatchType WatchType
HWBreakIndex uint8 // hardware breakpoint index
@ -316,7 +317,11 @@ func (t *Target) SetWatchpoint(scope *EvalScope, expr string, wtype WatchType, c
return nil, errors.New("can not watch stack allocated variable")
}
return t.setBreakpointInternal(xv.Addr, UserBreakpoint, wtype.withSize(uint8(sz)), cond)
bp, err := t.setBreakpointInternal(xv.Addr, UserBreakpoint, wtype.withSize(uint8(sz)), cond)
if bp != nil {
bp.WatchExpr = expr
}
return bp, err
}
func (t *Target) setBreakpointInternal(addr uint64, kind BreakpointKind, wtype WatchType, cond ast.Expr) (*Breakpoint, error) {

@ -130,6 +130,21 @@ See also: "help on", "help cond" and "help clear"`},
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/go-delve/delve/Documentation/cli/locspec.md for the syntax of linespec.
See also: "help on", "help cond" and "help clear"`},
{aliases: []string{"watch"}, group: breakCmds, cmdFn: watchpoint, helpMsg: `Set watchpoint.
watch [-r|-w|-rw] <expr>
-r stops when the memory location is read
-w stops when the memory location is written
-rw stops when the memory location is read or written
The memory location is specified with the same expression language used by 'print', for example:
watch v
will watch the address of variable 'v'.
See also: "help print".`},
{aliases: []string{"restart", "r"}, group: runCmds, cmdFn: restart, helpMsg: `Restart process.
For recorded targets the command takes the following forms:
@ -1664,6 +1679,30 @@ func edit(t *Term, ctx callContext, args string) error {
return cmd.Run()
}
func watchpoint(t *Term, ctx callContext, args string) error {
v := strings.SplitN(args, " ", 2)
if len(v) != 2 {
return errors.New("wrong number of arguments: watch [-r|-w|-rw] <expr>")
}
var wtype api.WatchType
switch v[0] {
case "-r":
wtype = api.WatchRead
case "-w":
wtype = api.WatchWrite
case "-rw":
wtype = api.WatchRead | api.WatchWrite
default:
return fmt.Errorf("wrong argument %q to watch", v[0])
}
bp, err := t.client.CreateWatchpoint(ctx.Scope, v[1], wtype)
if err != nil {
return err
}
fmt.Printf("%s set at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp))
return nil
}
func examineMemoryCmd(t *Term, ctx callContext, argstr string) error {
var (
address uint64
@ -2432,7 +2471,9 @@ func printcontextThread(t *Term, th *api.Thread) {
}
bpname := ""
if th.Breakpoint.Name != "" {
if th.Breakpoint.WatchExpr != "" {
bpname = fmt.Sprintf("watchpoint on [%s] ", th.Breakpoint.WatchExpr)
} else if th.Breakpoint.Name != "" {
bpname = fmt.Sprintf("[%s] ", th.Breakpoint.Name)
}
@ -2794,6 +2835,9 @@ func formatBreakpointName(bp *api.Breakpoint, upcase bool) string {
if bp.Tracepoint {
thing = "tracepoint"
}
if bp.WatchExpr != "" {
thing = "watchpoint"
}
if upcase {
thing = strings.Title(thing)
}
@ -2801,6 +2845,9 @@ func formatBreakpointName(bp *api.Breakpoint, upcase bool) string {
if id == "" {
id = strconv.Itoa(bp.ID)
}
if bp.WatchExpr != "" && bp.WatchExpr != bp.Name {
return fmt.Sprintf("%s %s on [%s]", thing, id, bp.WatchExpr)
}
state := "(enabled)"
if bp.Disabled {
state = "(disabled)"
@ -2822,11 +2869,13 @@ func (t *Term) formatBreakpointLocation(bp *api.Breakpoint) string {
// In case we are connecting to an older version of delve that does not return the Addrs field.
fmt.Fprintf(&out, "%#x", bp.Addr)
}
fmt.Fprintf(&out, " for ")
p := t.formatPath(bp.File)
if bp.FunctionName != "" {
fmt.Fprintf(&out, "%s() ", bp.FunctionName)
if bp.WatchExpr == "" {
fmt.Fprintf(&out, " for ")
p := t.formatPath(bp.File)
if bp.FunctionName != "" {
fmt.Fprintf(&out, "%s() ", bp.FunctionName)
}
fmt.Fprintf(&out, "%s:%d", p, bp.Line)
}
fmt.Fprintf(&out, "%s:%d", p, bp.Line)
return out.String()
}

@ -313,6 +313,54 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
r["create_watchpoint"] = starlark.NewBuiltin("create_watchpoint", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if err := isCancelled(thread); err != nil {
return starlark.None, decorateError(thread, err)
}
var rpcArgs rpc2.CreateWatchpointIn
var rpcRet rpc2.CreateWatchpointOut
if len(args) > 0 && args[0] != starlark.None {
err := unmarshalStarlarkValue(args[0], &rpcArgs.Scope, "Scope")
if err != nil {
return starlark.None, decorateError(thread, err)
}
} else {
rpcArgs.Scope = env.ctx.Scope()
}
if len(args) > 1 && args[1] != starlark.None {
err := unmarshalStarlarkValue(args[1], &rpcArgs.Expr, "Expr")
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
if len(args) > 2 && args[2] != starlark.None {
err := unmarshalStarlarkValue(args[2], &rpcArgs.Type, "Type")
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
for _, kv := range kwargs {
var err error
switch kv[0].(starlark.String) {
case "Scope":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Scope, "Scope")
case "Expr":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Expr, "Expr")
case "Type":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Type, "Type")
default:
err = fmt.Errorf("unknown argument %q", kv[0])
}
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
err := env.ctx.Client().CallAPI("CreateWatchpoint", &rpcArgs, &rpcRet)
if err != nil {
return starlark.None, err
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
r["detach"] = starlark.NewBuiltin("detach", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
if err := isCancelled(thread); err != nil {
return starlark.None, decorateError(thread, err)

@ -33,6 +33,8 @@ func ConvertBreakpoint(bp *proc.Breakpoint) *Breakpoint {
Variables: bp.Variables,
LoadArgs: LoadConfigFromProc(bp.LoadArgs),
LoadLocals: LoadConfigFromProc(bp.LoadLocals),
WatchExpr: bp.WatchExpr,
WatchType: WatchType(bp.WatchType),
TotalHitCount: bp.TotalHitCount,
Addrs: []uint64{bp.Addr},
}

@ -83,6 +83,11 @@ type Breakpoint struct {
LoadArgs *LoadConfig
// LoadLocals requests loading function locals when the breakpoint is hit
LoadLocals *LoadConfig
// WatchExpr is the expression used to create this watchpoint
WatchExpr string
WatchType WatchType
// number of times a breakpoint has been reached in a certain goroutine
HitCount map[string]uint64 `json:"hitCount"`
// number of times a breakpoint has been reached
@ -109,6 +114,14 @@ func ValidBreakpointName(name string) error {
return nil
}
// WatchType is the watchpoint type
type WatchType uint8
const (
WatchRead WatchType = 1 << iota
WatchWrite
)
// Thread is a thread within the debugged process.
type Thread struct {
// ID is a unique identifier for the thread.

@ -66,6 +66,8 @@ type Client interface {
GetBreakpointByName(name string) (*api.Breakpoint, error)
// CreateBreakpoint creates a new breakpoint.
CreateBreakpoint(*api.Breakpoint) (*api.Breakpoint, error)
// CreateWatchpoint creates a new watchpoint.
CreateWatchpoint(api.EvalScope, string, api.WatchType) (*api.Breakpoint, error)
// ListBreakpoints gets all breakpoints.
ListBreakpoints() ([]*api.Breakpoint, error)
// ClearBreakpoint deletes a breakpoint by ID.

@ -517,7 +517,9 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
if oldBp.ID > maxID {
maxID = oldBp.ID
}
if len(oldBp.File) > 0 {
if oldBp.WatchExpr != "" {
discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: oldBp, Reason: "can not recreate watchpoints on restart"})
} else if len(oldBp.File) > 0 {
addrs, err := proc.FindFileLocation(p, oldBp.File, oldBp.Line)
if err != nil {
discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: oldBp, Reason: err.Error()})
@ -527,6 +529,7 @@ func (d *Debugger) Restart(rerecord bool, pos string, resetArgs bool, newArgs []
} else {
// Avoid setting a breakpoint based on address when rebuilding
if rebuild {
discarded = append(discarded, api.DiscardedBreakpoint{Breakpoint: oldBp, Reason: "can not recreate address breakpoints on restart"})
continue
}
newBp, err := p.SetBreakpointWithID(oldBp.ID, oldBp.Addr)
@ -730,6 +733,11 @@ func (d *Debugger) AmendBreakpoint(amend *api.Breakpoint) error {
defer d.targetMutex.Unlock()
originals := d.findBreakpoint(amend.ID)
if len(originals) > 0 && originals[0].WatchExpr != "" && amend.Disabled {
return errors.New("can not disable watchpoints")
}
_, disabled := d.disabledBreakpoints[amend.ID]
if originals == nil && !disabled {
return fmt.Errorf("no breakpoint with ID %d", amend.ID)
@ -938,6 +946,22 @@ func (d *Debugger) findDisabledBreakpointByName(name string) *api.Breakpoint {
return nil
}
// CreateWatchpoint creates a watchpoint on the specified expression.
func (d *Debugger) CreateWatchpoint(goid, frame, deferredCall int, expr string, wtype api.WatchType) (*api.Breakpoint, error) {
s, err := proc.ConvertEvalScope(d.target, goid, frame, deferredCall)
if err != nil {
return nil, err
}
bp, err := d.target.SetWatchpoint(s, expr, proc.WatchType(wtype), nil)
if err != nil {
return nil, err
}
if api.ValidBreakpointName(expr) == nil && d.findBreakpointByName(expr) == nil {
bp.Name = expr
}
return api.ConvertBreakpoint(bp), nil
}
// Threads returns the threads of the target process.
func (d *Debugger) Threads() ([]proc.Thread, error) {
d.targetMutex.Lock()
@ -1889,6 +1913,9 @@ func (v breakpointsByLogicalID) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
func (v breakpointsByLogicalID) Less(i, j int) bool {
if v[i].LogicalID == v[j].LogicalID {
if v[i].WatchType != v[j].WatchType {
return v[i].WatchType > v[j].WatchType // if a logical breakpoint contains a watchpoint let the watchpoint sort first
}
return v[i].Addr < v[j].Addr
}
return v[i].LogicalID < v[j].LogicalID

@ -232,6 +232,12 @@ func (c *RPCClient) CreateBreakpoint(breakPoint *api.Breakpoint) (*api.Breakpoin
return &out.Breakpoint, err
}
func (c *RPCClient) CreateWatchpoint(scope api.EvalScope, expr string, wtype api.WatchType) (*api.Breakpoint, error) {
var out CreateWatchpointOut
err := c.call("CreateWatchpoint", CreateWatchpointIn{scope, expr, wtype}, &out)
return out.Breakpoint, err
}
func (c *RPCClient) ListBreakpoints() ([]*api.Breakpoint, error) {
var out ListBreakpointsOut
err := c.call("ListBreakpoints", ListBreakpointsIn{}, &out)

@ -939,3 +939,19 @@ type DumpCancelOut struct {
func (s *RPCServer) DumpCancel(arg DumpCancelIn, out *DumpCancelOut) error {
return s.debugger.DumpCancel()
}
type CreateWatchpointIn struct {
Scope api.EvalScope
Expr string
Type api.WatchType
}
type CreateWatchpointOut struct {
*api.Breakpoint
}
func (s *RPCServer) CreateWatchpoint(arg CreateWatchpointIn, out *CreateWatchpointOut) error {
var err error
out.Breakpoint, err = s.debugger.CreateWatchpoint(arg.Scope.GoroutineID, arg.Scope.Frame, arg.Scope.DeferredCall, arg.Expr, arg.Type)
return err
}