diff --git a/Documentation/cli/starlark.md b/Documentation/cli/starlark.md index a30b90ca..2f0e8e88 100644 --- a/Documentation/cli/starlark.md +++ b/Documentation/cli/starlark.md @@ -25,7 +25,7 @@ cancel_next() | Equivalent to API call [CancelNext](https://pkg.go.dev/github.co checkpoint(Where) | Equivalent to API call [Checkpoint](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.Checkpoint) clear_breakpoint(Id, Name) | Equivalent to API call [ClearBreakpoint](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.ClearBreakpoint) clear_checkpoint(ID) | Equivalent to API call [ClearCheckpoint](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.ClearCheckpoint) -raw_command(Name, ThreadID, GoroutineID, ReturnInfoLoadConfig, Expr, UnsafeCall) | Equivalent to API call [Command](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.Command) +raw_command(Name, ThreadID, GoroutineID, ReturnInfoLoadConfig, Expr, WithEvents, UnsafeCall) | Equivalent to API call [Command](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.Command) create_breakpoint(Breakpoint, LocExpr, SubstitutePathRules, Suspended) | Equivalent to API call [CreateBreakpoint](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.CreateBreakpoint) create_ebpf_tracepoint(FunctionName) | Equivalent to API call [CreateEBPFTracepoint](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.CreateEBPFTracepoint) create_watchpoint(Scope, Expr, Type) | Equivalent to API call [CreateWatchpoint](https://pkg.go.dev/github.com/go-delve/delve/service/rpc2#RPCServer.CreateWatchpoint) diff --git a/cmd/dlv/dlv_test.go b/cmd/dlv/dlv_test.go index 7f433aa7..b8ca3d70 100644 --- a/cmd/dlv/dlv_test.go +++ b/cmd/dlv/dlv_test.go @@ -454,7 +454,7 @@ func findCallCall(fndecl *ast.FuncDecl) *ast.CallExpr { continue } fun, issel := callx.Fun.(*ast.SelectorExpr) - if !issel || fun.Sel.Name != "call" { + if !issel || (fun.Sel.Name != "call" && fun.Sel.Name != "callWhileDrainingEvents") { continue } return callx @@ -467,9 +467,6 @@ func qf(*types.Package) string { } func TestTypecheckRPC(t *testing.T) { - if goversion.VersionAfterOrEqual(runtime.Version(), 1, 24) { - t.Skip("disabled due to export format changes") - } fset := &token.FileSet{} cfg := &packages.Config{ Mode: packages.NeedSyntax | packages.NeedTypesInfo | packages.NeedName | packages.NeedCompiledGoFiles | packages.NeedTypes, @@ -511,7 +508,7 @@ func TestTypecheckRPC(t *testing.T) { case "Continue", "Rewind": // wrappers over continueDir continue - case "SetReturnValuesLoadConfig", "Disconnect": + case "SetReturnValuesLoadConfig", "Disconnect", "SetEventsFn": // support functions continue } diff --git a/pkg/proc/bininfo.go b/pkg/proc/bininfo.go index 4149651f..9415cf65 100644 --- a/pkg/proc/bininfo.go +++ b/pkg/proc/bininfo.go @@ -115,6 +115,7 @@ type BinaryInfo struct { debugPinnerFn *Function logger logflags.Logger + eventsFn func(*Event) } var ( @@ -1559,7 +1560,19 @@ func (bi *BinaryInfo) openSeparateDebugInfo(image *Image, exe *elf.File, debugIn // has debuginfod so that we can use that in order to find any relevant debug information. if debugFilePath == "" { var err error - debugFilePath, err = debuginfod.GetDebuginfo(image.BuildID) + var notify func(string) + if bi.eventsFn != nil { + notify = func(s string) { + bi.eventsFn(&Event{ + Kind: EventBinaryInfoDownload, + BinaryInfoDownloadEventDetails: &BinaryInfoDownloadEventDetails{ + ImagePath: image.Path, + Progress: s, + }, + }) + } + } + debugFilePath, err = debuginfod.GetDebuginfo(notify, image.BuildID) if err != nil { return nil, nil, ErrNoDebugInfoFound } diff --git a/pkg/proc/debuginfod/debuginfod.go b/pkg/proc/debuginfod/debuginfod.go index e0297b1f..be02f92f 100644 --- a/pkg/proc/debuginfod/debuginfod.go +++ b/pkg/proc/debuginfod/debuginfod.go @@ -1,17 +1,51 @@ package debuginfod import ( + "bufio" + "bytes" + "os" "os/exec" "strings" + "time" ) const debuginfodFind = "debuginfod-find" +const notificationThrottle time.Duration = 1 * time.Second -func execFind(args ...string) (string, error) { +func execFind(notify func(string), args ...string) (string, error) { if _, err := exec.LookPath(debuginfodFind); err != nil { return "", err } cmd := exec.Command(debuginfodFind, args...) + if notify != nil { + cmd.Env = append(os.Environ(), "DEBUGINFOD_PROGRESS=yes") + stderr, err := cmd.StderrPipe() + if err != nil { + return "", err + } + s := bufio.NewScanner(stderr) + s.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.IndexAny(data, "\n\r"); i >= 0 { + return i + 1, dropCR(data[0:i]), nil + } + if atEOF { + return len(data), dropCR(data), nil + } + return 0, nil, nil + }) + go func() { + var tlast time.Time + for s.Scan() { + if time.Since(tlast) > notificationThrottle { + tlast = time.Now() + notify(string(s.Text())) + } + } + }() + } out, err := cmd.Output() // ignore stderr if err != nil { return "", err @@ -19,10 +53,15 @@ func execFind(args ...string) (string, error) { return strings.TrimSpace(string(out)), err } -func GetSource(buildid, filename string) (string, error) { - return execFind("source", buildid, filename) +func dropCR(data []byte) []byte { + r, _ := bytes.CutSuffix(data, []byte{'\r'}) + return r } -func GetDebuginfo(buildid string) (string, error) { - return execFind("debuginfo", buildid) +func GetSource(buildid, filename string) (string, error) { + return execFind(nil, "source", buildid, filename) +} + +func GetDebuginfo(notify func(string), buildid string) (string, error) { + return execFind(notify, "debuginfo", buildid) } diff --git a/pkg/proc/target_group.go b/pkg/proc/target_group.go index 8ebec98a..ca11bbdd 100644 --- a/pkg/proc/target_group.go +++ b/pkg/proc/target_group.go @@ -537,6 +537,12 @@ func (grp *TargetGroup) FollowExecEnabled() bool { return grp.followExecEnabled } +// SetEventsFn sets a function that is called to communicate events +// happening while the target process is running. +func (grp *TargetGroup) SetEventsFn(eventsFn func(*Event)) { + grp.Selected.BinInfo().eventsFn = eventsFn +} + // ValidTargets iterates through all valid targets in Group. type ValidTargets struct { *Target @@ -564,3 +570,22 @@ func (it *ValidTargets) Reset() { it.Target = nil it.start = 0 } + +// Event is an event that happened during execution of the debugged program. +type Event struct { + Kind EventKind + *BinaryInfoDownloadEventDetails +} + +type EventKind uint8 + +const ( + EventResumed EventKind = iota + EventStopped + EventBinaryInfoDownload +) + +// BinaryInfoDownloadEventDetails details of a BinaryInfoDownload event. +type BinaryInfoDownloadEventDetails struct { + ImagePath, Progress string +} diff --git a/pkg/terminal/starbind/starlark_mapping.go b/pkg/terminal/starbind/starlark_mapping.go index 3c8d2a17..9b5c0b0e 100644 --- a/pkg/terminal/starbind/starlark_mapping.go +++ b/pkg/terminal/starbind/starlark_mapping.go @@ -271,7 +271,13 @@ func (env *Env) starlarkPredeclare() (starlark.StringDict, map[string]string) { } } if len(args) > 5 && args[5] != starlark.None { - err := unmarshalStarlarkValue(args[5], &rpcArgs.UnsafeCall, "UnsafeCall") + err := unmarshalStarlarkValue(args[5], &rpcArgs.WithEvents, "WithEvents") + if err != nil { + return starlark.None, decorateError(thread, err) + } + } + if len(args) > 6 && args[6] != starlark.None { + err := unmarshalStarlarkValue(args[6], &rpcArgs.UnsafeCall, "UnsafeCall") if err != nil { return starlark.None, decorateError(thread, err) } @@ -289,6 +295,8 @@ func (env *Env) starlarkPredeclare() (starlark.StringDict, map[string]string) { err = unmarshalStarlarkValue(kv[1], &rpcArgs.ReturnInfoLoadConfig, "ReturnInfoLoadConfig") case "Expr": err = unmarshalStarlarkValue(kv[1], &rpcArgs.Expr, "Expr") + case "WithEvents": + err = unmarshalStarlarkValue(kv[1], &rpcArgs.WithEvents, "WithEvents") case "UnsafeCall": err = unmarshalStarlarkValue(kv[1], &rpcArgs.UnsafeCall, "UnsafeCall") default: @@ -304,7 +312,7 @@ func (env *Env) starlarkPredeclare() (starlark.StringDict, map[string]string) { } return env.interfaceToStarlarkValue(&rpcRet), nil }) - doc["raw_command"] = "builtin raw_command(Name, ThreadID, GoroutineID, ReturnInfoLoadConfig, Expr, UnsafeCall)\n\nraw_command interrupts, continues and steps through the program." + doc["raw_command"] = "builtin raw_command(Name, ThreadID, GoroutineID, ReturnInfoLoadConfig, Expr, WithEvents, UnsafeCall)\n\nraw_command interrupts, continues and steps through the program." r["create_breakpoint"] = starlark.NewBuiltin("create_breakpoint", func(thread *starlark.Thread, _ *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { if err := isCancelled(thread); err != nil { return starlark.None, decorateError(thread, err) diff --git a/pkg/terminal/terminal.go b/pkg/terminal/terminal.go index c7e705b0..d122e3d6 100644 --- a/pkg/terminal/terminal.go +++ b/pkg/terminal/terminal.go @@ -122,6 +122,23 @@ func New(client service.Client, conf *config.Config) *Term { if state, err := client.GetState(); err == nil { t.oldPid = state.Pid } + firstEventBinaryInfoDownload := true + client.SetEventsFn(func(event *api.Event) { + switch event.Kind { + case api.EventResumed: + firstEventBinaryInfoDownload = true + case api.EventBinaryInfoDownload: + if !firstEventBinaryInfoDownload { + fmt.Fprintf(t.stdout, "\r") + } + fmt.Fprintf(t.stdout, "Downloading debug info for %s: %s", event.BinaryInfoDownloadEventDetails.ImagePath, event.BinaryInfoDownloadEventDetails.Progress) + firstEventBinaryInfoDownload = false + case api.EventStopped: + if !firstEventBinaryInfoDownload { + fmt.Fprintf(t.stdout, "\n") + } + } + }) } t.starlarkEnv = starbind.New(starlarkContext{t}, t.stdout) diff --git a/service/api/conversions.go b/service/api/conversions.go index 14bf025a..20ff98bb 100644 --- a/service/api/conversions.go +++ b/service/api/conversions.go @@ -462,3 +462,16 @@ func ConvertTarget(tgt *proc.Target, convertThreadBreakpoint func(proc.Thread) * CurrentThread: ConvertThread(tgt.CurrentThread(), convertThreadBreakpoint(tgt.CurrentThread())), } } + +func ConvertEvent(event *proc.Event) *Event { + r := &Event{Kind: EventKind(event.Kind)} + + if event.BinaryInfoDownloadEventDetails != nil { + r.BinaryInfoDownloadEventDetails = &BinaryInfoDownloadEventDetails{ + ImagePath: event.BinaryInfoDownloadEventDetails.ImagePath, + Progress: event.BinaryInfoDownloadEventDetails.Progress, + } + } + + return r +} diff --git a/service/api/types.go b/service/api/types.go index 2ddde601..f26b3c3b 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -411,6 +411,10 @@ type DebuggerCommand struct { // Expr is the expression argument for a Call command Expr string `json:"expr,omitempty"` + // If WithEvents is set events are generated that should be read by calling + // GetEvents. + WithEvents bool + // UnsafeCall disables parameter escape checking for function calls. // Go objects can be allocated on the stack or on the heap. Heap objects // can be used by any goroutine; stack objects can only be used by the @@ -684,3 +688,22 @@ type GuessSubstitutePathIn struct { ClientGOROOT string ClientModuleDirectories map[string]string } + +// Event is an event that happened during execution of the debugged program. +type Event struct { + Kind EventKind + *BinaryInfoDownloadEventDetails +} + +type EventKind uint8 + +const ( + EventResumed EventKind = iota + EventStopped + EventBinaryInfoDownload +) + +// BinaryInfoDownloadEventDetails describes the details of a BinaryInfoDownloadEvent +type BinaryInfoDownloadEventDetails struct { + ImagePath, Progress string +} diff --git a/service/client.go b/service/client.go index b667e6ae..46111aa7 100644 --- a/service/client.go +++ b/service/client.go @@ -166,6 +166,9 @@ type Client interface { // SetReturnValuesLoadConfig sets the load configuration for return values. SetReturnValuesLoadConfig(*api.LoadConfig) + // SetEventsFn sets a function that will be called whenever a debugger event is received. + SetEventsFn(func(*api.Event)) + // IsMulticlient returns true if the headless instance is multiclient. IsMulticlient() bool diff --git a/service/dap/server.go b/service/dap/server.go index a890c552..8cf0c833 100644 --- a/service/dap/server.go +++ b/service/dap/server.go @@ -1398,7 +1398,7 @@ func (s *Session) halt() (*api.DebuggerState, error) { s.config.log.Debug("halting") // Only send a halt request if the debuggee is running. if s.debugger.IsRunning() { - return s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil, nil) + return s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil, nil, nil) } s.config.log.Debug("process not running") return s.debugger.State(false) @@ -2102,7 +2102,7 @@ func (s *Session) stoppedOnBreakpointGoroutineID(state *api.DebuggerState) (int6 // due to an error, so the server is ready to receive new requests. func (s *Session) stepUntilStopAndNotify(command string, threadId int, granularity dap.SteppingGranularity, allowNextStateChange *syncflag) { defer allowNextStateChange.raise() - _, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: int64(threadId)}, nil, s.conn.closedChan) + _, err := s.debugger.Command(&api.DebuggerCommand{Name: api.SwitchGoroutine, GoroutineID: int64(threadId)}, nil, s.conn.closedChan, nil) if err != nil { s.config.log.Errorf("Error switching goroutines while stepping: %v", err) // If we encounter an error, we will have to send a stopped event @@ -2972,7 +2972,8 @@ func (s *Session) doCall(goid, frame int, expr string) (*api.DebuggerState, []*p Expr: expr, UnsafeCall: false, GoroutineID: int64(goid), - }, nil, s.conn.closedChan) + WithEvents: true, + }, nil, s.conn.closedChan, s.convertDebuggerEvent) if processExited(state, err) { s.preTerminatedWG.Wait() e := &dap.TerminatedEvent{Event: *newEvent("terminated")} @@ -3775,7 +3776,7 @@ func (s *Session) resumeOnce(command string, allowNextStateChange *syncflag) (bo state, err := s.debugger.State(false) return false, state, err } - state, err := s.debugger.Command(&api.DebuggerCommand{Name: command}, asyncSetupDone, s.conn.closedChan) + state, err := s.debugger.Command(&api.DebuggerCommand{Name: command, WithEvents: true}, asyncSetupDone, s.conn.closedChan, s.convertDebuggerEvent) return true, state, err } @@ -4046,6 +4047,19 @@ func (s *Session) toServerPath(path string) string { return serverPath } +func (s *Session) convertDebuggerEvent(event *proc.Event) { + switch event.Kind { + case proc.EventBinaryInfoDownload: + s.send(&dap.OutputEvent{ + Event: *newEvent("output"), + Body: dap.OutputEventBody{ + Output: fmt.Sprintf("Download debug info for %s: %s\n", event.BinaryInfoDownloadEventDetails.ImagePath, event.BinaryInfoDownloadEventDetails.Progress), + Category: "console", + }, + }) + } +} + type logMessage struct { format string args []string diff --git a/service/dap/server_test.go b/service/dap/server_test.go index e299a0f4..5fda51c5 100644 --- a/service/dap/server_test.go +++ b/service/dap/server_test.go @@ -6819,7 +6819,7 @@ func launchDebuggerWithTargetRunning(t *testing.T, fixture string) (*protest.Fix var err error go func() { t.Helper() - _, err = dbg.Command(&api.DebuggerCommand{Name: api.Continue}, running, nil) + _, err = dbg.Command(&api.DebuggerCommand{Name: api.Continue}, running, nil, nil) select { case <-running: default: @@ -7021,7 +7021,7 @@ func (s *MultiClientCloseServerMock) stop(t *testing.T) { // they are part of dap.Session. // We must take it down manually as if we are in rpccommon::ServerImpl::Stop. if s.debugger.IsRunning() { - s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil, nil) + s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil, nil, nil) } s.debugger.Detach(true) } diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index c5ce026f..39f02378 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -1059,7 +1059,7 @@ func (d *Debugger) IsRunning() bool { } // Command handles commands which control the debugger lifecycle -func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struct{}, clientStatusCh chan struct{}) (state *api.DebuggerState, err error) { +func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struct{}, clientStatusCh chan struct{}, eventsFn func(*proc.Event)) (state *api.DebuggerState, err error) { if command.Name == api.Halt { // RequestManualStop does not invoke any ptrace syscalls, so it's safe to // access the process directly. @@ -1085,8 +1085,16 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc d.setRunning(true) defer d.setRunning(false) + d.target.SetEventsFn(nil) if command.Name != api.SwitchGoroutine && command.Name != api.SwitchThread && command.Name != api.Halt { d.target.ResumeNotify(resumeNotify) + + if eventsFn != nil { + eventsFn(&proc.Event{Kind: proc.EventResumed}) + defer eventsFn(&proc.Event{Kind: proc.EventStopped}) + } + + d.target.SetEventsFn(eventsFn) } else if resumeNotify != nil { close(resumeNotify) } diff --git a/service/rpc2/client.go b/service/rpc2/client.go index 87040c8b..e5f330bc 100644 --- a/service/rpc2/client.go +++ b/service/rpc2/client.go @@ -22,6 +22,7 @@ type RPCClient struct { client *rpc.Client retValLoadCfg *api.LoadConfig + eventsFn func(*api.Event) } // Ensure the implementation satisfies the interface. @@ -112,7 +113,7 @@ func (c *RPCClient) continueDir(cmd string) <-chan *api.DebuggerState { go func() { for { out := new(CommandOut) - err := c.call("Command", &api.DebuggerCommand{Name: cmd, ReturnInfoLoadConfig: c.retValLoadCfg}, &out) + err := c.callWhileDrainingEvents("Command", &api.DebuggerCommand{Name: cmd, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out) state := out.State if err != nil { state.Err = err @@ -146,45 +147,70 @@ func (c *RPCClient) continueDir(cmd string) <-chan *api.DebuggerState { return ch } +func (c *RPCClient) drainEvents() <-chan struct{} { + done := make(chan struct{}) + if c.eventsFn == nil { + close(done) + return done + } + go func() { + defer close(done) + for { + out := new(GetEventsOut) + err := c.call("GetEvents", &GetEventsIn{}, &out) + if err != nil { + break + } + for _, event := range out.Events { + c.eventsFn(&event) + if event.Kind == api.EventStopped { + return + } + } + } + }() + return done +} + func (c *RPCClient) Next() (*api.DebuggerState, error) { var out CommandOut - err := c.call("Command", api.DebuggerCommand{Name: api.Next, ReturnInfoLoadConfig: c.retValLoadCfg}, &out) + err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.Next, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out) return &out.State, err } func (c *RPCClient) ReverseNext() (*api.DebuggerState, error) { var out CommandOut - err := c.call("Command", api.DebuggerCommand{Name: api.ReverseNext, ReturnInfoLoadConfig: c.retValLoadCfg}, &out) + err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.ReverseNext, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out) return &out.State, err } func (c *RPCClient) Step() (*api.DebuggerState, error) { var out CommandOut - err := c.call("Command", api.DebuggerCommand{Name: api.Step, ReturnInfoLoadConfig: c.retValLoadCfg}, &out) + err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.Step, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out) return &out.State, err } func (c *RPCClient) ReverseStep() (*api.DebuggerState, error) { var out CommandOut - err := c.call("Command", api.DebuggerCommand{Name: api.ReverseStep, ReturnInfoLoadConfig: c.retValLoadCfg}, &out) + err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.ReverseStep, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out) return &out.State, err } func (c *RPCClient) StepOut() (*api.DebuggerState, error) { var out CommandOut - err := c.call("Command", api.DebuggerCommand{Name: api.StepOut, ReturnInfoLoadConfig: c.retValLoadCfg}, &out) + err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.StepOut, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out) return &out.State, err } func (c *RPCClient) ReverseStepOut() (*api.DebuggerState, error) { var out CommandOut - err := c.call("Command", api.DebuggerCommand{Name: api.ReverseStepOut, ReturnInfoLoadConfig: c.retValLoadCfg}, &out) + err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.ReverseStepOut, ReturnInfoLoadConfig: c.retValLoadCfg, WithEvents: c.eventsFn != nil}, &out) return &out.State, err } func (c *RPCClient) Call(goroutineID int64, expr string, unsafe bool) (*api.DebuggerState, error) { var out CommandOut - err := c.call("Command", api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe, GoroutineID: goroutineID}, &out) + err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: api.Call, ReturnInfoLoadConfig: c.retValLoadCfg, Expr: expr, UnsafeCall: unsafe, GoroutineID: goroutineID, WithEvents: c.eventsFn != nil}, &out) return &out.State, err } @@ -194,7 +220,7 @@ func (c *RPCClient) StepInstruction(skipCalls bool) (*api.DebuggerState, error) if skipCalls { name = api.NextInstruction } - err := c.call("Command", api.DebuggerCommand{Name: name}, &out) + err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: name, WithEvents: c.eventsFn != nil}, &out) return &out.State, err } @@ -204,7 +230,7 @@ func (c *RPCClient) ReverseStepInstruction(skipCalls bool) (*api.DebuggerState, if skipCalls { name = api.ReverseNextInstruction } - err := c.call("Command", api.DebuggerCommand{Name: name}, &out) + err := c.callWhileDrainingEvents("Command", api.DebuggerCommand{Name: name, WithEvents: c.eventsFn != nil}, &out) return &out.State, err } @@ -492,6 +518,10 @@ func (c *RPCClient) SetReturnValuesLoadConfig(cfg *api.LoadConfig) { c.retValLoadCfg = cfg } +func (c *RPCClient) SetEventsFn(eventsFn func(*api.Event)) { + c.eventsFn = eventsFn +} + func (c *RPCClient) FunctionReturnLocations(fnName string) ([]uint64, error) { var out FunctionReturnLocationsOut err := c.call("FunctionReturnLocations", FunctionReturnLocationsIn{fnName}, &out) @@ -667,6 +697,13 @@ func (c *RPCClient) call(method string, args, reply interface{}) error { return c.client.Call("RPCServer."+method, args, reply) } +func (c *RPCClient) callWhileDrainingEvents(method string, args, reply interface{}) error { + done := c.drainEvents() + err := c.call(method, args, reply) + <-done + return err +} + func (c *RPCClient) CallAPI(method string, args, reply interface{}) error { return c.call(method, args, reply) } diff --git a/service/rpc2/server.go b/service/rpc2/server.go index c636a7c0..32758306 100644 --- a/service/rpc2/server.go +++ b/service/rpc2/server.go @@ -18,11 +18,14 @@ type RPCServer struct { // config is all the information necessary to start the debugger and server. config *service.Config // debugger is a debugger service. - debugger *debugger.Debugger + debugger *debugger.Debugger + eventsChan chan *proc.Event } +const eventBufferSize = 100 + func NewServer(config *service.Config, debugger *debugger.Debugger) *RPCServer { - return &RPCServer{config, debugger} + return &RPCServer{config, debugger, make(chan *proc.Event, eventBufferSize)} } type ProcessPidIn struct { @@ -127,7 +130,11 @@ type CommandOut struct { // Command interrupts, continues and steps through the program. func (s *RPCServer) Command(command api.DebuggerCommand, cb service.RPCCallback) { - st, err := s.debugger.Command(&command, cb.SetupDoneChan(), cb.DisconnectChan()) + eventsFn := s.eventsFn + if !command.WithEvents { + eventsFn = nil + } + st, err := s.debugger.Command(&command, cb.SetupDoneChan(), cb.DisconnectChan(), eventsFn) if err != nil { cb.Return(nil, err) return @@ -137,6 +144,10 @@ func (s *RPCServer) Command(command api.DebuggerCommand, cb service.RPCCallback) cb.Return(out, nil) } +func (s *RPCServer) eventsFn(event *proc.Event) { + s.eventsChan <- event +} + type GetBufferedTracepointsIn struct { } @@ -1160,3 +1171,26 @@ func (s *RPCServer) GuessSubstitutePath(arg GuessSubstitutePathIn, out *GuessSub } return nil } + +type GetEventsIn struct { +} + +type GetEventsOut struct { + Events []api.Event +} + +func (s *RPCServer) GetEvents(arg GetEventsIn, cb service.RPCCallback) { + close(cb.SetupDoneChan()) + out := new(GetEventsOut) + out.Events = append(out.Events, *api.ConvertEvent(<-s.eventsChan)) + for len(out.Events) < eventBufferSize { + select { + case event := <-s.eventsChan: + out.Events = append(out.Events, *api.ConvertEvent(event)) + default: + cb.Return(out, nil) + return + } + } + cb.Return(out, nil) +} diff --git a/service/rpccommon/server.go b/service/rpccommon/server.go index 6965cb62..07a5f538 100644 --- a/service/rpccommon/server.go +++ b/service/rpccommon/server.go @@ -95,7 +95,7 @@ func (s *ServerImpl) Stop() error { s.listener.Close() } if s.debugger.IsRunning() { - s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil, nil) + s.debugger.Command(&api.DebuggerCommand{Name: api.Halt}, nil, nil, nil) } kill := s.config.Debugger.AttachPid == 0 return s.debugger.Detach(kill) diff --git a/service/rpccommon/suitablemethods.go b/service/rpccommon/suitablemethods.go index 0b4e8053..2e3bf0ca 100644 --- a/service/rpccommon/suitablemethods.go +++ b/service/rpccommon/suitablemethods.go @@ -34,6 +34,7 @@ func suitableMethods2(s *rpc2.RPCServer, methods map[string]*methodType) { methods["RPCServer.FunctionReturnLocations"] = &methodType{method: reflect.ValueOf(s.FunctionReturnLocations)} methods["RPCServer.GetBreakpoint"] = &methodType{method: reflect.ValueOf(s.GetBreakpoint)} methods["RPCServer.GetBufferedTracepoints"] = &methodType{method: reflect.ValueOf(s.GetBufferedTracepoints)} + methods["RPCServer.GetEvents"] = &methodType{method: reflect.ValueOf(s.GetEvents)} methods["RPCServer.GetThread"] = &methodType{method: reflect.ValueOf(s.GetThread)} methods["RPCServer.GuessSubstitutePath"] = &methodType{method: reflect.ValueOf(s.GuessSubstitutePath)} methods["RPCServer.IsMulticlient"] = &methodType{method: reflect.ValueOf(s.IsMulticlient)}