terminal,service: auto-continue during next and step (#448)

* proc: bugfix: StepInto can not function when temp bps exist

* terminal,service: auto-continue during next and step

Make dlv call continue automatically when a breakpoint is hit on a
different goroutine during a next or step operation.
Added API hooks to implement the other solution to this problem (cancel
the next/step operation if a different breakpoint is hit).

Fixes #387
This commit is contained in:
Alessandro Arzilli 2016-04-25 01:20:02 +02:00 committed by Derek Parker
parent a7a0cc75e1
commit c4e01da5ca
10 changed files with 146 additions and 8 deletions

21
_fixtures/issue387.go Normal file

@ -0,0 +1,21 @@
package main
import (
"fmt"
"sync"
)
func dostuff(id int, wg *sync.WaitGroup) {
fmt.Println("goroutine:", id)
fmt.Println("goroutine:", id)
wg.Done()
}
func main() {
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go dostuff(i, &wg)
}
wg.Wait()
}

@ -285,7 +285,7 @@ func (dbp *Process) Next() (err error) {
case GoroutineExitingError: case GoroutineExitingError:
goroutineExiting = t.goid == g.ID goroutineExiting = t.goid == g.ID
default: default:
dbp.clearTempBreakpoints() dbp.ClearTempBreakpoints()
return return
} }
} }
@ -385,7 +385,7 @@ func (dbp *Process) Continue() error {
} }
return dbp.conditionErrors() return dbp.conditionErrors()
case dbp.CurrentThread.onTriggeredTempBreakpoint(): case dbp.CurrentThread.onTriggeredTempBreakpoint():
err := dbp.clearTempBreakpoints() err := dbp.ClearTempBreakpoints()
if err != nil { if err != nil {
return err return err
} }
@ -396,7 +396,7 @@ func (dbp *Process) Continue() error {
return err return err
} }
if onNextGoroutine { if onNextGoroutine {
err := dbp.clearTempBreakpoints() err := dbp.ClearTempBreakpoints()
if err != nil { if err != nil {
return err return err
} }
@ -484,6 +484,11 @@ func (dbp *Process) Step() (err error) {
// StepInto sets a temp breakpoint after the prologue of fn and calls Continue // StepInto sets a temp breakpoint after the prologue of fn and calls Continue
func (dbp *Process) StepInto(fn *gosym.Func) error { func (dbp *Process) StepInto(fn *gosym.Func) error {
for i := range dbp.Breakpoints {
if dbp.Breakpoints[i].Temp {
return fmt.Errorf("next while nexting")
}
}
pc, _ := dbp.FirstPCAfterPrologue(fn, false) pc, _ := dbp.FirstPCAfterPrologue(fn, false)
if _, err := dbp.SetTempBreakpoint(pc); err != nil { if _, err := dbp.SetTempBreakpoint(pc); err != nil {
return err return err
@ -754,7 +759,7 @@ func initializeDebugProcess(dbp *Process, path string, attach bool) (*Process, e
return dbp, nil return dbp, nil
} }
func (dbp *Process) clearTempBreakpoints() error { func (dbp *Process) ClearTempBreakpoints() error {
for _, bp := range dbp.Breakpoints { for _, bp := range dbp.Breakpoints {
if !bp.Temp { if !bp.Temp {
continue continue

@ -18,6 +18,11 @@ type DebuggerState struct {
SelectedGoroutine *Goroutine `json:"currentGoroutine,omitempty"` SelectedGoroutine *Goroutine `json:"currentGoroutine,omitempty"`
// List of all the process threads // List of all the process threads
Threads []*Thread Threads []*Thread
// NextInProgress indicates that a next or step operation was interrupted by another breakpoint
// or a manual stop and is waiting to complete.
// While NextInProgress is set further requests for next or step may be rejected.
// Either execute continue until NextInProgress is false or call CancelNext
NextInProgress bool
// 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"`

@ -49,6 +49,8 @@ type Client interface {
// Allows user to update an existing breakpoint for example to change the information // 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 // retrieved when the breakpoint is hit or to change, add or remove the break condition
AmendBreakpoint(*api.Breakpoint) error AmendBreakpoint(*api.Breakpoint) error
// Cancels a Next or Step call that was interrupted by a manual stop or by another breakpoint
CancelNext() error
// ListThreads lists all threads. // ListThreads lists all threads.
ListThreads() ([]*api.Thread, error) ListThreads() ([]*api.Thread, error)

@ -163,6 +163,13 @@ func (d *Debugger) state() (*api.DebuggerState, error) {
} }
} }
for _, bp := range d.process.Breakpoints {
if bp.Temp {
state.NextInProgress = true
break
}
}
return state, nil return state, nil
} }
@ -243,6 +250,10 @@ func (d *Debugger) AmendBreakpoint(amend *api.Breakpoint) error {
return copyBreakpointInfo(original, amend) return copyBreakpointInfo(original, amend)
} }
func (d *Debugger) CancelNext() error {
return d.process.ClearTempBreakpoints()
}
func copyBreakpointInfo(bp *proc.Breakpoint, requested *api.Breakpoint) (err error) { func copyBreakpointInfo(bp *proc.Breakpoint, requested *api.Breakpoint) (err error) {
bp.Name = requested.Name bp.Name = requested.Name
bp.Tracepoint = requested.Tracepoint bp.Tracepoint = requested.Tracepoint

@ -5,6 +5,7 @@ import (
"log" "log"
"net/rpc" "net/rpc"
"net/rpc/jsonrpc" "net/rpc/jsonrpc"
"errors"
"github.com/derekparker/delve/service/api" "github.com/derekparker/delve/service/api"
"sync" "sync"
@ -19,6 +20,8 @@ type RPCClient struct {
haltReq bool haltReq bool
} }
var unsupportedApiError = errors.New("unsupported")
// NewClient creates a new RPCClient. // NewClient creates a new RPCClient.
func NewClient(addr string) *RPCClient { func NewClient(addr string) *RPCClient {
client, err := jsonrpc.Dial("tcp", addr) client, err := jsonrpc.Dial("tcp", addr)
@ -186,6 +189,10 @@ func (c *RPCClient) AmendBreakpoint(bp *api.Breakpoint) error {
return err return err
} }
func (c *RPCClient) CancelNext() error {
return unsupportedApiError
}
func (c *RPCClient) ListThreads() ([]*api.Thread, error) { func (c *RPCClient) ListThreads() ([]*api.Thread, error) {
var threads []*api.Thread var threads []*api.Thread
err := c.call("ListThreads", nil, &threads) err := c.call("ListThreads", nil, &threads)

@ -178,6 +178,11 @@ func (c *RPCClient) AmendBreakpoint(bp *api.Breakpoint) error {
return err return err
} }
func (c *RPCClient) CancelNext() error {
var out CancelNextOut
return c.call("CancelNext", CancelNextIn{}, &out)
}
func (c *RPCClient) ListThreads() ([]*api.Thread, error) { func (c *RPCClient) ListThreads() ([]*api.Thread, error) {
var out ListThreadsOut var out ListThreadsOut
err := c.call("ListThreads", ListThreadsIn{}, &out) err := c.call("ListThreads", ListThreadsIn{}, &out)

@ -315,6 +315,16 @@ func (s *RPCServer) AmendBreakpoint(arg AmendBreakpointIn, out *AmendBreakpointO
return s.debugger.AmendBreakpoint(&arg.Breakpoint) return s.debugger.AmendBreakpoint(&arg.Breakpoint)
} }
type CancelNextIn struct {
}
type CancelNextOut struct {
}
func (s *RPCServer) CancelNext(arg CancelNextIn, out *CancelNextOut) error {
return s.debugger.CancelNext()
}
type ListThreadsIn struct { type ListThreadsIn struct {
} }

@ -449,14 +449,35 @@ func cont(t *Term, ctx callContext, args string) error {
return nil return nil
} }
func continueUntilCompleteNext(t *Term, state *api.DebuggerState, op string) error {
if !state.NextInProgress {
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
return nil
}
for {
stateChan := t.client.Continue()
var state *api.DebuggerState
for state = range stateChan {
if state.Err != nil {
return state.Err
}
printcontext(t, state)
}
if !state.NextInProgress {
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true)
return nil
}
fmt.Printf("\tbreakpoint hit during %s, continuing...\n", op)
}
}
func step(t *Term, ctx callContext, args string) error { func step(t *Term, ctx callContext, args string) error {
state, err := t.client.Step() state, err := t.client.Step()
if err != nil { if err != nil {
return err return err
} }
printcontext(t, state) printcontext(t, state)
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true) return continueUntilCompleteNext(t, state, "step")
return nil
} }
func stepInstruction(t *Term, ctx callContext, args string) error { func stepInstruction(t *Term, ctx callContext, args string) error {
@ -475,8 +496,7 @@ func next(t *Term, ctx callContext, args string) error {
return err return err
} }
printcontext(t, state) printcontext(t, state)
printfile(t, state.CurrentThread.File, state.CurrentThread.Line, true) return continueUntilCompleteNext(t, state, "next")
return nil
} }
func clear(t *Term, ctx callContext, args string) error { func clear(t *Term, ctx callContext, args string) error {

@ -71,6 +71,7 @@ func (ft *FakeTerminal) AssertExecError(cmdstr, tgterr string) {
} }
func withTestTerminal(name string, t testing.TB, fn func(*FakeTerminal)) { func withTestTerminal(name string, t testing.TB, fn func(*FakeTerminal)) {
os.Setenv("TERM", "dumb")
listener, err := net.Listen("tcp", "localhost:0") listener, err := net.Listen("tcp", "localhost:0")
if err != nil { if err != nil {
t.Fatalf("couldn't start listener: %s\n", err) t.Fatalf("couldn't start listener: %s\n", err)
@ -398,3 +399,54 @@ func TestOnPrefixLocals(t *testing.T) {
} }
}) })
} }
func countOccourences(s string, needle string) int {
count := 0
for {
idx := strings.Index(s, needle)
if idx < 0 {
break
}
count++
s = s[idx+len(needle):]
}
return count
}
func TestIssue387(t *testing.T) {
// a breakpoint triggering during a 'next' operation will interrupt it
withTestTerminal("issue387", t, func(term *FakeTerminal) {
breakpointHitCount := 0
term.MustExec("break dostuff")
for {
outstr, err := term.Exec("continue")
breakpointHitCount += countOccourences(outstr, "issue387.go:8")
t.Log(outstr)
if err != nil {
if strings.Index(err.Error(), "exited") < 0 {
t.Fatalf("Unexpected error executing 'continue': %v", err)
}
break
}
pos := 9
for {
outstr = term.MustExec("next")
breakpointHitCount += countOccourences(outstr, "issue387.go:8")
t.Log(outstr)
if countOccourences(outstr, fmt.Sprintf("issue387.go:%d", pos)) == 0 {
t.Fatalf("did not continue to expected position %d", pos)
}
pos++
if pos > 11 {
break
}
}
}
if breakpointHitCount != 10 {
t.Fatalf("Breakpoint hit wrong number of times, expected 10 got %d", breakpointHitCount)
}
})
}