proc,terminal,service: support stack watchpoints (#2521)

* terminal,service: add way to see internal breakpoints

Now that Delve has internal breakpoints that survive for long periods
of time it will be useful to have an option to display them.

* proc,terminal,service: support stack watchpoints

Adds support for watchpoints on stack allocated variables.

When a stack variable is watched, in addition to the normal watchpoint
some support breakpoints are created:

- one breakpoint inside runtime.copystack, used to adjust the address
  of the watchpoint when the stack is resized
- one or more breakpoints used to detect when the stack variable goes
  out of scope, those are similar to the breakpoints set by StepOut.

Implements #279
This commit is contained in:
Alessandro Arzilli 2021-08-09 19:41:25 +02:00 committed by GitHub
parent f3e76238e3
commit 4264bf00f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 480 additions and 52 deletions

@ -1,31 +1,31 @@
Tests skipped by each supported backend: Tests skipped by each supported backend:
* 386 skipped = 6 * 386 skipped = 7
* 1 broken * 1 broken
* 3 broken - cgo stacktraces * 3 broken - cgo stacktraces
* 2 not implemented * 3 not implemented
* arm64 skipped = 4 * arm64 skipped = 5
* 1 broken * 1 broken
* 1 broken - global variable symbolication * 1 broken - global variable symbolication
* 2 not implemented * 3 not implemented
* darwin skipped = 2 * darwin skipped = 3
* 2 not implemented * 3 not implemented
* darwin/arm64 skipped = 1 * darwin/arm64 skipped = 1
* 1 broken - cgo stacktraces * 1 broken - cgo stacktraces
* darwin/lldb skipped = 1 * darwin/lldb skipped = 1
* 1 upstream issue * 1 upstream issue
* freebsd skipped = 14 * freebsd skipped = 15
* 11 broken * 11 broken
* 3 not implemented * 4 not implemented
* linux/386/pie skipped = 1 * linux/386/pie skipped = 1
* 1 broken * 1 broken
* linux/arm64 skipped = 1 * linux/arm64 skipped = 1
* 1 broken - cgo stacktraces * 1 broken - cgo stacktraces
* pie skipped = 2 * pie skipped = 2
* 2 upstream issue - https://github.com/golang/go/issues/29322 * 2 upstream issue - https://github.com/golang/go/issues/29322
* rr skipped = 2 * rr skipped = 3
* 2 not implemented * 3 not implemented
* windows skipped = 4 * windows skipped = 5
* 1 broken * 1 broken
* 2 not implemented * 3 not implemented
* 1 upstream issue * 1 upstream issue

@ -114,6 +114,10 @@ Aliases: b
## breakpoints ## breakpoints
Print out info for active breakpoints. Print out info for active breakpoints.
breakpoints [-a]
Specifying -a prints all physical breakpoint, including internal breakpoints.
Aliases: bp Aliases: bp

@ -42,7 +42,7 @@ get_buffered_tracepoints() | Equivalent to API call [GetBufferedTracepoints](htt
get_thread(Id) | Equivalent to API call [GetThread](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.GetThread) get_thread(Id) | Equivalent to API call [GetThread](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.GetThread)
is_multiclient() | Equivalent to API call [IsMulticlient](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.IsMulticlient) is_multiclient() | Equivalent to API call [IsMulticlient](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.IsMulticlient)
last_modified() | Equivalent to API call [LastModified](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.LastModified) last_modified() | Equivalent to API call [LastModified](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.LastModified)
breakpoints() | Equivalent to API call [ListBreakpoints](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListBreakpoints) breakpoints(All) | Equivalent to API call [ListBreakpoints](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListBreakpoints)
checkpoints() | Equivalent to API call [ListCheckpoints](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListCheckpoints) checkpoints() | Equivalent to API call [ListCheckpoints](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListCheckpoints)
dynamic_libraries() | Equivalent to API call [ListDynamicLibraries](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListDynamicLibraries) dynamic_libraries() | Equivalent to API call [ListDynamicLibraries](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListDynamicLibraries)
function_args(Scope, Cfg) | Equivalent to API call [ListFunctionArgs](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListFunctionArgs) function_args(Scope, Cfg) | Equivalent to API call [ListFunctionArgs](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListFunctionArgs)

25
_fixtures/databpstack.go Normal file

@ -0,0 +1,25 @@
package main
import (
"fmt"
"runtime"
)
func f() {
w := 0
runtime.Breakpoint()
g(1000, &w) // Position 0
}
func g(cnt int, p *int) {
if cnt == 0 {
*p = 10
return // Position 1
}
g(cnt-1, p)
}
func main() {
f()
fmt.Printf("done\n") // Position 2
}

@ -42,9 +42,10 @@ type Breakpoint struct {
Name string // User defined name of the breakpoint Name string // User defined name of the breakpoint
LogicalID int // ID of the logical breakpoint that owns this physical breakpoint LogicalID int // ID of the logical breakpoint that owns this physical breakpoint
WatchExpr string WatchExpr string
WatchType WatchType WatchType WatchType
HWBreakIndex uint8 // hardware breakpoint index HWBreakIndex uint8 // hardware breakpoint index
watchStackOff int64 // for watchpoints of stack variables, offset of the address from top of the stack
// Breaklets is the list of overlapping breakpoints on this physical breakpoint. // Breaklets is the list of overlapping breakpoints on this physical breakpoint.
// There can be at most one UserBreakpoint in this list but multiple internal breakpoints are allowed. // There can be at most one UserBreakpoint in this list but multiple internal breakpoints are allowed.
@ -94,6 +95,20 @@ type Breaklet struct {
Op token.Token Op token.Token
Val int Val int
} }
// checkPanicCall checks that the breakpoint happened while the function was
// called by a panic. It is only checked for WatchOutOfScopeBreakpoint Kind.
checkPanicCall bool
// callback is called if every other condition for this breaklet is met,
// the return value will determine if the breaklet should be considered
// active.
// The callback can have side-effects.
callback func(th Thread) bool
// For WatchOutOfScopeBreakpoints and StackResizeBreakpoints the watchpoint
// field contains the watchpoint related to this out of scope sentinel.
watchpoint *Breakpoint
} }
// BreakpointKind determines the behavior of delve when the // BreakpointKind determines the behavior of delve when the
@ -116,6 +131,14 @@ const (
// destination of CALL, delete this breakpoint and then continue again // destination of CALL, delete this breakpoint and then continue again
StepBreakpoint StepBreakpoint
// WatchOutOfScopeBreakpoint is a breakpoint used to detect when a watched
// stack variable goes out of scope.
WatchOutOfScopeBreakpoint
// StackResizeBreakpoint is a breakpoint used to detect stack resizes to
// adjust the watchpoint of stack variables.
StackResizeBreakpoint
steppingMask = NextBreakpoint | NextDeferBreakpoint | StepBreakpoint steppingMask = NextBreakpoint | NextDeferBreakpoint | StepBreakpoint
) )
@ -153,6 +176,38 @@ func (bp *Breakpoint) String() string {
return fmt.Sprintf("Breakpoint %d at %#v %s:%d", bp.LogicalID, bp.Addr, bp.File, bp.Line) return fmt.Sprintf("Breakpoint %d at %#v %s:%d", bp.LogicalID, bp.Addr, bp.File, bp.Line)
} }
// VerboseDescr returns a string describing parts of the breakpoint struct
// that aren't otherwise user visible, for debugging purposes.
func (bp *Breakpoint) VerboseDescr() []string {
r := []string{}
r = append(r, fmt.Sprintf("OriginalData=%#x", bp.OriginalData))
if bp.WatchType != 0 {
r = append(r, fmt.Sprintf("HWBreakIndex=%#x watchStackOff=%#x", bp.HWBreakIndex, bp.watchStackOff))
}
for _, breaklet := range bp.Breaklets {
switch breaklet.Kind {
case UserBreakpoint:
r = append(r, fmt.Sprintf("User Cond=%q HitCond=%v", exprToString(breaklet.Cond), breaklet.HitCond))
case NextBreakpoint:
r = append(r, fmt.Sprintf("Next Cond=%q", exprToString(breaklet.Cond)))
case NextDeferBreakpoint:
r = append(r, fmt.Sprintf("NextDefer Cond=%q DeferReturns=%#x", exprToString(breaklet.Cond), breaklet.DeferReturns))
case StepBreakpoint:
r = append(r, fmt.Sprintf("Step Cond=%q", exprToString(breaklet.Cond)))
case WatchOutOfScopeBreakpoint:
r = append(r, fmt.Sprintf("WatchOutOfScope Cond=%q checkPanicCall=%v", exprToString(breaklet.Cond), breaklet.checkPanicCall))
case StackResizeBreakpoint:
r = append(r, fmt.Sprintf("StackResizeBreakpoint Cond=%q", exprToString(breaklet.Cond)))
default:
r = append(r, fmt.Sprintf("Unknown %d", breaklet.Kind))
}
}
return r
}
// BreakpointExistsError is returned when trying to set a breakpoint at // BreakpointExistsError is returned when trying to set a breakpoint at
// an address that already has a breakpoint set for it. // an address that already has a breakpoint set for it.
type BreakpointExistsError struct { type BreakpointExistsError struct {
@ -232,12 +287,27 @@ func (bpstate *BreakpointState) checkCond(tgt *Target, breaklet *Breaklet, threa
} }
} }
case WatchOutOfScopeBreakpoint:
if breaklet.checkPanicCall {
frames, err := ThreadStacktrace(thread, 2)
if err == nil {
ipc, _ := isPanicCall(frames)
active = active && ipc
}
}
case StackResizeBreakpoint:
// no further checks
default: default:
bpstate.CondError = fmt.Errorf("internal error unknown breakpoint kind %v", breaklet.Kind) bpstate.CondError = fmt.Errorf("internal error unknown breakpoint kind %v", breaklet.Kind)
} }
if active { if active {
bpstate.Active = true if breaklet.callback != nil {
active = breaklet.callback(thread)
}
bpstate.Active = active
} }
} }
@ -372,6 +442,10 @@ func (nbp NoBreakpointError) Error() string {
type BreakpointMap struct { type BreakpointMap struct {
M map[uint64]*Breakpoint M map[uint64]*Breakpoint
// WatchOutOfScope is the list of watchpoints that went out of scope during
// the last resume operation
WatchOutOfScope []*Breakpoint
breakpointIDCounter int breakpointIDCounter int
internalBreakpointIDCounter int internalBreakpointIDCounter int
} }
@ -466,16 +540,31 @@ func (t *Target) SetWatchpoint(scope *EvalScope, expr string, wtype WatchType, c
//member fields here. //member fields here.
return nil, fmt.Errorf("can not watch variable of type %s", xv.DwarfType.String()) return nil, fmt.Errorf("can not watch variable of type %s", xv.DwarfType.String())
} }
if xv.Addr >= scope.g.stack.lo && xv.Addr < scope.g.stack.hi {
//TODO(aarzilli): support watching stack variables stackWatch := scope.g != nil && !scope.g.SystemStack && xv.Addr >= scope.g.stack.lo && xv.Addr < scope.g.stack.hi
return nil, errors.New("can not watch stack allocated variable")
if stackWatch && wtype&WatchRead != 0 {
// In theory this would work except for the fact that the runtime will
// read them randomly to resize stacks so it doesn't make sense to do
// this.
return nil, errors.New("can not watch stack allocated variable for reads")
} }
bp, err := 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 { if err != nil {
bp.WatchExpr = expr return bp, err
} }
return bp, err bp.WatchExpr = expr
if stackWatch {
bp.watchStackOff = int64(bp.Addr) - int64(scope.g.stack.hi)
err := t.setStackWatchBreakpoints(scope, bp)
if err != nil {
return bp, err
}
}
return bp, nil
} }
func (t *Target) setBreakpointInternal(addr uint64, kind BreakpointKind, wtype WatchType, cond ast.Expr) (*Breakpoint, error) { func (t *Target) setBreakpointInternal(addr uint64, kind BreakpointKind, wtype WatchType, cond ast.Expr) (*Breakpoint, error) {
@ -588,6 +677,15 @@ func (t *Target) ClearBreakpoint(addr uint64) (*Breakpoint, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
if bp.WatchExpr != "" && bp.watchStackOff != 0 {
// stack watchpoint, must remove all its WatchOutOfScopeBreakpoints/StackResizeBreakpoints
err := t.clearStackWatchBreakpoints(bp)
if err != nil {
return bp, err
}
}
return bp, nil return bp, nil
} }

@ -5448,3 +5448,72 @@ func TestDwrapStartLocation(t *testing.T) {
} }
}) })
} }
func TestWatchpointStack(t *testing.T) {
skipOn(t, "not implemented", "windows")
skipOn(t, "not implemented", "freebsd")
skipOn(t, "not implemented", "darwin")
skipOn(t, "not implemented", "386")
skipOn(t, "not implemented", "arm64")
skipOn(t, "not implemented", "rr")
withTestProcess("databpstack", t, func(p *proc.Target, fixture protest.Fixture) {
clearlen := len(p.Breakpoints().M)
assertNoError(p.Continue(), t, "Continue 0")
assertLineNumber(p, t, 11, "Continue 0") // Position 0
scope, err := proc.GoroutineScope(p, p.CurrentThread())
assertNoError(err, t, "GoroutineScope")
_, err = p.SetWatchpoint(scope, "w", proc.WatchWrite, nil)
assertNoError(err, t, "SetDataBreakpoint(write-only)")
if len(p.Breakpoints().M) != clearlen+3 {
// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel
t.Errorf("wrong number of breakpoints after setting watchpoint: %d", len(p.Breakpoints().M)-clearlen)
}
var retaddr uint64
for _, bp := range p.Breakpoints().M {
for _, breaklet := range bp.Breaklets {
if breaklet.Kind&proc.WatchOutOfScopeBreakpoint != 0 {
retaddr = bp.Addr
break
}
}
}
_, err = p.SetBreakpoint(retaddr, proc.UserBreakpoint, nil)
assertNoError(err, t, "SetBreakpoint")
if len(p.Breakpoints().M) != clearlen+3 {
// want 1 watchpoint, 1 stack resize breakpoint, 1 out of scope sentinel (which is also a user breakpoint)
t.Errorf("wrong number of breakpoints after setting watchpoint: %d", len(p.Breakpoints().M)-clearlen)
}
assertNoError(p.Continue(), t, "Continue 1")
assertLineNumber(p, t, 17, "Continue 1") // Position 1
assertNoError(p.Continue(), t, "Continue 2")
t.Logf("%#v", p.CurrentThread().Breakpoint().Breakpoint)
assertLineNumber(p, t, 24, "Continue 2") // Position 2 (watchpoint gone out of scope)
if len(p.Breakpoints().M) != clearlen+1 {
// want 1 user breakpoint set at retaddr
t.Errorf("wrong number of breakpoints after watchpoint goes out of scope: %d", len(p.Breakpoints().M)-clearlen)
}
if len(p.Breakpoints().WatchOutOfScope) != 1 {
t.Errorf("wrong number of out-of-scope watchpoints after watchpoint goes out of scope: %d", len(p.Breakpoints().WatchOutOfScope))
}
_, err = p.ClearBreakpoint(retaddr)
assertNoError(err, t, "ClearBreakpoint")
if len(p.Breakpoints().M) != clearlen {
// want 1 user breakpoint set at retaddr
t.Errorf("wrong number of breakpoints after removing user breakpoint: %d", len(p.Breakpoints().M)-clearlen)
}
})
}

