Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

service/debugger,terminal: API and user interface for follow exec mode #3286

Merged
merged 1 commit into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Documentation/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ Command | Description
[list](#list) | Show source code.
[source](#source) | Executes a file containing a list of delve commands
[sources](#sources) | Print list of source files.
[target](#target) | Manages child process debugging.
[transcript](#transcript) | Appends command output to a file.
[types](#types) | Print list of types

Expand Down Expand Up @@ -623,6 +624,22 @@ Step out of the current function.

Aliases: so

## target
Manages child process debugging.

target follow-exec [-on [regex]] [-off]

Enables or disables follow exec mode. When follow exec mode Delve will automatically attach to new child processes executed by the target process. An optional regular expression can be passed to 'target follow-exec', only child processes with a command line matching the regular expression will be followed.

target list

List currently attached processes.

target switch [pid]

Switches to the specified process.


## thread
Switch to the specified thread.

Expand Down
3 changes: 3 additions & 0 deletions Documentation/cli/starlark.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ dump_wait(Wait) | Equivalent to API call [DumpWait](https://godoc.org/github.com
eval(Scope, Expr, Cfg) | Equivalent to API call [Eval](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.Eval)
examine_memory(Address, Length) | Equivalent to API call [ExamineMemory](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ExamineMemory)
find_location(Scope, Loc, IncludeNonExecutableLines, SubstitutePathRules) | Equivalent to API call [FindLocation](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.FindLocation)
follow_exec(Enable, Regex) | Equivalent to API call [FollowExec](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.FollowExec)
follow_exec_enabled() | Equivalent to API call [FollowExecEnabled](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.FollowExecEnabled)
function_return_locations(FnName) | Equivalent to API call [FunctionReturnLocations](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.FunctionReturnLocations)
get_breakpoint(Id, Name) | Equivalent to API call [GetBreakpoint](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.GetBreakpoint)
get_buffered_tracepoints() | Equivalent to API call [GetBufferedTracepoints](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.GetBufferedTracepoints)
Expand All @@ -54,6 +56,7 @@ package_vars(Filter, Cfg) | Equivalent to API call [ListPackageVars](https://god
packages_build_info(IncludeFiles) | Equivalent to API call [ListPackagesBuildInfo](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListPackagesBuildInfo)
registers(ThreadID, IncludeFp, Scope) | Equivalent to API call [ListRegisters](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListRegisters)
sources(Filter) | Equivalent to API call [ListSources](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListSources)
targets() | Equivalent to API call [ListTargets](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListTargets)
threads() | Equivalent to API call [ListThreads](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListThreads)
types(Filter) | Equivalent to API call [ListTypes](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ListTypes)
process_pid() | Equivalent to API call [ProcessPid](https://godoc.org/github.com/go-delve/delve/service/rpc2#RPCServer.ProcessPid)
Expand Down
5 changes: 5 additions & 0 deletions pkg/proc/target_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,11 @@ func (grp *TargetGroup) FollowExec(v bool, regex string) error {
return nil
}

// FollowExecEnabled returns true if follow exec is enabled
func (grp *TargetGroup) FollowExecEnabled() bool {
return grp.followExecEnabled
}

// ValidTargets iterates through all valid targets in Group.
type ValidTargets struct {
*Target
Expand Down
88 changes: 87 additions & 1 deletion pkg/terminal/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,20 @@ The core dump is always written in ELF, even on systems (windows, macOS) where t
Output of Delve's command is appended to the specified output file. If '-t' is specified and the output file exists it is truncated. If '-x' is specified output to stdout is suppressed instead.

Using the -off option disables the transcript.`},

{aliases: []string{"target"}, cmdFn: target, helpMsg: `Manages child process debugging.

target follow-exec [-on [regex]] [-off]

Enables or disables follow exec mode. When follow exec mode Delve will automatically attach to new child processes executed by the target process. An optional regular expression can be passed to 'target follow-exec', only child processes with a command line matching the regular expression will be followed.

target list

List currently attached processes.

target switch [pid]

Switches to the specified process.`},
}

addrecorded := client == nil
Expand Down Expand Up @@ -1212,6 +1226,7 @@ func parseOptionalCount(arg string) (int64, error) {
}

func restartLive(t *Term, ctx callContext, args string) error {
t.oldPid = 0
resetArgs, newArgv, newRedirects, err := parseNewArgv(args)
if err != nil {
return err
Expand Down Expand Up @@ -2522,6 +2537,12 @@ func printcontext(t *Term, state *api.DebuggerState) {
return
}

if state.Pid != t.oldPid {
if t.oldPid != 0 {
fmt.Fprintf(t.stdout, "Switch target process from %d to %d\n", t.oldPid, state.Pid)
}
t.oldPid = state.Pid
}
for i := range state.Threads {
if (state.CurrentThread != nil) && (state.Threads[i].ID == state.CurrentThread.ID) {
continue
Expand Down Expand Up @@ -3177,6 +3198,71 @@ func transcript(t *Term, ctx callContext, args string) error {
return nil
}

func target(t *Term, ctx callContext, args string) error {
argv := config.Split2PartsBySpace(args)
switch argv[0] {
case "list":
tgts, err := t.client.ListTargets()
if err != nil {
return err
}
w := new(tabwriter.Writer)
w.Init(t.stdout, 4, 4, 2, ' ', 0)
for _, tgt := range tgts {
selected := ""
if tgt.Pid == t.oldPid {
selected = "*"
}
fmt.Fprintf(w, "%s\t%d\t%s\n", selected, tgt.Pid, tgt.CmdLine)
}
w.Flush()
return nil
case "follow-exec":
if len(argv) == 1 {
return errors.New("not enough arguments")
}
argv = config.Split2PartsBySpace(argv[1])
switch argv[0] {
case "-on":
var regex string
if len(argv) == 2 {
regex = argv[1]
}
t.client.FollowExec(true, regex)
case "-off":
if len(argv) > 1 {
return errors.New("too many arguments")
}
t.client.FollowExec(false, "")
default:
return fmt.Errorf("unknown argument %q to 'target follow-exec'", argv[0])
}
return nil
case "switch":
tgts, err := t.client.ListTargets()
if err != nil {
return err
}
pid, err := strconv.Atoi(argv[1])
if err != nil {
return err
}
found := false
for _, tgt := range tgts {
if tgt.Pid == pid {
found = true
t.client.SwitchThread(tgt.CurrentThread.ID)
}
}
if !found {
return fmt.Errorf("could not find target %d", pid)
}
return nil
default:
return fmt.Errorf("unknown command 'target %s'", argv[0])
}
}

func formatBreakpointName(bp *api.Breakpoint, upcase bool) string {
thing := "breakpoint"
if bp.Tracepoint {
Expand Down Expand Up @@ -3226,5 +3312,5 @@ func (t *Term) formatBreakpointLocation(bp *api.Breakpoint) string {
func shouldAskToSuspendBreakpoint(t *Term) bool {
fns, _ := t.client.ListFunctions(`^plugin\.Open$`)
_, err := t.client.GetState()
return len(fns) > 0 || isErrProcessExited(err)
return len(fns) > 0 || isErrProcessExited(err) || t.client.FollowExecEnabled()
}
62 changes: 62 additions & 0 deletions pkg/terminal/starbind/starlark_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,56 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
r["follow_exec"] = starlark.NewBuiltin("follow_exec", 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)
}
var rpcArgs rpc2.FollowExecIn
var rpcRet rpc2.FollowExecOut
if len(args) > 0 && args[0] != starlark.None {
err := unmarshalStarlarkValue(args[0], &rpcArgs.Enable, "Enable")
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
if len(args) > 1 && args[1] != starlark.None {
err := unmarshalStarlarkValue(args[1], &rpcArgs.Regex, "Regex")
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
for _, kv := range kwargs {
var err error
switch kv[0].(starlark.String) {
case "Enable":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Enable, "Enable")
case "Regex":
err = unmarshalStarlarkValue(kv[1], &rpcArgs.Regex, "Regex")
default:
err = fmt.Errorf("unknown argument %q", kv[0])
}
if err != nil {
return starlark.None, decorateError(thread, err)
}
}
err := env.ctx.Client().CallAPI("FollowExec", &rpcArgs, &rpcRet)
if err != nil {
return starlark.None, err
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
r["follow_exec_enabled"] = starlark.NewBuiltin("follow_exec_enabled", 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)
}
var rpcArgs rpc2.FollowExecEnabledIn
var rpcRet rpc2.FollowExecEnabledOut
err := env.ctx.Client().CallAPI("FollowExecEnabled", &rpcArgs, &rpcRet)
if err != nil {
return starlark.None, err
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
r["function_return_locations"] = starlark.NewBuiltin("function_return_locations", 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)
Expand Down Expand Up @@ -1235,6 +1285,18 @@ func (env *Env) starlarkPredeclare() starlark.StringDict {
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
r["targets"] = starlark.NewBuiltin("targets", 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)
}
var rpcArgs rpc2.ListTargetsIn
var rpcRet rpc2.ListTargetsOut
err := env.ctx.Client().CallAPI("ListTargets", &rpcArgs, &rpcRet)
if err != nil {
return starlark.None, err
}
return env.interfaceToStarlarkValue(rpcRet), nil
})
r["threads"] = starlark.NewBuiltin("threads", 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)
Expand Down
4 changes: 4 additions & 0 deletions pkg/terminal/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type Term struct {
stdout *transcriptWriter
InitFile string
displays []displayEntry
oldPid int

historyFile *os.File

Expand Down Expand Up @@ -115,6 +116,9 @@ func New(client service.Client, conf *config.Config) *Term {
if client != nil {
lcfg := t.loadConfig()
client.SetReturnValuesLoadConfig(&lcfg)
if state, err := client.GetState(); err == nil {
t.oldPid = state.Pid
}
}

t.starlarkEnv = starbind.New(starlarkContext{t}, t.stdout)
Expand Down
11 changes: 11 additions & 0 deletions service/api/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,10 +429,12 @@ func ConvertRegisters(in *op.DwarfRegisters, dwarfRegisterToString func(int, *op
return
}

// ConvertImage convers proc.Image to api.Image.
func ConvertImage(image *proc.Image) Image {
return Image{Path: image.Path, Address: image.StaticBase}
}

// ConvertDumpState converts proc.DumpState to api.DumpState.
func ConvertDumpState(dumpState *proc.DumpState) *DumpState {
dumpState.Mutex.Lock()
defer dumpState.Mutex.Unlock()
Expand All @@ -449,3 +451,12 @@ func ConvertDumpState(dumpState *proc.DumpState) *DumpState {
}
return r
}

// ConvertTarget converts a proc.Target into a api.Target.
func ConvertTarget(tgt *proc.Target, convertThreadBreakpoint func(proc.Thread) *Breakpoint) *Target {
//TODO(aarzilli): copy command line here
return &Target{
Pid: tgt.Pid(),
CurrentThread: ConvertThread(tgt.CurrentThread(), convertThreadBreakpoint(tgt.CurrentThread())),
}
}
7 changes: 7 additions & 0 deletions service/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,3 +655,10 @@ type GoroutineGroupingOptions struct {
MaxGroupMembers int
MaxGroups int
}

// Target represents a debugging target.
type Target struct {
Pid int
CmdLine string
CurrentThread *Thread
}
8 changes: 8 additions & 0 deletions service/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,14 @@ type Client interface {
// CoreDumpCancel cancels a core dump in progress
CoreDumpCancel() error

// ListTargets returns the list of connected targets
ListTargets() ([]api.Target, error)
// FollowExec enables or disables the follow exec mode. In follow exec mode
// Delve will automatically debug child processes launched by the target
// process
FollowExec(bool, string) error
FollowExecEnabled() bool

// Disconnect closes the connection to the server without sending a Detach request first.
// If cont is true a continue command will be sent instead.
Disconnect(cont bool) error
Expand Down
21 changes: 21 additions & 0 deletions service/debugger/debugger.go
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,13 @@ func (d *Debugger) Command(command *api.DebuggerCommand, resumeNotify chan struc
err = d.target.StepOut()
case api.SwitchThread:
d.log.Debugf("switching to thread %d", command.ThreadID)
t := proc.ValidTargets{Group: d.target}
for t.Next() {
if _, ok := t.FindThread(command.ThreadID); ok {
d.target.Selected = t.Target
break
}
}
err = d.target.Selected.SwitchThread(command.ThreadID)
withBreakpointInfo = false
case api.SwitchGoroutine:
Expand Down Expand Up @@ -2247,6 +2254,20 @@ func (d *Debugger) GetBufferedTracepoints() []api.TracepointResult {
return results
}

// FollowExec enabled or disables follow exec mode.
func (d *Debugger) FollowExec(enabled bool, regex string) error {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
return d.target.FollowExec(enabled, regex)
}

// FollowExecEnabled returns true if follow exec mode is enabled.
func (d *Debugger) FollowExecEnabled() bool {
d.targetMutex.Lock()
defer d.targetMutex.Unlock()
return d.target.FollowExecEnabled()
}

func go11DecodeErrorCheck(err error) error {
if _, isdecodeerr := err.(dwarf.DecodeError); !isdecodeerr {
return err
Expand Down
24 changes: 24 additions & 0 deletions service/rpc2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,30 @@ func (c *RPCClient) CoreDumpCancel() error {
return c.call("DumpCancel", DumpCancelIn{}, out)
}

// ListTargets returns the current list of debug targets.
func (c *RPCClient) ListTargets() ([]api.Target, error) {
out := &ListTargetsOut{}
err := c.call("ListTargets", ListTargetsIn{}, out)
return out.Targets, err
}

// FollowExec enabled or disabled follow exec mode. When follow exec is
// enabled Delve will automatically attach to new subprocesses with a
// command line matched by regex, if regex is nil all new subprocesses are
// automatically debugged.
func (c *RPCClient) FollowExec(v bool, regex string) error {
out := &FollowExecOut{}
err := c.call("FollowExec", FollowExecIn{Enable: v, Regex: regex}, out)
return err
}

// FollowExecEnabled returns true if follow exex mode is enabled.
func (c *RPCClient) FollowExecEnabled() bool {
out := &FollowExecEnabledOut{}
_ = c.call("FollowExecEnabled", FollowExecEnabledIn{}, out)
return out.Enabled
}

func (c *RPCClient) call(method string, args, reply interface{}) error {
return c.client.Call("RPCServer."+method, args, reply)
}
Expand Down
Loading