diff --git a/proc/breakpoints.go b/proc/breakpoints.go index 70f49b9a..73a1af7a 100644 --- a/proc/breakpoints.go +++ b/proc/breakpoints.go @@ -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 diff --git a/proc/proc.go b/proc/proc.go index 5f991445..efb7adf7 100644 --- a/proc/proc.go +++ b/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 diff --git a/proc/proc_test.go b/proc/proc_test.go index 1ea4e22d..011f02a2 100644 --- a/proc/proc_test.go +++ b/proc/proc_test.go @@ -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) + } + } + }) +} diff --git a/proc/threads.go b/proc/threads.go index 17bffe73..1a55e4d5 100644 --- a/proc/threads.go +++ b/proc/threads.go @@ -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) } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index e804150a..8b896fea 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -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