delve/pkg/proc/target_group.go
Alessandro Arzilli ec5fcc07fe
proc,service/debugger: track how breakpoints were originally set (#3148)
Adds field to breakpoint struct to track how a breakpoint was
originally set, moves the logic for disabling and enabling a breakpoint
to proc.
This will allow creating suspended breakpoints that are automatically
enabled when a plugin is loaded. When follow exec mode is implemented
it will also be possible to automatically enable breakpoints (whether
or not they were suspended) on new child processes, as they are
spawned.

It also improves breakpoint restore after a restart, before this after
a restart breakpoints would be re-enabled using their file:line
position, for breakpoints set using a function name or a location
expression this could be the wrong location after a recompile.

Updates #1653
Updates #2551
2022-09-28 11:35:07 -07:00

298 lines
7.4 KiB
Go

package proc
import (
"bytes"
"fmt"
"strings"
)
// TargetGroup reperesents 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 {
targets []*Target
Selected *Target
RecordingManipulation
recman RecordingManipulationInternal
// 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
continueOnce ContinueOnceFunc
cctx *ContinueOnceContext
}
// NewGroup creates a TargetGroup containing the specified Target.
func NewGroup(t *Target) *TargetGroup {
if t.partOfGroup {
panic("internal error: target is already part of a group")
}
t.partOfGroup = true
if t.Breakpoints().Logical == nil {
t.Breakpoints().Logical = make(map[int]*LogicalBreakpoint)
}
return &TargetGroup{
RecordingManipulation: t.recman,
targets: []*Target{t},
Selected: t,
cctx: &ContinueOnceContext{},
recman: t.recman,
LogicalBreakpoints: t.Breakpoints().Logical,
continueOnce: t.continueOnce,
}
}
// NewGroupRestart creates a new group of targets containing t and
// sets breakpoints and other attributes from oldgrp.
// Breakpoints that can not be set will be discarded, if discard is not nil
// it will be called for each discarded breakpoint.
func NewGroupRestart(t *Target, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) *TargetGroup {
grp := NewGroup(t)
grp.LogicalBreakpoints = oldgrp.LogicalBreakpoints
t.Breakpoints().Logical = grp.LogicalBreakpoints
for _, bp := range grp.LogicalBreakpoints {
if bp.LogicalID < 0 || !bp.Enabled {
continue
}
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
err := grp.EnableBreakpoint(bp)
if err != nil {
if discard != nil {
discard(bp, err)
}
delete(grp.LogicalBreakpoints, bp.LogicalID)
}
}
return grp
}
// 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
}
// Detach detaches all targets in the group.
func (grp *TargetGroup) Detach(kill bool) error {
var errs []string
for _, t := range grp.targets {
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(thread Thread) *Target {
for _, t := range grp.targets {
for _, th := range t.ThreadList() {
if th == thread {
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 lbp.Set.Expr != nil:
addrs = lbp.Set.Expr(p)
case len(lbp.Set.PidAddrs) > 0:
for _, pidAddr := range lbp.Set.PidAddrs {
if pidAddr.Pid == p.Pid() {
addrs = append(addrs, pidAddr.Addr)
}
}
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
}
// 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
}