
Read the command line of the main target process as well as any other process Delve attaches to in follow exec mode. The command line can be viewed using the 'target list' command. In follow exec mode this command line is used to match the follow exec regex to decide whether or not to attach to a child process. On macOS or when using rr the list of arguments is not available for attached processes since there is no way to use the gdb serial protocol to read it. Fixes #2242
407 lines
10 KiB
Go
407 lines
10 KiB
Go
package proc
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/go-delve/delve/pkg/logflags"
|
|
)
|
|
|
|
// TargetGroup represents a group of target processes being debugged that
|
|
// will be resumed and stopped simultaneously.
|
|
// New targets are automatically added to the group if exec catching is
|
|
// enabled and the backend supports it, otherwise the group will always
|
|
// contain a single target process.
|
|
type TargetGroup struct {
|
|
procgrp ProcessGroup
|
|
|
|
targets []*Target
|
|
Selected *Target
|
|
followExecEnabled bool
|
|
followExecRegex *regexp.Regexp
|
|
|
|
RecordingManipulation
|
|
recman RecordingManipulationInternal
|
|
|
|
// StopReason describes the reason why the selected target process is stopped.
|
|
// A process could be stopped for multiple simultaneous reasons, in which
|
|
// case only one will be reported.
|
|
StopReason StopReason
|
|
|
|
// KeepSteppingBreakpoints determines whether certain stop reasons (e.g. manual halts)
|
|
// will keep the stepping breakpoints instead of clearing them.
|
|
KeepSteppingBreakpoints KeepSteppingBreakpoints
|
|
|
|
LogicalBreakpoints map[int]*LogicalBreakpoint
|
|
|
|
cctx *ContinueOnceContext
|
|
cfg NewTargetGroupConfig
|
|
CanDump bool
|
|
}
|
|
|
|
// NewTargetGroupConfig contains the configuration for a new TargetGroup object,
|
|
type NewTargetGroupConfig struct {
|
|
DebugInfoDirs []string // Directories to search for split debug info
|
|
DisableAsyncPreempt bool // Go 1.14 asynchronous preemption should be disabled
|
|
StopReason StopReason // Initial stop reason
|
|
CanDump bool // Can create core dumps (must implement ProcessInternal.MemoryMap)
|
|
}
|
|
|
|
type AddTargetFunc func(ProcessInternal, int, Thread, string, StopReason, string) (*Target, error)
|
|
|
|
// NewGroup creates a TargetGroup containing the specified Target.
|
|
func NewGroup(procgrp ProcessGroup, cfg NewTargetGroupConfig) (*TargetGroup, AddTargetFunc) {
|
|
grp := &TargetGroup{
|
|
procgrp: procgrp,
|
|
cctx: &ContinueOnceContext{},
|
|
LogicalBreakpoints: make(map[int]*LogicalBreakpoint),
|
|
StopReason: cfg.StopReason,
|
|
cfg: cfg,
|
|
CanDump: cfg.CanDump,
|
|
}
|
|
return grp, grp.addTarget
|
|
}
|
|
|
|
// Restart copies breakpoints and follow exec status from oldgrp into grp.
|
|
// Breakpoints that can not be set will be discarded, if discard is not nil
|
|
// it will be called for each discarded breakpoint.
|
|
func Restart(grp, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) {
|
|
for _, bp := range oldgrp.LogicalBreakpoints {
|
|
if _, ok := grp.LogicalBreakpoints[bp.LogicalID]; ok {
|
|
continue
|
|
}
|
|
grp.LogicalBreakpoints[bp.LogicalID] = bp
|
|
bp.TotalHitCount = 0
|
|
bp.HitCount = make(map[int64]uint64)
|
|
bp.Set.PidAddrs = nil // breakpoints set through a list of addresses can not be restored after a restart
|
|
if bp.Enabled {
|
|
err := grp.EnableBreakpoint(bp)
|
|
if err != nil {
|
|
if discard != nil {
|
|
discard(bp, err)
|
|
}
|
|
delete(grp.LogicalBreakpoints, bp.LogicalID)
|
|
}
|
|
}
|
|
}
|
|
if oldgrp.followExecEnabled {
|
|
rgx := ""
|
|
if oldgrp.followExecRegex != nil {
|
|
rgx = oldgrp.followExecRegex.String()
|
|
}
|
|
grp.FollowExec(true, rgx)
|
|
}
|
|
}
|
|
|
|
func (grp *TargetGroup) addTarget(p ProcessInternal, pid int, currentThread Thread, path string, stopReason StopReason, cmdline string) (*Target, error) {
|
|
logger := logflags.DebuggerLogger()
|
|
t, err := grp.newTarget(p, pid, currentThread, path, cmdline)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
t.StopReason = stopReason
|
|
if grp.followExecRegex != nil && len(grp.targets) > 0 {
|
|
if !grp.followExecRegex.MatchString(cmdline) {
|
|
logger.Debugf("Detaching from child target %d %q", t.Pid(), t.CmdLine)
|
|
t.detach(false)
|
|
return nil, nil
|
|
}
|
|
}
|
|
logger.Debugf("Adding target %d %q", t.Pid(), t.CmdLine)
|
|
if t.partOfGroup {
|
|
panic("internal error: target is already part of group")
|
|
}
|
|
t.partOfGroup = true
|
|
if grp.RecordingManipulation == nil {
|
|
grp.RecordingManipulation = t.recman
|
|
grp.recman = t.recman
|
|
}
|
|
if grp.Selected == nil {
|
|
grp.Selected = t
|
|
}
|
|
t.Breakpoints().Logical = grp.LogicalBreakpoints
|
|
for _, lbp := range grp.LogicalBreakpoints {
|
|
if lbp.LogicalID < 0 {
|
|
continue
|
|
}
|
|
err := enableBreakpointOnTarget(t, lbp)
|
|
if err != nil {
|
|
logger.Debugf("could not enable breakpoint %d on new target %d: %v", lbp.LogicalID, t.Pid(), err)
|
|
} else {
|
|
logger.Debugf("breakpoint %d enabled on new target %d: %v", lbp.LogicalID, t.Pid(), err)
|
|
}
|
|
}
|
|
grp.targets = append(grp.targets, t)
|
|
return t, nil
|
|
}
|
|
|
|
// Targets returns a slice of all targets in the group, including the
|
|
// ones that are no longer valid.
|
|
func (grp *TargetGroup) Targets() []*Target {
|
|
return grp.targets
|
|
}
|
|
|
|
// Valid returns true if any target in the target group is valid.
|
|
func (grp *TargetGroup) Valid() (bool, error) {
|
|
var err0 error
|
|
for _, t := range grp.targets {
|
|
ok, err := t.Valid()
|
|
if ok {
|
|
return true, nil
|
|
}
|
|
if err0 == nil {
|
|
err0 = err
|
|
}
|
|
}
|
|
return false, err0
|
|
}
|
|
|
|
func (grp *TargetGroup) numValid() int {
|
|
r := 0
|
|
for _, t := range grp.targets {
|
|
ok, _ := t.Valid()
|
|
if ok {
|
|
r++
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// Detach detaches all targets in the group.
|
|
func (grp *TargetGroup) Detach(kill bool) error {
|
|
var errs []string
|
|
for i := len(grp.targets) - 1; i >= 0; i-- {
|
|
t := grp.targets[i]
|
|
isvalid, _ := t.Valid()
|
|
if !isvalid {
|
|
continue
|
|
}
|
|
err := t.detach(kill)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Sprintf("could not detach process %d: %v", t.Pid(), err))
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("%s", strings.Join(errs, "\n"))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// HasSteppingBreakpoints returns true if any of the targets has stepping breakpoints set.
|
|
func (grp *TargetGroup) HasSteppingBreakpoints() bool {
|
|
for _, t := range grp.targets {
|
|
if t.Breakpoints().HasSteppingBreakpoints() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ClearSteppingBreakpoints removes all stepping breakpoints.
|
|
func (grp *TargetGroup) ClearSteppingBreakpoints() error {
|
|
for _, t := range grp.targets {
|
|
if t.Breakpoints().HasSteppingBreakpoints() {
|
|
return t.ClearSteppingBreakpoints()
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ThreadList returns a list of all threads in all target processes.
|
|
func (grp *TargetGroup) ThreadList() []Thread {
|
|
r := []Thread{}
|
|
for _, t := range grp.targets {
|
|
r = append(r, t.ThreadList()...)
|
|
}
|
|
return r
|
|
}
|
|
|
|
// TargetForThread returns the target containing the given thread.
|
|
func (grp *TargetGroup) TargetForThread(tid int) *Target {
|
|
for _, t := range grp.targets {
|
|
if _, ok := t.FindThread(tid); ok {
|
|
return t
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// EnableBreakpoint re-enables a disabled logical breakpoint.
|
|
func (grp *TargetGroup) EnableBreakpoint(lbp *LogicalBreakpoint) error {
|
|
var err0, errNotFound, errExists error
|
|
didSet := false
|
|
targetLoop:
|
|
for _, p := range grp.targets {
|
|
err := enableBreakpointOnTarget(p, lbp)
|
|
|
|
switch err.(type) {
|
|
case nil:
|
|
didSet = true
|
|
case *ErrFunctionNotFound, *ErrCouldNotFindLine:
|
|
errNotFound = err
|
|
case BreakpointExistsError:
|
|
errExists = err
|
|
default:
|
|
err0 = err
|
|
break targetLoop
|
|
}
|
|
}
|
|
if errNotFound != nil && !didSet {
|
|
return errNotFound
|
|
}
|
|
if errExists != nil && !didSet {
|
|
return errExists
|
|
}
|
|
if !didSet {
|
|
if _, err := grp.Valid(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err0 != nil {
|
|
it := ValidTargets{Group: grp}
|
|
for it.Next() {
|
|
for _, bp := range it.Breakpoints().M {
|
|
if bp.LogicalID() == lbp.LogicalID {
|
|
if err1 := it.ClearBreakpoint(bp.Addr); err1 != nil {
|
|
return fmt.Errorf("error while creating breakpoint: %v, additionally the breakpoint could not be properly rolled back: %v", err0, err1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return err0
|
|
}
|
|
lbp.Enabled = true
|
|
return nil
|
|
}
|
|
|
|
func enableBreakpointOnTarget(p *Target, lbp *LogicalBreakpoint) error {
|
|
var err error
|
|
var addrs []uint64
|
|
switch {
|
|
case lbp.Set.File != "":
|
|
addrs, err = FindFileLocation(p, lbp.Set.File, lbp.Set.Line)
|
|
case lbp.Set.FunctionName != "":
|
|
addrs, err = FindFunctionLocation(p, lbp.Set.FunctionName, lbp.Set.Line)
|
|
case len(lbp.Set.PidAddrs) > 0:
|
|
for _, pidAddr := range lbp.Set.PidAddrs {
|
|
if pidAddr.Pid == p.Pid() {
|
|
addrs = append(addrs, pidAddr.Addr)
|
|
}
|
|
}
|
|
case lbp.Set.Expr != nil:
|
|
addrs = lbp.Set.Expr(p)
|
|
default:
|
|
return fmt.Errorf("breakpoint %d can not be enabled", lbp.LogicalID)
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, addr := range addrs {
|
|
_, err = p.SetBreakpoint(lbp.LogicalID, addr, UserBreakpoint, nil)
|
|
if err != nil {
|
|
if _, isexists := err.(BreakpointExistsError); isexists {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// DisableBreakpoint disables a logical breakpoint.
|
|
func (grp *TargetGroup) DisableBreakpoint(lbp *LogicalBreakpoint) error {
|
|
var errs []error
|
|
n := 0
|
|
it := ValidTargets{Group: grp}
|
|
for it.Next() {
|
|
for _, bp := range it.Breakpoints().M {
|
|
if bp.LogicalID() == lbp.LogicalID {
|
|
n++
|
|
err := it.ClearBreakpoint(bp.Addr)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
buf := new(bytes.Buffer)
|
|
for i, err := range errs {
|
|
fmt.Fprintf(buf, "%s", err)
|
|
if i != len(errs)-1 {
|
|
fmt.Fprintf(buf, ", ")
|
|
}
|
|
}
|
|
|
|
if len(errs) == n {
|
|
return fmt.Errorf("unable to clear breakpoint %d: %v", lbp.LogicalID, buf.String())
|
|
}
|
|
return fmt.Errorf("unable to clear breakpoint %d (partial): %s", lbp.LogicalID, buf.String())
|
|
}
|
|
lbp.Enabled = false
|
|
return nil
|
|
}
|
|
|
|
// FollowExec enables or disables follow exec mode. When follow exec mode is
|
|
// enabled new processes spawned by the target process are automatically
|
|
// added to the target group.
|
|
// If regex is not the empty string only processes whose command line
|
|
// matches regex will be added to the target group.
|
|
func (grp *TargetGroup) FollowExec(v bool, regex string) error {
|
|
grp.followExecRegex = nil
|
|
if regex != "" && v {
|
|
var err error
|
|
grp.followExecRegex, err = regexp.Compile(regex)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
it := ValidTargets{Group: grp}
|
|
for it.Next() {
|
|
err := it.proc.FollowExec(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
grp.followExecEnabled = v
|
|
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
|
|
Group *TargetGroup
|
|
start int
|
|
}
|
|
|
|
// Next moves to the next valid target, returns false if there aren't more
|
|
// valid targets in the group.
|
|
func (it *ValidTargets) Next() bool {
|
|
for i := it.start; i < len(it.Group.targets); i++ {
|
|
if ok, _ := it.Group.targets[i].Valid(); ok {
|
|
it.Target = it.Group.targets[i]
|
|
it.start = i + 1
|
|
return true
|
|
}
|
|
}
|
|
it.start = len(it.Group.targets)
|
|
it.Target = nil
|
|
return false
|
|
}
|
|
|
|
// Reset returns the iterator to the start of the group.
|
|
func (it *ValidTargets) Reset() {
|
|
it.Target = nil
|
|
it.start = 0
|
|
}
|