proc, terminal: next on parked goroutines
Previously Next would step through the goroutine associated with CurrentThread if SelectedGoroutine was parked Also fixes a bug with proc.(*Process).StepInto where StepInto could switch to a different goroutine.
This commit is contained in:
parent
323c0450d1
commit
d17c7c3a61
64
proc/proc.go
64
proc/proc.go
@ -225,8 +225,13 @@ func (dbp *Process) SetBreakpoint(addr uint64) (*Breakpoint, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetTempBreakpoint sets a temp breakpoint. Used during 'next' operations.
|
// SetTempBreakpoint sets a temp breakpoint. Used during 'next' operations.
|
||||||
func (dbp *Process) SetTempBreakpoint(addr uint64) (*Breakpoint, error) {
|
func (dbp *Process) SetTempBreakpoint(addr uint64, cond ast.Expr) (*Breakpoint, error) {
|
||||||
return dbp.setBreakpoint(dbp.CurrentThread.ID, addr, true)
|
bp, err := dbp.setBreakpoint(dbp.CurrentThread.ID, addr, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bp.Cond = cond
|
||||||
|
return bp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearBreakpoint clears the breakpoint at addr.
|
// ClearBreakpoint clears the breakpoint at addr.
|
||||||
@ -264,14 +269,6 @@ func (dbp *Process) Next() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the goroutine for the current thread. We will
|
|
||||||
// use it later in order to ensure we are on the same
|
|
||||||
// goroutine.
|
|
||||||
g, err := dbp.CurrentThread.GetG()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set breakpoints for any goroutine that is currently
|
// Set breakpoints for any goroutine that is currently
|
||||||
// blocked trying to read from a channel. This is so that
|
// blocked trying to read from a channel. This is so that
|
||||||
// if control flow switches to that goroutine, we end up
|
// if control flow switches to that goroutine, we end up
|
||||||
@ -280,36 +277,15 @@ func (dbp *Process) Next() (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var goroutineExiting bool
|
if err = dbp.setNextBreakpoints(); err != nil {
|
||||||
if err = dbp.CurrentThread.setNextBreakpoints(); err != nil {
|
switch err.(type) {
|
||||||
switch t := err.(type) {
|
|
||||||
case ThreadBlockedError, NoReturnAddr: // Noop
|
case ThreadBlockedError, NoReturnAddr: // Noop
|
||||||
case GoroutineExitingError:
|
|
||||||
goroutineExiting = t.goid == g.ID
|
|
||||||
default:
|
default:
|
||||||
dbp.ClearTempBreakpoints()
|
dbp.ClearTempBreakpoints()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !goroutineExiting {
|
|
||||||
for i := range dbp.Breakpoints {
|
|
||||||
if dbp.Breakpoints[i].Temp {
|
|
||||||
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)},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbp.Continue()
|
return dbp.Continue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,7 +305,7 @@ func (dbp *Process) setChanRecvBreakpoints() (int, error) {
|
|||||||
}
|
}
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if _, err = dbp.SetTempBreakpoint(ret); err != nil {
|
if _, err = dbp.SetTempBreakpoint(ret, nil); err != nil {
|
||||||
if _, ok := err.(BreakpointExistsError); ok {
|
if _, ok := err.(BreakpointExistsError); ok {
|
||||||
// Ignore duplicate breakpoints in case if multiple
|
// Ignore duplicate breakpoints in case if multiple
|
||||||
// goroutines wait on the same channel
|
// goroutines wait on the same channel
|
||||||
@ -492,12 +468,30 @@ func (dbp *Process) StepInto(fn *gosym.Func) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pc, _ := dbp.FirstPCAfterPrologue(fn, false)
|
pc, _ := dbp.FirstPCAfterPrologue(fn, false)
|
||||||
if _, err := dbp.SetTempBreakpoint(pc); err != nil {
|
if _, err := dbp.SetTempBreakpoint(pc, sameGoroutineCondition(dbp.SelectedGoroutine)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return dbp.Continue()
|
return dbp.Continue()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns an expression that evaluates to true when the current goroutine is g
|
||||||
|
func sameGoroutineCondition(g *G) ast.Expr {
|
||||||
|
if g == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &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)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// StepInstruction will continue the current thread for exactly
|
// StepInstruction will continue the current thread for exactly
|
||||||
// one instruction. This method affects only the thread
|
// one instruction. This method affects only the thread
|
||||||
// asssociated with the selected goroutine. All other
|
// asssociated with the selected goroutine. All other
|
||||||
|
@ -485,7 +485,8 @@ func TestNextFunctionReturnDefer(t *testing.T) {
|
|||||||
{5, 8},
|
{5, 8},
|
||||||
{8, 9},
|
{8, 9},
|
||||||
{9, 10},
|
{9, 10},
|
||||||
{10, 7},
|
{10, 6},
|
||||||
|
{6, 7},
|
||||||
{7, 8},
|
{7, 8},
|
||||||
}
|
}
|
||||||
testnext("testnextdefer", testcases, "main.main", t)
|
testnext("testnextdefer", testcases, "main.main", t)
|
||||||
@ -1751,3 +1752,40 @@ func TestIssue554(t *testing.T) {
|
|||||||
t.Fatalf("should be false")
|
t.Fatalf("should be false")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNextParked(t *testing.T) {
|
||||||
|
withTestProcess("parallel_next", t, func(p *Process, fixture protest.Fixture) {
|
||||||
|
bp, err := setFunctionBreakpoint(p, "main.sayhi")
|
||||||
|
assertNoError(err, t, "SetBreakpoint()")
|
||||||
|
|
||||||
|
// continue until a parked goroutine exists
|
||||||
|
var parkedg *G
|
||||||
|
LookForParkedG:
|
||||||
|
for {
|
||||||
|
err := p.Continue()
|
||||||
|
if _, exited := err.(ProcessExitedError); exited {
|
||||||
|
t.Log("could not find parked goroutine")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assertNoError(err, t, "Continue()")
|
||||||
|
|
||||||
|
gs, err := p.GoroutinesInfo()
|
||||||
|
assertNoError(err, t, "GoroutinesInfo()")
|
||||||
|
|
||||||
|
for _, g := range gs {
|
||||||
|
if g.thread == nil {
|
||||||
|
parkedg = g
|
||||||
|
break LookForParkedG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNoError(p.SwitchGoroutine(parkedg.ID), t, "SwitchGoroutine()")
|
||||||
|
p.ClearBreakpoint(bp.Addr)
|
||||||
|
assertNoError(p.Next(), t, "Next()")
|
||||||
|
|
||||||
|
if p.SelectedGoroutine.ID != parkedg.ID {
|
||||||
|
t.Fatalf("Next did not continue on the selected goroutine, expected %d got %d", parkedg.ID, p.SelectedGoroutine.ID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
115
proc/threads.go
115
proc/threads.go
@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -124,79 +125,54 @@ func (tbe ThreadBlockedError) Error() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set breakpoints for potential next lines.
|
// Set breakpoints for potential next lines.
|
||||||
func (thread *Thread) setNextBreakpoints() (err error) {
|
func (dbp *Process) setNextBreakpoints() (err error) {
|
||||||
if thread.blocked() {
|
var frames []Stackframe
|
||||||
return ThreadBlockedError{}
|
|
||||||
|
if dbp.SelectedGoroutine == nil {
|
||||||
|
if dbp.CurrentThread.blocked() {
|
||||||
|
return ThreadBlockedError{}
|
||||||
|
}
|
||||||
|
frames, err = dbp.CurrentThread.Stacktrace(0)
|
||||||
|
} else {
|
||||||
|
frames, err = dbp.SelectedGoroutine.Stacktrace(0)
|
||||||
}
|
}
|
||||||
curpc, err := thread.PC()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(frames) < 1 {
|
||||||
|
return errors.New("empty stack trace")
|
||||||
|
}
|
||||||
|
|
||||||
// Grab info on our current stack frame. Used to determine
|
// Grab info on our current stack frame. Used to determine
|
||||||
// whether we may be stepping outside of the current function.
|
// whether we may be stepping outside of the current function.
|
||||||
fde, err := thread.dbp.frameEntries.FDEForPC(curpc)
|
fde, err := dbp.frameEntries.FDEForPC(frames[0].Current.PC)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current file/line.
|
if filepath.Ext(frames[0].Current.File) != ".go" {
|
||||||
loc, err := thread.Location()
|
return dbp.cnext(frames[0], fde)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if filepath.Ext(loc.File) == ".go" {
|
|
||||||
err = thread.next(curpc, fde, loc.File, loc.Line)
|
|
||||||
} else {
|
|
||||||
err = thread.cnext(curpc, fde, loc.File)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GoroutineExitingError is returned when the
|
return dbp.next(dbp.SelectedGoroutine, frames[0], fde)
|
||||||
// goroutine specified by `goid` is in the process
|
|
||||||
// of exiting.
|
|
||||||
type GoroutineExitingError struct {
|
|
||||||
goid int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ge GoroutineExitingError) Error() string {
|
|
||||||
return fmt.Sprintf("goroutine %d is exiting", ge.goid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set breakpoints at every line, and the return address. Also look for
|
// Set breakpoints at every line, and the return address. Also look for
|
||||||
// a deferred function and set a breakpoint there too.
|
// a deferred function and set a breakpoint there too.
|
||||||
func (thread *Thread) next(curpc uint64, fde *frame.FrameDescriptionEntry, file string, line int) error {
|
// The first return value is set to true if the goroutine is in the process of exiting
|
||||||
pcs := thread.dbp.lineInfo.AllPCsBetween(fde.Begin(), fde.End()-1, file)
|
func (dbp *Process) next(g *G, topframe Stackframe, fde *frame.FrameDescriptionEntry) error {
|
||||||
|
pcs := dbp.lineInfo.AllPCsBetween(fde.Begin(), fde.End()-1, topframe.Current.File)
|
||||||
|
|
||||||
g, err := thread.GetG()
|
var deferpc uint64 = 0
|
||||||
if err != nil {
|
if g != nil && g.DeferPC != 0 {
|
||||||
return err
|
_, _, deferfn := dbp.goSymTable.PCToLine(g.DeferPC)
|
||||||
}
|
var err error
|
||||||
if g.DeferPC != 0 {
|
deferpc, err = dbp.FirstPCAfterPrologue(deferfn, false)
|
||||||
f, lineno, _ := thread.dbp.goSymTable.PCToLine(g.DeferPC)
|
if err != nil {
|
||||||
for {
|
return err
|
||||||
lineno++
|
|
||||||
dpc, _, err := thread.dbp.goSymTable.LineToPC(f, lineno)
|
|
||||||
if err == nil {
|
|
||||||
// We want to avoid setting an actual breakpoint on the
|
|
||||||
// entry point of the deferred function so instead create
|
|
||||||
// a fake breakpoint which will be cleaned up later.
|
|
||||||
thread.dbp.Breakpoints[g.DeferPC] = new(Breakpoint)
|
|
||||||
defer func() { delete(thread.dbp.Breakpoints, g.DeferPC) }()
|
|
||||||
if _, err = thread.dbp.SetTempBreakpoint(dpc); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ret, err := thread.ReturnAddress()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var covered bool
|
var covered bool
|
||||||
for i := range pcs {
|
for i := range pcs {
|
||||||
if fde.Cover(pcs[i]) {
|
if fde.Cover(pcs[i]) {
|
||||||
@ -206,38 +182,35 @@ func (thread *Thread) next(curpc uint64, fde *frame.FrameDescriptionEntry, file
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !covered {
|
if !covered {
|
||||||
fn := thread.dbp.goSymTable.PCToFunc(ret)
|
fn := dbp.goSymTable.PCToFunc(topframe.Ret)
|
||||||
if fn != nil && fn.Name == "runtime.goexit" {
|
if g != nil && fn != nil && fn.Name == "runtime.goexit" {
|
||||||
g, err := thread.GetG()
|
return nil
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return GoroutineExitingError{goid: g.ID}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pcs = append(pcs, ret)
|
if deferpc != 0 {
|
||||||
return thread.setNextTempBreakpoints(curpc, pcs)
|
pcs = append(pcs, deferpc)
|
||||||
|
}
|
||||||
|
pcs = append(pcs, topframe.Ret)
|
||||||
|
return dbp.setTempBreakpoints(topframe.Current.PC, pcs, sameGoroutineCondition(dbp.SelectedGoroutine))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a breakpoint at every reachable location, as well as the return address. Without
|
// Set a breakpoint at every reachable location, as well as the return address. Without
|
||||||
// the benefit of an AST we can't be sure we're not at a branching statement and thus
|
// the benefit of an AST we can't be sure we're not at a branching statement and thus
|
||||||
// cannot accurately predict where we may end up.
|
// cannot accurately predict where we may end up.
|
||||||
func (thread *Thread) cnext(curpc uint64, fde *frame.FrameDescriptionEntry, file string) error {
|
func (dbp *Process) cnext(topframe Stackframe, fde *frame.FrameDescriptionEntry) error {
|
||||||
pcs := thread.dbp.lineInfo.AllPCsBetween(fde.Begin(), fde.End(), file)
|
pcs := dbp.lineInfo.AllPCsBetween(fde.Begin(), fde.End(), topframe.Current.File)
|
||||||
ret, err := thread.ReturnAddress()
|
pcs = append(pcs, topframe.Ret)
|
||||||
if err != nil {
|
return dbp.setTempBreakpoints(topframe.Current.PC, pcs, sameGoroutineCondition(dbp.SelectedGoroutine))
|
||||||
return err
|
|
||||||
}
|
|
||||||
pcs = append(pcs, ret)
|
|
||||||
return thread.setNextTempBreakpoints(curpc, pcs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (thread *Thread) setNextTempBreakpoints(curpc uint64, pcs []uint64) error {
|
// setTempBreakpoints sets a breakpoint to all addresses specified in pcs
|
||||||
|
// skipping over curpc and curpc-1
|
||||||
|
func (dbp *Process) setTempBreakpoints(curpc uint64, pcs []uint64, cond ast.Expr) error {
|
||||||
for i := range pcs {
|
for i := range pcs {
|
||||||
if pcs[i] == curpc || pcs[i] == curpc-1 {
|
if pcs[i] == curpc || pcs[i] == curpc-1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, err := thread.dbp.SetTempBreakpoint(pcs[i]); err != nil {
|
if _, err := dbp.SetTempBreakpoint(pcs[i], cond); err != nil {
|
||||||
if _, ok := err.(BreakpointExistsError); !ok {
|
if _, ok := err.(BreakpointExistsError); !ok {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -101,7 +101,7 @@ See also: "help on", "help cond" and "help clear"`},
|
|||||||
{aliases: []string{"continue", "c"}, cmdFn: cont, helpMsg: "Run until breakpoint or program termination."},
|
{aliases: []string{"continue", "c"}, cmdFn: cont, helpMsg: "Run until breakpoint or program termination."},
|
||||||
{aliases: []string{"step", "s"}, cmdFn: step, helpMsg: "Single step through program."},
|
{aliases: []string{"step", "s"}, cmdFn: step, helpMsg: "Single step through program."},
|
||||||
{aliases: []string{"step-instruction", "si"}, cmdFn: stepInstruction, helpMsg: "Single step a single cpu instruction."},
|
{aliases: []string{"step-instruction", "si"}, cmdFn: stepInstruction, helpMsg: "Single step a single cpu instruction."},
|
||||||
{aliases: []string{"next", "n"}, cmdFn: next, helpMsg: "Step over to next source line."},
|
{aliases: []string{"next", "n"}, allowedPrefixes: scopePrefix, cmdFn: next, helpMsg: "Step over to next source line."},
|
||||||
{aliases: []string{"threads"}, cmdFn: threads, helpMsg: "Print out info for every traced thread."},
|
{aliases: []string{"threads"}, cmdFn: threads, helpMsg: "Print out info for every traced thread."},
|
||||||
{aliases: []string{"thread", "tr"}, cmdFn: thread, helpMsg: `Switch to the specified thread.
|
{aliases: []string{"thread", "tr"}, cmdFn: thread, helpMsg: `Switch to the specified thread.
|
||||||
|
|
||||||
@ -625,6 +625,17 @@ func stepInstruction(t *Term, ctx callContext, args string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func next(t *Term, ctx callContext, args string) error {
|
func next(t *Term, ctx callContext, args string) error {
|
||||||
|
if ctx.Prefix == scopePrefix {
|
||||||
|
if ctx.Scope.Frame != 0 {
|
||||||
|
return errors.New("can not prefix next with frame")
|
||||||
|
}
|
||||||
|
if ctx.Scope.GoroutineID > 0 {
|
||||||
|
_, err := t.client.SwitchGoroutine(ctx.Scope.GoroutineID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
state, err := t.client.Next()
|
state, err := t.client.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
Loading…
Reference in New Issue
Block a user