170
pkg/proc/stackwatch.go Normal file

@ -0,0 +1,170 @@
package proc
import (
"errors"
"github.com/go-delve/delve/pkg/astutil"
"github.com/go-delve/delve/pkg/logflags"
)
// This file implements most of the details needed to support stack
// watchpoints. Some of the remaining details are in breakpoints, along with
// the code to support non-stack allocated watchpoints.
//
// In Go goroutine stacks start small and are frequently resized by the
// runtime according to the needs of the goroutine.
// To support this behavior we create a StackResizeBreakpoint, deep inside
// the Go runtime, when this breakpoint is hit all the stack watchpoints on
// the goroutine being resized are adjusted for the new stack.
// Furthermore, we need to detect when a goroutine leaves the stack frame
// where the variable we are watching was declared, so that we can notify
// the user that the variable went out of scope and clear the watchpoint.
//
// These breakpoints are created by setStackWatchBreakpoints and cleared by
// clearStackWatchBreakpoints.
// setStackWatchBreakpoints sets the out of scope sentinel breakpoints for
// watchpoint and a stack resize breakpoint.
func (t *Target) setStackWatchBreakpoints(scope *EvalScope, watchpoint *Breakpoint) error {
// Watchpoint Out-of-scope Sentinel
woos := func(_ Thread) bool {
watchpointOutOfScope(t, watchpoint)
return true
}
topframe, retframe, err := topframe(scope.g, nil)
if err != nil {
return err
}
sameGCond := sameGoroutineCondition(scope.g)
retFrameCond := astutil.And(sameGCond, frameoffCondition(&retframe))
var deferpc uint64
if topframe.TopmostDefer != nil {
_, _, deferfn := topframe.TopmostDefer.DeferredFunc(t)
if deferfn != nil {
var err error
deferpc, err = FirstPCAfterPrologue(t, deferfn, false)
if err != nil {
return err
}
}
}
if deferpc != 0 && deferpc != topframe.Current.PC {
deferbp, err := t.SetBreakpoint(deferpc, WatchOutOfScopeBreakpoint, sameGCond)
if err != nil {
return err
}
deferbreaklet := deferbp.Breaklets[len(deferbp.Breaklets)-1]
deferbreaklet.checkPanicCall = true
deferbreaklet.watchpoint = watchpoint
deferbreaklet.callback = woos
}
retbp, err := t.SetBreakpoint(retframe.Current.PC, WatchOutOfScopeBreakpoint, retFrameCond)
if err != nil {
return err
}
retbreaklet := retbp.Breaklets[len(retbp.Breaklets)-1]
retbreaklet.watchpoint = watchpoint
retbreaklet.callback = woos
// Stack Resize Sentinel
fn := t.BinInfo().LookupFunc["runtime.copystack"]
if fn == nil {
return errors.New("could not find runtime.copystack")
}
text, err := Disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), fn.Entry, fn.End)
if err != nil {
return err
}
var retpc uint64
for _, instr := range text {
if instr.IsRet() {
if retpc != 0 {
return errors.New("runtime.copystack has too many return instructions")
}
retpc = instr.Loc.PC
}
}
if retpc == 0 {
return errors.New("could not find return instruction in runtime.copystack")
}
rszbp, err := t.SetBreakpoint(retpc, StackResizeBreakpoint, sameGCond)
if err != nil {
return err
}
rszbreaklet := rszbp.Breaklets[len(rszbp.Breaklets)-1]
rszbreaklet.watchpoint = watchpoint
rszbreaklet.callback = func(th Thread) bool {
adjustStackWatchpoint(t, th, watchpoint)
return false // we never want this breakpoint to be shown to the user
}
return nil
}
// clearStackWatchBreakpoints clears all accessory breakpoints for
// watchpoint.
func (t *Target) clearStackWatchBreakpoints(watchpoint *Breakpoint) error {
bpmap := t.Breakpoints()
for _, bp := range bpmap.M {
changed := false
for i, breaklet := range bp.Breaklets {
if breaklet.watchpoint == watchpoint {
bp.Breaklets[i] = nil
changed = true
}
}
if changed {
_, err := t.finishClearBreakpoint(bp)
if err != nil {
return err
}
}
}
return nil
}
// watchpointOutOfScope is called when the watchpoint goes out of scope. It
// is used as a breaklet callback function.
// Its responsibility is to delete the watchpoint and make sure that the
// user is notified of the watchpoint going out of scope.
func watchpointOutOfScope(t *Target, watchpoint *Breakpoint) {
t.Breakpoints().WatchOutOfScope = append(t.Breakpoints().WatchOutOfScope, watchpoint)
_, err := t.ClearBreakpoint(watchpoint.Addr)
if err != nil {
log := logflags.DebuggerLogger()
log.Errorf("could not clear out-of-scope watchpoint: %v", err)
}
}
// adjustStackWatchpoint is called when the goroutine of watchpoint resizes
// its stack. It is used as a breaklet callback function.
// Its responsibility is to move the watchpoint to a its new address.
func adjustStackWatchpoint(t *Target, th Thread, watchpoint *Breakpoint) {
g, _ := GetG(th)
if g == nil {
return
}
err := t.proc.EraseBreakpoint(watchpoint)
if err != nil {
log := logflags.DebuggerLogger()
log.Errorf("could not adjust watchpoint at %#x: %v", watchpoint.Addr, err)
return
}
delete(t.Breakpoints().M, watchpoint.Addr)
watchpoint.Addr = uint64(int64(g.stack.hi) + watchpoint.watchStackOff)
err = t.proc.WriteBreakpoint(watchpoint)
if err != nil {
log := logflags.DebuggerLogger()
log.Errorf("could not adjust watchpoint at %#x: %v", watchpoint.Addr, err)
return
}
t.Breakpoints().M[watchpoint.Addr] = watchpoint
}

