proc: implement conditional breakpoints
Backend only, no UI Implements #120 (partial)
This commit is contained in:
parent
092571591f
commit
d9a31dd598
@ -1,6 +1,12 @@
|
||||
package proc
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Breakpoint represents a breakpoint. Stores information on the break
|
||||
// point including the byte of data that originally was stored at that
|
||||
@ -24,7 +30,7 @@ type Breakpoint struct {
|
||||
HitCount map[int]uint64 // Number of times a breakpoint has been reached in a certain goroutine
|
||||
TotalHitCount uint64 // Number of times a breakpoint has been reached
|
||||
|
||||
Cond int // When Cond is greater than zero this breakpoint will trigger only when the current goroutine id is equal to it
|
||||
Cond ast.Expr // When Cond is not nil the breakpoint will be triggered only if evaluating Cond returns true
|
||||
}
|
||||
|
||||
func (bp *Breakpoint) String() string {
|
||||
@ -78,7 +84,7 @@ func (dbp *Process) setBreakpoint(tid int, addr uint64, temp bool) (*Breakpoint,
|
||||
Line: l,
|
||||
Addr: addr,
|
||||
Temp: temp,
|
||||
Cond: -1,
|
||||
Cond: nil,
|
||||
HitCount: map[int]uint64{},
|
||||
}
|
||||
|
||||
@ -109,15 +115,25 @@ func (dbp *Process) writeSoftwareBreakpoint(thread *Thread, addr uint64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (bp *Breakpoint) checkCondition(thread *Thread) bool {
|
||||
if bp.Cond < 0 {
|
||||
return true
|
||||
func (bp *Breakpoint) checkCondition(thread *Thread) (bool, error) {
|
||||
if bp.Cond == nil {
|
||||
return true, nil
|
||||
}
|
||||
g, err := thread.GetG()
|
||||
scope, err := thread.Scope()
|
||||
if err != nil {
|
||||
return false
|
||||
return true, err
|
||||
}
|
||||
return g.ID == bp.Cond
|
||||
v, err := scope.evalAST(bp.Cond)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("error evaluating expression: %v", err)
|
||||
}
|
||||
if v.Unreadable != nil {
|
||||
return true, fmt.Errorf("condition expression unreadable: %v", v.Unreadable)
|
||||
}
|
||||
if v.Kind != reflect.Bool {
|
||||
return true, errors.New("condition expression not boolean")
|
||||
}
|
||||
return constant.BoolVal(v.Value), nil
|
||||
}
|
||||
|
||||
// NoBreakpointError is returned when trying to
|
||||
|
54
proc/proc.go
54
proc/proc.go
@ -6,10 +6,13 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/token"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@ -297,7 +300,17 @@ func (dbp *Process) Next() (err error) {
|
||||
if !goroutineExiting {
|
||||
for i := range dbp.Breakpoints {
|
||||
if dbp.Breakpoints[i].Temp {
|
||||
dbp.Breakpoints[i].Cond = g.ID
|
||||
dbp.Breakpoints[i].Cond = &ast.BinaryExpr{
|
||||
Op: token.EQL,
|
||||
X: &ast.SelectorExpr{
|
||||
X: &ast.SelectorExpr{
|
||||
X: &ast.Ident{Name: "runtime"},
|
||||
Sel: &ast.Ident{Name: "curg"},
|
||||
},
|
||||
Sel: &ast.Ident{Name: "goid"},
|
||||
},
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: strconv.Itoa(g.ID)},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -374,20 +387,45 @@ func (dbp *Process) Continue() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return dbp.conditionErrors()
|
||||
case dbp.CurrentThread.onTriggeredTempBreakpoint():
|
||||
return dbp.clearTempBreakpoints()
|
||||
case dbp.CurrentThread.onTriggeredBreakpoint():
|
||||
if dbp.CurrentThread.onNextGoroutine() {
|
||||
return dbp.clearTempBreakpoints()
|
||||
err := dbp.clearTempBreakpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return dbp.conditionErrors()
|
||||
case dbp.CurrentThread.onTriggeredBreakpoint():
|
||||
onNextGoroutine, err := dbp.CurrentThread.onNextGoroutine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if onNextGoroutine {
|
||||
err := dbp.clearTempBreakpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return dbp.conditionErrors()
|
||||
default:
|
||||
// not a manual stop, not on runtime.Breakpoint, not on a breakpoint, just repeat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dbp *Process) conditionErrors() error {
|
||||
var condErr error
|
||||
for _, th := range dbp.Threads {
|
||||
if th.CurrentBreakpoint != nil && th.BreakpointConditionError != nil {
|
||||
if condErr == nil {
|
||||
condErr = th.BreakpointConditionError
|
||||
} else {
|
||||
return fmt.Errorf("multiple errors evaluating conditions")
|
||||
}
|
||||
}
|
||||
}
|
||||
return condErr
|
||||
}
|
||||
|
||||
// pick a new dbp.CurrentThread, with the following priority:
|
||||
// - a thread with onTriggeredTempBreakpoint() == true
|
||||
// - a thread with onTriggeredBreakpoint() == true (prioritizing trapthread)
|
||||
@ -665,6 +703,8 @@ func (dbp *Process) run(fn func() error) error {
|
||||
}
|
||||
for _, th := range dbp.Threads {
|
||||
th.CurrentBreakpoint = nil
|
||||
th.BreakpointConditionMet = false
|
||||
th.BreakpointConditionError = nil
|
||||
}
|
||||
if err := fn(); err != nil {
|
||||
return err
|
||||
|
@ -3,7 +3,9 @@ package proc
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/token"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -1356,3 +1358,71 @@ func BenchmarkLocalVariables(b *testing.B) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCondBreakpoint(t *testing.T) {
|
||||
withTestProcess("parallel_next", t, func(p *Process, fixture protest.Fixture) {
|
||||
addr, _, err := p.goSymTable.LineToPC(fixture.Source, 9)
|
||||
assertNoError(err, t, "LineToPC")
|
||||
bp, err := p.SetBreakpoint(addr)
|
||||
assertNoError(err, t, "SetBreakpoint()")
|
||||
bp.Cond = &ast.BinaryExpr{
|
||||
Op: token.EQL,
|
||||
X: &ast.Ident{Name: "n"},
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "7"},
|
||||
}
|
||||
|
||||
assertNoError(p.Continue(), t, "Continue()")
|
||||
|
||||
nvar, err := evalVariable(p, "n")
|
||||
assertNoError(err, t, "EvalVariable()")
|
||||
|
||||
n, _ := constant.Int64Val(nvar.Value)
|
||||
if n != 7 {
|
||||
t.Fatalf("Stoppend on wrong goroutine %d\n", n)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCondBreakpointError(t *testing.T) {
|
||||
withTestProcess("parallel_next", t, func(p *Process, fixture protest.Fixture) {
|
||||
addr, _, err := p.goSymTable.LineToPC(fixture.Source, 9)
|
||||
assertNoError(err, t, "LineToPC")
|
||||
bp, err := p.SetBreakpoint(addr)
|
||||
assertNoError(err, t, "SetBreakpoint()")
|
||||
bp.Cond = &ast.BinaryExpr{
|
||||
Op: token.EQL,
|
||||
X: &ast.Ident{Name: "nonexistentvariable"},
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "7"},
|
||||
}
|
||||
|
||||
err = p.Continue()
|
||||
if err == nil {
|
||||
t.Fatalf("No error on first Continue()")
|
||||
}
|
||||
|
||||
if err.Error() != "error evaluating expression: could not find symbol value for nonexistentvariable" && err.Error() != "multiple errors evaluating conditions" {
|
||||
t.Fatalf("Unexpected error on first Continue(): %v", err)
|
||||
}
|
||||
|
||||
bp.Cond = &ast.BinaryExpr{
|
||||
Op: token.EQL,
|
||||
X: &ast.Ident{Name: "n"},
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "7"},
|
||||
}
|
||||
|
||||
err = p.Continue()
|
||||
if err != nil {
|
||||
if _, exited := err.(ProcessExitedError); !exited {
|
||||
t.Fatalf("Unexpected error on second Continue(): %v", err)
|
||||
}
|
||||
} else {
|
||||
nvar, err := evalVariable(p, "n")
|
||||
assertNoError(err, t, "EvalVariable()")
|
||||
|
||||
n, _ := constant.Int64Val(nvar.Value)
|
||||
if n != 7 {
|
||||
t.Fatalf("Stoppend on wrong goroutine %d\n", n)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -17,10 +17,12 @@ import (
|
||||
// a whole, and Status represents the last result of a `wait` call
|
||||
// on this thread.
|
||||
type Thread struct {
|
||||
ID int // Thread ID or mach port
|
||||
Status *WaitStatus // Status returned from last wait call
|
||||
CurrentBreakpoint *Breakpoint // Breakpoint thread is currently stopped at
|
||||
BreakpointConditionMet bool // Output of evaluating the breakpoint's condition
|
||||
ID int // Thread ID or mach port
|
||||
Status *WaitStatus // Status returned from last wait call
|
||||
CurrentBreakpoint *Breakpoint // Breakpoint thread is currently stopped at
|
||||
BreakpointConditionMet bool // Output of evaluating the breakpoint's condition
|
||||
BreakpointConditionError error // Error evaluating the breakpoint's condition
|
||||
|
||||
dbp *Process
|
||||
singleStepping bool
|
||||
running bool
|
||||
@ -362,7 +364,7 @@ func (thread *Thread) SetCurrentBreakpoint() error {
|
||||
if err = thread.SetPC(bp.Addr); err != nil {
|
||||
return err
|
||||
}
|
||||
thread.BreakpointConditionMet = bp.checkCondition(thread)
|
||||
thread.BreakpointConditionMet, thread.BreakpointConditionError = bp.checkCondition(thread)
|
||||
if thread.onTriggeredBreakpoint() {
|
||||
if g, err := thread.GetG(); err == nil {
|
||||
thread.CurrentBreakpoint.HitCount[g.ID]++
|
||||
@ -390,7 +392,7 @@ func (thread *Thread) onRuntimeBreakpoint() bool {
|
||||
}
|
||||
|
||||
// Returns true if this thread is on the goroutine requested by the current 'next' command
|
||||
func (th *Thread) onNextGoroutine() bool {
|
||||
func (th *Thread) onNextGoroutine() (bool, error) {
|
||||
var bp *Breakpoint
|
||||
for i := range th.dbp.Breakpoints {
|
||||
if th.dbp.Breakpoints[i].Temp {
|
||||
@ -398,7 +400,7 @@ func (th *Thread) onNextGoroutine() bool {
|
||||
}
|
||||
}
|
||||
if bp == nil {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
return bp.checkCondition(th)
|
||||
}
|
||||
|
@ -172,7 +172,7 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint) (*api.Breakpoin
|
||||
bp.Goroutine = requestedBp.Goroutine
|
||||
bp.Stacktrace = requestedBp.Stacktrace
|
||||
bp.Variables = requestedBp.Variables
|
||||
bp.Cond = -1
|
||||
bp.Cond = nil
|
||||
createdBp = api.ConvertBreakpoint(bp)
|
||||
log.Printf("created breakpoint: %#v", createdBp)
|
||||
return createdBp, nil
|
||||
|
Loading…
Reference in New Issue
Block a user