proc,terminal: allow setting suspended breakpoints (#3154)

Allows setting suspended breakpoints and try to enable them
automatically after every time a plugin is loaded.

Fixes #1653
Updates #2551
This commit is contained in:
Alessandro Arzilli 2022-10-04 17:07:05 +02:00 committed by GitHub
parent aa03666596
commit 6e7e1d8830
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 223 additions and 53 deletions

@ -26,7 +26,7 @@ checkpoint(Where) | Equivalent to API call [Checkpoint](https://godoc.org/github
clear_breakpoint(Id, Name) | Equivalent to API call [ClearBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ClearBreakpoint)
clear_checkpoint(ID) | Equivalent to API call [ClearCheckpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ClearCheckpoint)
raw_command(Name, ThreadID, GoroutineID, ReturnInfoLoadConfig, Expr, UnsafeCall) | Equivalent to API call [Command](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Command)
create_breakpoint(Breakpoint, LocExpr, SubstitutePathRules) | Equivalent to API call [CreateBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CreateBreakpoint)
create_breakpoint(Breakpoint, LocExpr, SubstitutePathRules, Suspended) | Equivalent to API call [CreateBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CreateBreakpoint)
create_ebpf_tracepoint(FunctionName) | Equivalent to API call [CreateEBPFTracepoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CreateEBPFTracepoint)
create_watchpoint(Scope, Expr, Type) | Equivalent to API call [CreateWatchpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.CreateWatchpoint)
detach(Kill) | Equivalent to API call [Detach](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Detach)

@ -270,6 +270,10 @@ func packageMatch(specPkg, symPkg string, packageMap map[string][]string) bool {
// Find will search all functions in the target program and filter them via the
// regex location spec. Only functions matching the regex will be returned.
func (loc *RegexLocationSpec) Find(t *proc.Target, _ []string, scope *proc.EvalScope, locStr string, includeNonExecutableLines bool, _ [][2]string) ([]api.Location, error) {
if scope == nil {
//TODO(aarzilli): this needs only the list of function we should make it work
return nil, fmt.Errorf("could not determine location (scope is nil)")
}
funcs := scope.BinInfo.Functions
matches, err := regexFilterFuncs(loc.FuncRegex, funcs)
if err != nil {
@ -390,7 +394,10 @@ func (loc *NormalLocationSpec) Find(t *proc.Target, processArgs []string, scope
candidateFuncs = loc.findFuncCandidates(t.BinInfo(), limit)
}
if matching := len(candidateFiles) + len(candidateFuncs); matching == 0 && scope != nil {
if matching := len(candidateFiles) + len(candidateFuncs); matching == 0 {
if scope == nil {
return nil, fmt.Errorf("location \"%s\" not found", locStr)
}
// if no result was found this locations string could be an
// expression that the user forgot to prefix with '*', try treating it as
// such.

@ -392,6 +392,27 @@ func FirstPCAfterPrologue(p Process, fn *Function, sameline bool) (uint64, error
return pc, nil
}
func findRetPC(t *Target, name string) ([]uint64, error) {
fn := t.BinInfo().LookupFunc[name]
if fn == nil {
return nil, fmt.Errorf("could not find %s", name)
}
text, err := Disassemble(t.Memory(), nil, t.Breakpoints(), t.BinInfo(), fn.Entry, fn.End)
if err != nil {
return nil, err
}
r := []uint64{}
for _, instr := range text {
if instr.IsRet() {
r = append(r, instr.Loc.PC)
}
}
if len(r) == 0 {
return nil, fmt.Errorf("could not find return instruction in %s", name)
}
return r, nil
}
// cpuArch is a stringer interface representing CPU architectures.
type cpuArch interface {
String() string

@ -131,6 +131,10 @@ const (
// adjust the watchpoint of stack variables.
StackResizeBreakpoint
// PluginOpenBreakpoint is a breakpoint used to detect that a plugin has
// been loaded and we should try to enable suspended breakpoints.
PluginOpenBreakpoint
steppingMask = NextBreakpoint | NextDeferBreakpoint | StepBreakpoint
)
@ -204,6 +208,8 @@ func (bp *Breakpoint) VerboseDescr() []string {
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)))
case PluginOpenBreakpoint:
r = append(r, "PluginOpenBreakpoint")
default:
r = append(r, fmt.Sprintf("Unknown %d", breaklet.Kind))
}
@ -304,7 +310,7 @@ func (bpstate *BreakpointState) checkCond(tgt *Target, breaklet *Breaklet, threa
}
}
case StackResizeBreakpoint:
case StackResizeBreakpoint, PluginOpenBreakpoint:
// no further checks
default:

@ -96,27 +96,15 @@ func (t *Target) setStackWatchBreakpoints(scope *EvalScope, watchpoint *Breakpoi
// 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)
retpcs, err := findRetPC(t, "runtime.copystack")
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 len(retpcs) > 1 {
return errors.New("runtime.copystack has too many return instructions")
}
if retpc == 0 {
return errors.New("could not find return instruction in runtime.copystack")
}
rszbp, err := t.SetBreakpoint(0, retpc, StackResizeBreakpoint, sameGCond)
rszbp, err := t.SetBreakpoint(0, retpcs[0], StackResizeBreakpoint, sameGCond)
if err != nil {
return err
}

@ -10,6 +10,7 @@ import (
"github.com/go-delve/delve/pkg/dwarf/op"
"github.com/go-delve/delve/pkg/goversion"
"github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc/internal/ebpf"
)
@ -213,6 +214,7 @@ func NewTarget(p ProcessInternal, pid int, currentThread Thread, cfg NewTargetCo
t.createUnrecoveredPanicBreakpoint()
t.createFatalThrowBreakpoint()
t.createPluginOpenBreakpoint()
t.gcache.init(p.BinInfo())
t.fakeMemoryRegistryMap = make(map[string]*compositeMemory)
@ -426,6 +428,21 @@ func (t *Target) createFatalThrowBreakpoint() {
}
}
// createPluginOpenBreakpoint creates a breakpoint at the return instruction
// of plugin.Open (if it exists) that will try to enable suspended
// breakpoints.
func (t *Target) createPluginOpenBreakpoint() {
retpcs, _ := findRetPC(t, "plugin.Open")
for _, retpc := range retpcs {
bp, err := t.SetBreakpoint(0, retpc, PluginOpenBreakpoint, nil)
if err != nil {
t.BinInfo().logger.Errorf("could not set plugin.Open breakpoint: %v", err)
} else {
bp.Breaklets[len(bp.Breaklets)-1].callback = t.pluginOpenCallback
}
}
}
// CurrentThread returns the currently selected thread which will be used
// for next/step/stepout and for reading variables, unless a goroutine is
// selected.
@ -588,6 +605,30 @@ func (t *Target) dwrapUnwrap(fn *Function) *Function {
return fn
}
func (t *Target) pluginOpenCallback(Thread) bool {
logger := logflags.DebuggerLogger()
for _, lbp := range t.Breakpoints().Logical {
if isSuspended(t, lbp) {
err := enableBreakpointOnTarget(t, lbp)
if err != nil {
logger.Debugf("could not enable breakpoint %d: %v", lbp.LogicalID, err)
} else {
logger.Debugf("suspended breakpoint %d enabled", lbp.LogicalID)
}
}
}
return false
}
func isSuspended(t *Target, lbp *LogicalBreakpoint) bool {
for _, bp := range t.Breakpoints().M {
if bp.LogicalID() == lbp.LogicalID {
return false
}
}
return true
}
type dummyRecordingManipulation struct {
}

@ -1741,19 +1741,37 @@ func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]
}
requestedBp.Tracepoint = tracepoint
locs, err := t.client.FindLocation(ctx.Scope, spec, true, t.substitutePathRules())
if err != nil {
if requestedBp.Name == "" {
return nil, err
}
locs, findLocErr := t.client.FindLocation(ctx.Scope, spec, true, t.substitutePathRules())
if findLocErr != nil && requestedBp.Name != "" {
requestedBp.Name = ""
spec = argstr
var err2 error
locs, err2 = t.client.FindLocation(ctx.Scope, spec, true, t.substitutePathRules())
if err2 != nil {
return nil, err
if err2 == nil {
findLocErr = nil
}
}
if findLocErr != nil && shouldAskToSuspendBreakpoint(t) {
fmt.Fprintf(os.Stderr, "Command failed: %s\n", findLocErr.Error())
findLocErr = nil
answer, err := yesno(t.line, "Set a suspended breakpoint (Delve will try to set this breakpoint when a plugin is loaded) [Y/n]?")
if err != nil {
return nil, err
}
if !answer {
return nil, nil
}
bp, err := t.client.CreateBreakpointWithExpr(requestedBp, spec, t.substitutePathRules(), true)
if err != nil {
return nil, err
}
fmt.Fprintf(t.stdout, "%s set at %s\n", formatBreakpointName(bp, true), t.formatBreakpointLocation(bp))
return nil, nil
}
if findLocErr != nil {
return nil, findLocErr
}
created := []*api.Breakpoint{}
for _, loc := range locs {
requestedBp.Addr = loc.PC
@ -1763,7 +1781,7 @@ func setBreakpoint(t *Term, ctx callContext, tracepoint bool, argstr string) ([]
requestedBp.LoadArgs = &ShortLoadConfig
}
bp, err := t.client.CreateBreakpointWithExpr(requestedBp, spec, t.substitutePathRules())
bp, err := t.client.CreateBreakpointWithExpr(requestedBp, spec, t.substitutePathRules(), false)
if err != nil {
return nil, err
}
@ -3199,3 +3217,8 @@ func (t *Term) formatBreakpointLocation(bp *api.Breakpoint) string {
}
return out.String()
}
func shouldAskToSuspendBreakpoint(t *Term) bool {
fns, _ := t.client.ListFunctions(`^plugin\.Open$`)
return len(fns) > 0
}

@ -319,6 +319,12 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
return starlark.None, decorateError(thread, err)
}
}
if len(args) > 3 && args[3] != starlark.None {
err := unmarshalStarlarkValue(args[3], &rpcArgs.Suspended, "Suspended")
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
for _, kv := range kwargs {
var err error
switch kv[0].(starlark.String) {
@ -328,6 +334,8 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
err = unmarshalStarlarkValue(kv[1], &rpcArgs.LocExpr, "LocExpr")
case "SubstitutePathRules":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.SubstitutePathRules, "SubstitutePathRules")
case "Suspended":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Suspended, "Suspended")
default:
err = fmt.Errorf("unknown argument %q", kv[0])
}

@ -70,7 +70,7 @@ type Client interface {
// CreateBreakpoint creates a new breakpoint.
CreateBreakpoint(*api.Breakpoint) (*api.Breakpoint, error)
// CreateBreakpointWithExpr creates a new breakpoint and sets an expression to restore it after it is disabled.
CreateBreakpointWithExpr(*api.Breakpoint, string, [][2]string) (*api.Breakpoint, error)
CreateBreakpointWithExpr(*api.Breakpoint, string, [][2]string, bool) (*api.Breakpoint, error)
// CreateWatchpoint creates a new watchpoint.
CreateWatchpoint(api.EvalScope, string, api.WatchType) (*api.Breakpoint, error)
// ListBreakpoints gets all breakpoints.

@ -1381,7 +1381,7 @@ func (s *Session) setBreakpoints(prefix string, totalBps int, metadataFunc func(
err = setLogMessage(bp, want.logMessage)
if err == nil {
// Create new breakpoints.
got, err = s.debugger.CreateBreakpoint(bp, "", nil)
got, err = s.debugger.CreateBreakpoint(bp, "", nil, false)
}
}
}

@ -641,7 +641,11 @@ func (d *Debugger) state(retLoadCfg *proc.LoadConfig, withBreakpointInfo bool) (
//
// If LocExpr is specified it will be used, along with substitutePathRules,
// to re-enable the breakpoint after it is disabled.
func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint, locExpr string, substitutePathRules [][2]string) (*api.Breakpoint, error) {
//
// If suspended is true a logical breakpoint will be created even if the
// location can not be found, the backend will attempt to enable the
// breakpoint every time a new plugin is loaded.
func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint, locExpr string, substitutePathRules [][2]string, suspended bool) (*api.Breakpoint, error) {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
@ -721,12 +725,13 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint, locExpr string,
setbp.Expr = func(t *proc.Target) []uint64 {
locs, err := loc.Find(t, d.processArgs, nil, locExpr, false, substitutePathRules)
if err != nil || len(locs) != 1 {
logflags.DebuggerLogger().Debugf("could not evaluate breakpoint expression %q: %v (number of results %d)", locExpr, err, len(locs))
return nil
}
return locs[0].PCs
}
}
createdBp, err := createLogicalBreakpoint(d, requestedBp, &setbp)
createdBp, err := createLogicalBreakpoint(d, requestedBp, &setbp, suspended)
if err != nil {
return nil, err
@ -761,7 +766,7 @@ func (d *Debugger) ConvertThreadBreakpoint(thread proc.Thread) *api.Breakpoint {
// createLogicalBreakpoint creates one physical breakpoint for each address
// in addrs and associates all of them with the same logical breakpoint.
func createLogicalBreakpoint(d *Debugger, requestedBp *api.Breakpoint, setbp *proc.SetBreakpoint) (*api.Breakpoint, error) {
func createLogicalBreakpoint(d *Debugger, requestedBp *api.Breakpoint, setbp *proc.SetBreakpoint, suspended bool) (*api.Breakpoint, error) {
id := requestedBp.ID
var lbp *proc.LogicalBreakpoint
@ -781,8 +786,12 @@ func createLogicalBreakpoint(d *Debugger, requestedBp *api.Breakpoint, setbp *pr
err = d.target.EnableBreakpoint(lbp)
if err != nil {
delete(d.target.LogicalBreakpoints, lbp.LogicalID)
return nil, err
if suspended {
logflags.DebuggerLogger().Debugf("could not enable new breakpoint: %v (breakpoint will be suspended)", err)
} else {
delete(d.target.LogicalBreakpoints, lbp.LogicalID)
return nil, err
}
}
return d.convertBreakpoint(lbp), nil

@ -106,7 +106,7 @@ func (s *RPCServer) CreateBreakpoint(bp, newBreakpoint *api.Breakpoint) error {
if err := api.ValidBreakpointName(bp.Name); err != nil {
return err
}
createdbp, err := s.debugger.CreateBreakpoint(bp, "", nil)
createdbp, err := s.debugger.CreateBreakpoint(bp, "", nil, false)
if err != nil {
return err
}

@ -245,16 +245,16 @@ func (c *RPCClient) GetBreakpointByName(name string) (*api.Breakpoint, error) {
// https://pkg.go.dev/github.com/go-delve/delve/service/debugger#Debugger.CreateBreakpoint
func (c *RPCClient) CreateBreakpoint(breakPoint *api.Breakpoint) (*api.Breakpoint, error) {
var out CreateBreakpointOut
err := c.call("CreateBreakpoint", CreateBreakpointIn{*breakPoint, "", nil}, &out)
err := c.call("CreateBreakpoint", CreateBreakpointIn{*breakPoint, "", nil, false}, &out)
return &out.Breakpoint, err
}
// CreateBreakpointWithExpr is like CreateBreakpoint but will also set a
// location expression to be used to restore the breakpoint after it is
// disabled.
func (c *RPCClient) CreateBreakpointWithExpr(breakPoint *api.Breakpoint, locExpr string, substitutePathRules [][2]string) (*api.Breakpoint, error) {
func (c *RPCClient) CreateBreakpointWithExpr(breakPoint *api.Breakpoint, locExpr string, substitutePathRules [][2]string, suspended bool) (*api.Breakpoint, error) {
var out CreateBreakpointOut
err := c.call("CreateBreakpoint", CreateBreakpointIn{*breakPoint, locExpr, substitutePathRules}, &out)
err := c.call("CreateBreakpoint", CreateBreakpointIn{*breakPoint, locExpr, substitutePathRules, suspended}, &out)
return &out.Breakpoint, err
}

@ -245,6 +245,7 @@ type CreateBreakpointIn struct {
LocExpr string
SubstitutePathRules [][2]string
Suspended bool
}
type CreateBreakpointOut struct {
@ -259,7 +260,7 @@ func (s *RPCServer) CreateBreakpoint(arg CreateBreakpointIn, out *CreateBreakpoi
if err := api.ValidBreakpointName(arg.Breakpoint.Name); err != nil {
return err
}
createdbp, err := s.debugger.CreateBreakpoint(&arg.Breakpoint, arg.LocExpr, arg.SubstitutePathRules)
createdbp, err := s.debugger.CreateBreakpoint(&arg.Breakpoint, arg.LocExpr, arg.SubstitutePathRules, arg.Suspended)
if err != nil {
return err
}

@ -55,12 +55,12 @@ func TestMain(m *testing.M) {
}
func withTestClient2(name string, t *testing.T, fn func(c service.Client)) {
withTestClient2Extended(name, t, 0, [3]string{}, func(c service.Client, fixture protest.Fixture) {
withTestClient2Extended(name, t, 0, [3]string{}, nil, func(c service.Client, fixture protest.Fixture) {
fn(c)
})
}
func startServer(name string, buildFlags protest.BuildFlags, t *testing.T, redirects [3]string) (clientConn net.Conn, fixture protest.Fixture) {
func startServer(name string, buildFlags protest.BuildFlags, t *testing.T, redirects [3]string, args []string) (clientConn net.Conn, fixture protest.Fixture) {
if testBackend == "rr" {
protest.MustHaveRecordingAllowed(t)
}
@ -77,7 +77,7 @@ func startServer(name string, buildFlags protest.BuildFlags, t *testing.T, redir
}
server := rpccommon.NewServer(&service.Config{
Listener: listener,
ProcessArgs: []string{fixture.Path},
ProcessArgs: append([]string{fixture.Path}, args...),
Debugger: debugger.Config{
Backend: testBackend,
CheckGoVersion: true,
@ -93,8 +93,8 @@ func startServer(name string, buildFlags protest.BuildFlags, t *testing.T, redir
return clientConn, fixture
}
func withTestClient2Extended(name string, t *testing.T, buildFlags protest.BuildFlags, redirects [3]string, fn func(c service.Client, fixture protest.Fixture)) {
clientConn, fixture := startServer(name, buildFlags, t, redirects)
func withTestClient2Extended(name string, t *testing.T, buildFlags protest.BuildFlags, redirects [3]string, args []string, fn func(c service.Client, fixture protest.Fixture)) {
clientConn, fixture := startServer(name, buildFlags, t, redirects, args)
client := rpc2.NewClientFromConn(clientConn)
defer func() {
client.Detach(true)
@ -230,7 +230,7 @@ func TestRestart_rebuild(t *testing.T) {
// In the original fixture file the env var tested for is SOMEVAR.
os.Setenv("SOMEVAR", "bah")
withTestClient2Extended("testenv", t, 0, [3]string{}, func(c service.Client, f protest.Fixture) {
withTestClient2Extended("testenv", t, 0, [3]string{}, nil, func(c service.Client, f protest.Fixture) {
<-c.Continue()
var1, err := c.EvalVariable(api.EvalScope{GoroutineID: -1}, "x", normalLoadConfig)
@ -1019,7 +1019,7 @@ func TestClientServer_FindLocations(t *testing.T) {
findLocationHelper(t, c, "main.stacktraceme", false, 1, stacktracemeAddr)
})
withTestClient2Extended("locationsUpperCase", t, 0, [3]string{}, func(c service.Client, fixture protest.Fixture) {
withTestClient2Extended("locationsUpperCase", t, 0, [3]string{}, nil, func(c service.Client, fixture protest.Fixture) {
// Upper case
findLocationHelper(t, c, "locationsUpperCase.go:6", false, 1, 0)
@ -1724,7 +1724,7 @@ func TestClientServer_FpRegisters(t *testing.T) {
{"XMM12", "…[ZMM12hh] 0x3ff66666666666663ff4cccccccccccd"},
}
protest.AllowRecording(t)
withTestClient2Extended("fputest/", t, 0, [3]string{}, func(c service.Client, fixture protest.Fixture) {
withTestClient2Extended("fputest/", t, 0, [3]string{}, nil, func(c service.Client, fixture protest.Fixture) {
_, err := c.CreateBreakpoint(&api.Breakpoint{File: filepath.Join(fixture.BuildDir, "fputest.go"), Line: 25})
assertNoError(err, t, "CreateBreakpoint")
@ -2276,7 +2276,7 @@ func (c *brokenRPCClient) call(method string, args, reply interface{}) error {
}
func TestUnknownMethodCall(t *testing.T) {
clientConn, _ := startServer("continuetestprog", 0, t, [3]string{})
clientConn, _ := startServer("continuetestprog", 0, t, [3]string{}, nil)
client := &brokenRPCClient{jsonrpc.NewClient(clientConn)}
client.call("SetApiVersion", api.SetAPIVersionIn{APIVersion: 2}, &api.SetAPIVersionOut{})
defer client.Detach(true)
@ -2420,7 +2420,7 @@ func TestClearLogicalBreakpoint(t *testing.T) {
// Clearing a logical breakpoint should clear all associated physical
// breakpoints.
// Issue #1955.
withTestClient2Extended("testinline", t, protest.EnableInlining, [3]string{}, func(c service.Client, fixture protest.Fixture) {
withTestClient2Extended("testinline", t, protest.EnableInlining, [3]string{}, nil, func(c service.Client, fixture protest.Fixture) {
bp, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.inlineThis"})
assertNoError(err, t, "CreateBreakpoint()")
t.Logf("breakpoint set at %#v", bp.Addrs)
@ -2446,7 +2446,7 @@ func TestRedirects(t *testing.T) {
outfile = "redirect-output.txt"
)
protest.AllowRecording(t)
withTestClient2Extended("redirect", t, 0, [3]string{infile, outfile, ""}, func(c service.Client, fixture protest.Fixture) {
withTestClient2Extended("redirect", t, 0, [3]string{infile, outfile, ""}, nil, func(c service.Client, fixture protest.Fixture) {
outpath := filepath.Join(fixture.BuildDir, outfile)
<-c.Continue()
buf, err := ioutil.ReadFile(outpath)
@ -2774,7 +2774,7 @@ func TestClientServer_SinglelineStringFormattedWithBigInts(t *testing.T) {
if runtime.GOARCH != "amd64" {
t.Skip("N/A")
}
withTestClient2Extended("xmm0print/", t, 0, [3]string{}, func(c service.Client, fixture protest.Fixture) {
withTestClient2Extended("xmm0print/", t, 0, [3]string{}, nil, func(c service.Client, fixture protest.Fixture) {
_, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.VPSLLQ36", Line: 4})
assertNoError(err, t, "CreateBreakpoint")
state := <-c.Continue()
@ -2872,7 +2872,7 @@ func TestRestart_PreserveFunctionBreakpoint(t *testing.T) {
copy(filepath.Join(dir, "testfnpos1.go"))
withTestClient2Extended("testfnpos", t, 0, [3]string{}, func(c service.Client, f protest.Fixture) {
withTestClient2Extended("testfnpos", t, 0, [3]string{}, nil, func(c service.Client, f protest.Fixture) {
_, err := c.CreateBreakpoint(&api.Breakpoint{FunctionName: "main.f1"})
assertNoError(err, t, "CreateBreakpoint")
state := <-c.Continue()
@ -2895,3 +2895,69 @@ func TestRestart_PreserveFunctionBreakpoint(t *testing.T) {
}
})
}
func assertLine(t *testing.T, state *api.DebuggerState, file string, lineno int) {
t.Helper()
t.Logf("%s:%d", state.CurrentThread.File, state.CurrentThread.Line)
if !strings.HasSuffix(state.CurrentThread.File, file) || state.CurrentThread.Line != lineno {
t.Fatalf("wrong location %s:%d", state.CurrentThread.File, state.CurrentThread.Line)
}
}
func TestPluginSuspendedBreakpoint(t *testing.T) {
// Tests that breakpoints created in a suspended state will be enabled automatically when a plugin is loaded.
pluginFixtures := protest.WithPlugins(t, protest.AllNonOptimized, "plugin1/", "plugin2/")
dir, err := filepath.Abs(protest.FindFixturesDir())
assertNoError(err, t, "filepath.Abs")
withTestClient2Extended("plugintest", t, protest.AllNonOptimized, [3]string{}, []string{pluginFixtures[0].Path, pluginFixtures[1].Path}, func(c service.Client, f protest.Fixture) {
_, err := c.CreateBreakpointWithExpr(&api.Breakpoint{FunctionName: "github.com/go-delve/delve/_fixtures/plugin1.Fn1", Line: 1}, "", nil, true)
assertNoError(err, t, "CreateBreakpointWithExpr(Fn1) (suspended)")
_, err = c.CreateBreakpointWithExpr(&api.Breakpoint{File: filepath.Join(dir, "plugin2", "plugin2.go"), Line: 9}, "", nil, true)
assertNoError(err, t, "CreateBreakpointWithExpr(plugin2.go:9) (suspended)")
cont := func(name, file string, lineno int) {
t.Helper()
state := <-c.Continue()
assertNoError(state.Err, t, name)
assertLine(t, state, file, lineno)
}
cont("Continue 1", "plugintest.go", 22)
cont("Continue 2", "plugintest.go", 27)
cont("Continue 3", "plugin1.go", 6)
cont("Continue 4", "plugin2.go", 9)
})
withTestClient2Extended("plugintest", t, protest.AllNonOptimized, [3]string{}, []string{pluginFixtures[0].Path, pluginFixtures[1].Path}, func(c service.Client, f protest.Fixture) {
exprbreak := func(expr string) {
t.Helper()
_, err := c.CreateBreakpointWithExpr(&api.Breakpoint{}, expr, nil, true)
assertNoError(err, t, fmt.Sprintf("CreateBreakpointWithExpr(%s) (suspended)", expr))
}
cont := func(name, file string, lineno int) {
t.Helper()
state := <-c.Continue()
assertNoError(state.Err, t, name)
assertLine(t, state, file, lineno)
}
exprbreak("plugin1.Fn1")
exprbreak("plugin2.go:9")
// The following breakpoints can never be un-suspended because the
// expression is never resolved, but this shouldn't cause problems
exprbreak("m[0]")
exprbreak("*m[0]")
exprbreak("unknownfn")
exprbreak("+2")
cont("Continue 1", "plugintest.go", 22)
cont("Continue 2", "plugintest.go", 27)
cont("Continue 3", "plugin1.go", 5)
cont("Continue 4", "plugin2.go", 9)
})
}