diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index d1c2cfd75d..7942ef1d87 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -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) diff --git a/pkg/locspec/locations.go b/pkg/locspec/locations.go index b69a589fae..8cb2c1c0d4 100644 --- a/pkg/locspec/locations.go +++ b/pkg/locspec/locations.go @@ -270,6 +270,9 @@ 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 { + 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 +393,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. diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index b3fff480e0..191ad56160 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -394,6 +394,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 diff --git a/pkg/proc/breakpoints.go b/pkg/proc/breakpoints.go index 488729d2a8..6f63150801 100644 --- a/pkg/proc/breakpoints.go +++ b/pkg/proc/breakpoints.go @@ -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)) } @@ -302,7 +308,7 @@ func (bpstate *BreakpointState) checkCond(tgt *Target, breaklet *Breaklet, threa } } - case StackResizeBreakpoint: + case StackResizeBreakpoint, PluginOpenBreakpoint: // no further checks default: diff --git a/pkg/proc/stackwatch.go b/pkg/proc/stackwatch.go index 8d48289bc4..a092bd674d 100644 --- a/pkg/proc/stackwatch.go +++ b/pkg/proc/stackwatch.go @@ -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 } diff --git a/pkg/proc/target.go b/pkg/proc/target.go index 26c92511cd..71a036c37b 100644 --- a/pkg/proc/target.go +++ b/pkg/proc/target.go @@ -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. @@ -586,6 +603,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 { } diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index c77ea38999..9f58151ca4 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -1713,19 +1713,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 { + 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 @@ -1735,7 +1753,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 } @@ -3112,3 +3130,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 +} diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index c3c502491b..ae892b348e 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -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]) } diff --git a/service/client.go b/service/client.go index 1266c8b921..307f7a58e7 100644 --- a/service/client.go +++ b/service/client.go @@ -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. diff --git a/service/dap/server.go b/service/dap/server.go index 93c1f159cb..93866a2b15 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -1383,7 +1383,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) } } } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index d20cf0af57..a83b88364f 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -636,7 +636,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() @@ -699,7 +703,9 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint, locExpr string, } } default: - addrs = []pidAddr{{d.target.Sel.Pid(), requestedBp.Addr}} + if requestedBp.Addr != 0 { + addrs = []pidAddr{{d.target.Sel.Pid(), requestedBp.Addr}} + } } if err != nil { @@ -714,6 +720,7 @@ 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 @@ -740,8 +747,12 @@ func (d *Debugger) CreateBreakpoint(requestedBp *api.Breakpoint, locExpr string, if len(addrs) == 0 { 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 + } } } else { bps := make([]*proc.Breakpoint, len(addrs)) diff --git a/service/rpc1/server.go b/service/rpc1/server.go index d6417d9838..0a96bd3e91 100644 --- a/service/rpc1/server.go +++ b/service/rpc1/server.go @@ -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 } diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 410a8fb50b..04b0bcd823 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -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 } diff --git a/service/rpc2/server.go b/service/rpc2/server.go index 653d43eff6..83f35a84ca 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -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 } diff --git a/service/test/integration2_test.go b/service/test/integration2_test.go index 9938c6547f..216f4a65aa 100644 --- a/service/test/integration2_test.go +++ b/service/test/integration2_test.go @@ -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) @@ -2833,7 +2833,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() @@ -2856,3 +2856,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) + }) +}