delve/pkg/proc/target_group.go

555 lines
14 KiB
Go
Raw Normal View History

package proc
import (
"bytes"
"fmt"
"go/parser"
"go/token"
"regexp"
"strconv"
"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 {
bp.condSatisfiable = breakpointConditionSatisfiable(bp)
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()
if len(grp.targets) > 0 {
if !grp.followExecEnabled {
logger.Debugf("Detaching from child target (follow-exec disabled) %d %q", pid, cmdline)
return nil, nil
}
if grp.followExecRegex != nil && !grp.followExecRegex.MatchString(cmdline) {
logger.Debugf("Detaching from child target (follow-exec regex not matched) %d %q", pid, cmdline)
return nil, nil
}
}
t, err := grp.newTarget(p, pid, currentThread, path, cmdline)
if err != nil {
return nil, err
}
t.StopReason = stopReason
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 := grp.detachTarget(t, 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 grp.procgrp.Close()
}
// detachTarget will detach the target from the underlying process.
// This means the debugger will no longer receive events from the process
// we were previously debugging.
// If kill is true then the process will be killed when we detach.
func (grp *TargetGroup) detachTarget(t *Target, kill bool) error {
if !kill {
if t.asyncPreemptChanged {
setAsyncPreemptOff(t, t.asyncPreemptOff)
}
for _, bp := range t.Breakpoints().M {
if bp != nil {
err := t.ClearBreakpoint(bp.Addr)
if err != nil {
return err
}
}
}
}
t.StopReason = StopUnknown
return grp.procgrp.Detach(t.Pid(), kill)
}
// 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
}
// SetBreakpointEnabled either enables or disabled the specified breakpoint based on the value of enabled.
func (grp *TargetGroup) SetBreakpointEnabled(lbp *LogicalBreakpoint, enabled bool) (err error) {
switch {
case lbp.enabled && !enabled:
lbp.enabled = false
err = grp.disableBreakpoint(lbp)
case !lbp.enabled && enabled:
lbp.enabled = true
lbp.condSatisfiable = breakpointConditionSatisfiable(lbp)
err = grp.enableBreakpoint(lbp)
}
return
}
// 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
}
return nil
}
func enableBreakpointOnTarget(p *Target, lbp *LogicalBreakpoint) error {
if !lbp.enabled || !lbp.condSatisfiable {
return nil
}
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())
}
return nil
}
// ChangeBreakpointCondition changes the breakpoint condition of lbp.
func (grp *TargetGroup) ChangeBreakpointCondition(lbp *LogicalBreakpoint, cond, hitCond string, hitCondPerG bool) error {
lbp.cond = nil
if cond != "" {
var err error
lbp.cond, err = parser.ParseExpr(cond)
if err != nil {
return err
}
}
t := ValidTargets{Group: grp}
for t.Next() {
for _, bp := range t.Breakpoints().M {
if bp.LogicalID() == lbp.LogicalID {
bp.UserBreaklet().Cond = lbp.cond
}
}
}
lbp.hitCond = nil
if hitCond != "" {
opTok, val, err := parseHitCondition(hitCond)
if err != nil {
return err
}
lbp.hitCond = &struct {
Op token.Token
Val int
}{opTok, val}
lbp.HitCondPerG = hitCondPerG
}
if lbp.enabled {
switch {
case lbp.condSatisfiable && !breakpointConditionSatisfiable(lbp):
lbp.condSatisfiable = false
grp.disableBreakpoint(lbp)
case !lbp.condSatisfiable && breakpointConditionSatisfiable(lbp):
lbp.condSatisfiable = true
grp.enableBreakpoint(lbp)
}
}
return nil
}
func parseHitCondition(hitCond string) (token.Token, int, error) {
// A hit condition can be in the following formats:
// - "number"
// - "OP number"
hitConditionRegex := regexp.MustCompile(`(([=><%!])+|)( |)((\d|_)+)`)
match := hitConditionRegex.FindStringSubmatch(strings.TrimSpace(hitCond))
if match == nil || len(match) != 6 {
return 0, 0, fmt.Errorf("unable to parse breakpoint hit condition: %q\nhit conditions should be of the form \"number\" or \"OP number\"", hitCond)
}
opStr := match[1]
var opTok token.Token
switch opStr {
case "==", "":
opTok = token.EQL
case ">=":
opTok = token.GEQ
case "<=":
opTok = token.LEQ
case ">":
opTok = token.GTR
case "<":
opTok = token.LSS
case "%":
opTok = token.REM
case "!=":
opTok = token.NEQ
default:
return 0, 0, fmt.Errorf("unable to parse breakpoint hit condition: %q\ninvalid operator: %q", hitCond, opStr)
}
numStr := match[4]
val, parseErr := strconv.Atoi(numStr)
if parseErr != nil {
return 0, 0, fmt.Errorf("unable to parse breakpoint hit condition: %q\ninvalid number: %q", hitCond, numStr)
}
return opTok, val, nil
}
// manageUnsatisfiableBreakpoints automatically disables breakpoints with unsatisifiable hit conditions.
func (grp *TargetGroup) manageUnsatisfiableBreakpoints() error {
for _, lbp := range grp.LogicalBreakpoints {
if lbp.enabled {
if lbp.condSatisfiable && !breakpointConditionSatisfiable(lbp) {
lbp.condSatisfiable = false
err := grp.disableBreakpoint(lbp)
if err != nil {
return err
}
}
}
}
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
}