@ -54,6 +54,7 @@ func (dbp *Target) Continue() error {
thread.Common().CallReturn = false thread.Common().CallReturn = false
thread.Common().returnValues = nil thread.Common().returnValues = nil
} }
dbp.Breakpoints().WatchOutOfScope = nil
dbp.CheckAndClearManualStopRequest() dbp.CheckAndClearManualStopRequest()
defer func() { defer func() {
// Make sure we clear internal breakpoints if we simultaneously receive a // Make sure we clear internal breakpoints if we simultaneously receive a
@ -916,10 +917,12 @@ func setDeferBreakpoint(p *Target, text []AsmInstruction, topframe Stackframe, s
var deferpc uint64 var deferpc uint64
if topframe.TopmostDefer != nil && topframe.TopmostDefer.DwrapPC != 0 { if topframe.TopmostDefer != nil && topframe.TopmostDefer.DwrapPC != 0 {
_, _, deferfn := topframe.TopmostDefer.DeferredFunc(p) _, _, deferfn := topframe.TopmostDefer.DeferredFunc(p)
var err error if deferfn != nil {
deferpc, err = FirstPCAfterPrologue(p, deferfn, false) var err error
if err != nil { deferpc, err = FirstPCAfterPrologue(p, deferfn, false)
return 0, err if err != nil {
return 0, err
}
} }
} }
if deferpc != 0 && deferpc != topframe.Current.PC { if deferpc != 0 && deferpc != topframe.Current.PC {

@ -286,7 +286,11 @@ Groups goroutines by the value of the label with the specified key.
Called without arguments it will show information about the current goroutine. 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 a single argument it will switch to the specified goroutine.
Called with more arguments it will execute a command on the specified goroutine.`}, Called with more arguments it will execute a command on the specified goroutine.`},
{aliases: []string{"breakpoints", "bp"}, group: breakCmds, cmdFn: breakpoints, helpMsg: "Print out info for active breakpoints."}, {aliases: []string{"breakpoints", "bp"}, group: breakCmds, cmdFn: breakpoints, helpMsg: `Print out info for active breakpoints.
breakpoints [-a]
Specifying -a prints all physical breakpoint, including internal breakpoints.`},
{aliases: []string{"print", "p"}, group: dataCmds, allowedPrefixes: onPrefix | deferredPrefix, cmdFn: printVar, helpMsg: `Evaluate an expression. {aliases: []string{"print", "p"}, group: dataCmds, allowedPrefixes: onPrefix | deferredPrefix, cmdFn: printVar, helpMsg: `Evaluate an expression.
[goroutine <n>] [frame <m>] print [%format] <expression> [goroutine <n>] [frame <m>] print [%format] <expression>
@ -1674,7 +1678,7 @@ func clear(t *Term, ctx callContext, args string) error {
} }
func clearAll(t *Term, ctx callContext, args string) error { func clearAll(t *Term, ctx callContext, args string) error {
breakPoints, err := t.client.ListBreakpoints() breakPoints, err := t.client.ListBreakpoints(false)
if err != nil { if err != nil {
return err return err
} }
@ -1740,13 +1744,17 @@ 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 (a byID) Less(i, j int) bool { return a[i].ID < a[j].ID }
func breakpoints(t *Term, ctx callContext, args string) error { func breakpoints(t *Term, ctx callContext, args string) error {
breakPoints, err := t.client.ListBreakpoints() breakPoints, err := t.client.ListBreakpoints(args == "-a")
if err != nil { if err != nil {
return err return err
} }
sort.Sort(byID(breakPoints)) sort.Sort(byID(breakPoints))
for _, bp := range breakPoints { for _, bp := range breakPoints {
fmt.Printf("%s at %v (%d)\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp), bp.TotalHitCount) enabled := "(enabled)"
if bp.Disabled {
enabled = "(disabled)"
}
fmt.Printf("%s %s at %v (%d)\n", formatBreakpointName(bp, true), enabled, t.formatBreakpointLocation(bp), bp.TotalHitCount)
attrs := formatBreakpointAttrs("\t", bp, false) attrs := formatBreakpointAttrs("\t", bp, false)
@ -1791,6 +1799,9 @@ func formatBreakpointAttrs(prefix string, bp *api.Breakpoint, includeTrace bool)
if includeTrace && bp.Tracepoint { if includeTrace && bp.Tracepoint {
attrs = append(attrs, fmt.Sprintf("%strace", prefix)) attrs = append(attrs, fmt.Sprintf("%strace", prefix))
} }
for i := range bp.VerboseDescr {
attrs = append(attrs, fmt.Sprintf("%s%s", prefix, bp.VerboseDescr[i]))
}
return attrs return attrs
} }
@ -2602,6 +2613,10 @@ func printcontext(t *Term, state *api.DebuggerState) {
if state.When != "" { if state.When != "" {
fmt.Println(state.When) fmt.Println(state.When)
} }
for _, watchpoint := range state.WatchOutOfScope {
fmt.Printf("%s went out of scope and was cleared\n", formatBreakpointName(watchpoint, true))
}
} }
func printcontextLocation(t *Term, loc api.Location) { func printcontextLocation(t *Term, loc api.Location) {
@ -3113,11 +3128,7 @@ func formatBreakpointName(bp *api.Breakpoint, upcase bool) string {
if bp.WatchExpr != "" && bp.WatchExpr != bp.Name { if bp.WatchExpr != "" && bp.WatchExpr != bp.Name {
return fmt.Sprintf("%s %s on [%s]", thing, id, bp.WatchExpr) return fmt.Sprintf("%s %s on [%s]", thing, id, bp.WatchExpr)
} }
state := "(enabled)" return fmt.Sprintf("%s %s", thing, id)
if bp.Disabled {
state = "(disabled)"
}
return fmt.Sprintf("%s %s %s", thing, id, state)
} }
func (t *Term) formatBreakpointLocation(bp *api.Breakpoint) string { func (t *Term) formatBreakpointLocation(bp *api.Breakpoint) string {

@ -834,6 +834,24 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
} }
var rpcArgs rpc2.ListBreakpointsIn var rpcArgs rpc2.ListBreakpointsIn
var rpcRet rpc2.ListBreakpointsOut var rpcRet rpc2.ListBreakpointsOut
if len(args) > 0 && args[0] != starlark.None {
err := unmarshalStarlarkValue(args[0], &rpcArgs.All, "All")
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
for _, kv := range kwargs {
var err error
switch kv[0].(starlark.String) {
case "All":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.All, "All")
default:
err = fmt.Errorf("unknown argument %q", kv[0])
}
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
err := env.ctx.Client().CallAPI("ListBreakpoints", &rpcArgs, &rpcRet) err := env.ctx.Client().CallAPI("ListBreakpoints", &rpcArgs, &rpcRet)
if err != nil { if err != nil {
return starlark.None, err return starlark.None, err

@ -39,6 +39,9 @@ type DebuggerState struct {
// While NextInProgress is set further requests for next or step may be rejected. // While NextInProgress is set further requests for next or step may be rejected.
// Either execute continue until NextInProgress is false or call CancelNext // Either execute continue until NextInProgress is false or call CancelNext
NextInProgress bool NextInProgress bool
// WatchOutOfScope contains the list of watchpoints that went out of scope
// during the last continue.
WatchOutOfScope []*Breakpoint
// Exited indicates whether the debugged process has exited. // Exited indicates whether the debugged process has exited.
Exited bool `json:"exited"` Exited bool `json:"exited"`
ExitStatus int `json:"exitStatus"` ExitStatus int `json:"exitStatus"`
@ -108,6 +111,8 @@ type Breakpoint struct {
WatchExpr string WatchExpr string
WatchType WatchType WatchType WatchType
VerboseDescr []string `json:"VerboseDescr,omitempty"`
// number of times a breakpoint has been reached in a certain goroutine // number of times a breakpoint has been reached in a certain goroutine
HitCount map[string]uint64 `json:"hitCount"` HitCount map[string]uint64 `json:"hitCount"`
// number of times a breakpoint has been reached // number of times a breakpoint has been reached

@ -69,7 +69,7 @@ type Client interface {
// CreateWatchpoint creates a new watchpoint. // CreateWatchpoint creates a new watchpoint.
CreateWatchpoint(api.EvalScope, string, api.WatchType) (*api.Breakpoint, error) CreateWatchpoint(api.EvalScope, string, api.WatchType) (*api.Breakpoint, error)
// ListBreakpoints gets all breakpoints. // ListBreakpoints gets all breakpoints.
ListBreakpoints() ([]*api.Breakpoint, error) ListBreakpoints(bool) ([]*api.Breakpoint, error)
// ClearBreakpoint deletes a breakpoint by ID. // ClearBreakpoint deletes a breakpoint by ID.
ClearBreakpoint(id int) (*api.Breakpoint, error) ClearBreakpoint(id int) (*api.Breakpoint, error)
// ClearBreakpointByName deletes a breakpoint by name // ClearBreakpointByName deletes a breakpoint by name

@ -1367,7 +1367,7 @@ func (s *Server) clearBreakpoints(existingBps map[string]*api.Breakpoint, bpAdde
} }
func (s *Server) getMatchingBreakpoints(prefix string) map[string]*api.Breakpoint { func (s *Server) getMatchingBreakpoints(prefix string) map[string]*api.Breakpoint {
existing := s.debugger.Breakpoints() existing := s.debugger.Breakpoints(false)
matchingBps := make(map[string]*api.Breakpoint, len(existing)) matchingBps := make(map[string]*api.Breakpoint, len(existing))
for _, bp := range existing { for _, bp := range existing {
// Skip special breakpoints such as for panic. // Skip special breakpoints such as for panic.

@ -623,6 +623,11 @@ func (d *Debugger) state(retLoadCfg *proc.LoadConfig) (*api.DebuggerState, error
state.When, _ = d.target.When() state.When, _ = d.target.When()
} }
state.WatchOutOfScope = make([]*api.Breakpoint, 0, len(d.target.Breakpoints().WatchOutOfScope))
for _, bp := range d.target.Breakpoints().WatchOutOfScope {
state.WatchOutOfScope = append(state.WatchOutOfScope, api.ConvertBreakpoint(bp))
}
return state, nil return state, nil
} }
@ -946,11 +951,21 @@ func (d *Debugger) clearBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoint
} }
// Breakpoints returns the list of current breakpoints. // Breakpoints returns the list of current breakpoints.
func (d *Debugger) Breakpoints() []*api.Breakpoint { func (d *Debugger) Breakpoints(all bool) []*api.Breakpoint {
d.targetMutex.Lock() d.targetMutex.Lock()
defer d.targetMutex.Unlock() defer d.targetMutex.Unlock()
bps := api.ConvertBreakpoints(d.breakpoints()) var bps []*api.Breakpoint
if !all {
bps = api.ConvertBreakpoints(d.breakpoints())
} else {
for _, bp := range d.target.Breakpoints().M {
abp := api.ConvertBreakpoint(bp)
abp.VerboseDescr = bp.VerboseDescr()
bps = append(bps, abp)
}
}
for _, bp := range d.disabledBreakpoints { for _, bp := range d.disabledBreakpoints {
bps = append(bps, bp) bps = append(bps, bp)

@ -98,7 +98,7 @@ func (s *RPCServer) StacktraceGoroutine(args *StacktraceGoroutineArgs, locations
} }
func (s *RPCServer) ListBreakpoints(arg interface{}, breakpoints *[]*api.Breakpoint) error { func (s *RPCServer) ListBreakpoints(arg interface{}, breakpoints *[]*api.Breakpoint) error {
*breakpoints = s.debugger.Breakpoints() *breakpoints = s.debugger.Breakpoints(false)
return nil return nil
} }

@ -253,9 +253,9 @@ func (c *RPCClient) CreateWatchpoint(scope api.EvalScope, expr string, wtype api
return out.Breakpoint, err return out.Breakpoint, err
} }
func (c *RPCClient) ListBreakpoints() ([]*api.Breakpoint, error) { func (c *RPCClient) ListBreakpoints(all bool) ([]*api.Breakpoint, error) {
var out ListBreakpointsOut var out ListBreakpointsOut
err := c.call("ListBreakpoints", ListBreakpointsIn{}, &out) err := c.call("ListBreakpoints", ListBreakpointsIn{all}, &out)
return out.Breakpoints, err return out.Breakpoints, err
} }

@ -227,6 +227,7 @@ func (s *RPCServer) Ancestors(arg AncestorsIn, out *AncestorsOut) error {
} }
type ListBreakpointsIn struct { type ListBreakpointsIn struct {
All bool
} }
type ListBreakpointsOut struct { type ListBreakpointsOut struct {
@ -235,7 +236,7 @@ type ListBreakpointsOut struct {
// ListBreakpoints gets all breakpoints. // ListBreakpoints gets all breakpoints.
func (s *RPCServer) ListBreakpoints(arg ListBreakpointsIn, out *ListBreakpointsOut) error { func (s *RPCServer) ListBreakpoints(arg ListBreakpointsIn, out *ListBreakpointsOut) error {
out.Breakpoints = s.debugger.Breakpoints() out.Breakpoints = s.debugger.Breakpoints(arg.All)
return nil return nil
} }

@ -9,6 +9,8 @@ import (
"testing" "testing"
"github.com/go-delve/delve/service/api" "github.com/go-delve/delve/service/api"
"github.com/go-delve/delve/service/rpc1"
"github.com/go-delve/delve/service/rpc2"
) )
func assertNoError(err error, t *testing.T, s string) { func assertNoError(err error, t *testing.T, s string) {
@ -63,8 +65,15 @@ type BreakpointLister interface {
ListBreakpoints() ([]*api.Breakpoint, error) ListBreakpoints() ([]*api.Breakpoint, error)
} }
func countBreakpoints(t *testing.T, c BreakpointLister) int { func countBreakpoints(t *testing.T, c interface{}) int {
bps, err := c.ListBreakpoints() var bps []*api.Breakpoint
var err error
switch c := c.(type) {
case *rpc2.RPCClient:
bps, err = c.ListBreakpoints(false)
case *rpc1.RPCClient:
bps, err = c.ListBreakpoints()
}
assertNoError(err, t, "ListBreakpoints()") assertNoError(err, t, "ListBreakpoints()")
bpcount := 0 bpcount := 0
for _, bp := range bps { for _, bp := range bps {

@ -196,7 +196,7 @@ func TestRestart_duringStop(t *testing.T) {
if c.ProcessPid() == origPid { if c.ProcessPid() == origPid {
t.Fatal("did not spawn new process, has same PID") t.Fatal("did not spawn new process, has same PID")
} }
bps, err := c.ListBreakpoints() bps, err := c.ListBreakpoints(false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -1631,7 +1631,7 @@ func TestClientServer_RestartBreakpointPosition(t *testing.T) {
assertNoError(err, t, "Halt") assertNoError(err, t, "Halt")
_, err = c.Restart(false) _, err = c.Restart(false)
assertNoError(err, t, "Restart") assertNoError(err, t, "Restart")
bps, err := c.ListBreakpoints() bps, err := c.ListBreakpoints(false)
assertNoError(err, t, "ListBreakpoints") assertNoError(err, t, "ListBreakpoints")
for _, bp := range bps { for _, bp := range bps {
if bp.Name == bpBefore.Name { if bp.Name == bpBefore.Name {
@ -2138,7 +2138,7 @@ func TestDoubleCreateBreakpoint(t *testing.T) {
_, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.main", Line: 1, Name: "firstbreakpoint", Tracepoint: true}) _, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.main", Line: 1, Name: "firstbreakpoint", Tracepoint: true})
assertNoError(err, t, "CreateBreakpoint 1") assertNoError(err, t, "CreateBreakpoint 1")
bps, err := c.ListBreakpoints() bps, err := c.ListBreakpoints(false)
assertNoError(err, t, "ListBreakpoints 1") assertNoError(err, t, "ListBreakpoints 1")
t.Logf("breakpoints before second call:") t.Logf("breakpoints before second call:")
@ -2151,7 +2151,7 @@ func TestDoubleCreateBreakpoint(t *testing.T) {
_, err = c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.main", Line: 1, Name: "secondbreakpoint", Tracepoint: true}) _, err = c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.main", Line: 1, Name: "secondbreakpoint", Tracepoint: true})
assertError(err, t, "CreateBreakpoint 2") // breakpoint exists assertError(err, t, "CreateBreakpoint 2") // breakpoint exists
bps, err = c.ListBreakpoints() bps, err = c.ListBreakpoints(false)
assertNoError(err, t, "ListBreakpoints 2") assertNoError(err, t, "ListBreakpoints 2")
t.Logf("breakpoints after second call:") t.Logf("breakpoints after second call:")
@ -2201,7 +2201,7 @@ func TestClearLogicalBreakpoint(t *testing.T) {
} }
_, err = c.ClearBreakpoint(bp.ID) _, err = c.ClearBreakpoint(bp.ID)
assertNoError(err, t, "ClearBreakpoint()") assertNoError(err, t, "ClearBreakpoint()")
bps, err := c.ListBreakpoints() bps, err := c.ListBreakpoints(false)
assertNoError(err, t, "ListBreakpoints()") assertNoError(err, t, "ListBreakpoints()")
for _, curbp := range bps { for _, curbp := range bps {
if curbp.ID == bp.ID { if curbp.ID == bp.ID {
@ -2322,7 +2322,7 @@ func TestDetachLeaveRunning(t *testing.T) {
func assertNoDuplicateBreakpoints(t *testing.T, c service.Client) { func assertNoDuplicateBreakpoints(t *testing.T, c service.Client) {
t.Helper() t.Helper()
bps, _ := c.ListBreakpoints() bps, _ := c.ListBreakpoints(false)
seen := make(map[int]bool) seen := make(map[int]bool)
for _, bp := range bps { for _, bp := range bps {
t.Logf("%#v\n", bp) t.Logf("%#v\n", bp)