proc: add waitfor option to attach (#3445)

Adds a waitfor option to 'dlv attach' that waits for a process with a
name starting with a given prefix to appear before attaching to it.

Debugserver on macOS does not support follow-fork mode, but has this
feature instead which is not the same thing but still helps with
multiprocess debugging somewhat.
This commit is contained in:
Alessandro Arzilli 2023-08-09 19:30:22 +02:00 committed by GitHub
parent 2b785f293b
commit dc5d8de320
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 389 additions and 56 deletions

@ -5,6 +5,8 @@ Tests skipped by each supported backend:
* 3 not implemented
* arm64 skipped = 1
* 1 broken - global variable symbolication
* darwin skipped = 1
* 1 waitfor implementation is delegated to debugserver
* darwin/arm64 skipped = 2
* 2 broken - cgo stacktraces
* darwin/lldb skipped = 1

@ -18,8 +18,11 @@ dlv attach pid [executable] [flags]
### Options
```
--continue Continue the debugged process on start.
-h, --help help for attach
--continue Continue the debugged process on start.
-h, --help help for attach
--waitfor string Wait for a process with a name beginning with this prefix
--waitfor-duration float Total time to wait for a process
--waitfor-interval float Interval between checks of the process list, in millisecond (default 1)
```
### Options inherited from parent commands

@ -16,7 +16,7 @@ uninstall:
@go run _scripts/make.go uninstall
test: vet
@go run _scripts/make.go test
@go run _scripts/make.go test -v
vet:
@go vet $$(go list ./... | grep -v native)

@ -96,6 +96,10 @@ var (
loadConfErr error
rrOnProcessPid int
attachWaitFor string
attachWaitForInterval float64
attachWaitForDuration float64
)
const dlvCommandLongDesc = `Delve is a source level debugger for Go programs.
@ -162,7 +166,7 @@ begin a new debug session. When exiting the debug session you will have the
option to let the process continue or kill it.
`,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
if len(args) == 0 && attachWaitFor == "" {
return errors.New("you must provide a PID")
}
return nil
@ -170,6 +174,9 @@ option to let the process continue or kill it.
Run: attachCmd,
}
attachCommand.Flags().BoolVar(&continueOnStart, "continue", false, "Continue the debugged process on start.")
attachCommand.Flags().StringVar(&attachWaitFor, "waitfor", "", "Wait for a process with a name beginning with this prefix")
attachCommand.Flags().Float64Var(&attachWaitForInterval, "waitfor-interval", 1, "Interval between checks of the process list, in millisecond")
attachCommand.Flags().Float64Var(&attachWaitForDuration, "waitfor-duration", 0, "Total time to wait for a process")
rootCommand.AddCommand(attachCommand)
// 'connect' subcommand.
@ -305,7 +312,8 @@ to know what functions your process is executing.
The output of the trace sub command is printed to stderr, so if you would like to
only see the output of the trace operations you can redirect stdout.`,
Run: func(cmd *cobra.Command, args []string) {
os.Exit(traceCmd(cmd, args, conf)) },
os.Exit(traceCmd(cmd, args, conf))
},
}
traceCommand.Flags().IntVarP(&traceAttachPid, "pid", "p", 0, "Pid to attach to.")
traceCommand.Flags().StringVarP(&traceExecFile, "exec", "e", "", "Binary file to exec and trace.")
@ -647,10 +655,10 @@ func traceCmd(cmd *cobra.Command, args []string, conf *config.Config) int {
ProcessArgs: processArgs,
APIVersion: 2,
Debugger: debugger.Config{
AttachPid: traceAttachPid,
WorkingDir: workingDir,
Backend: backend,
CheckGoVersion: checkGoVersion,
AttachPid: traceAttachPid,
WorkingDir: workingDir,
Backend: backend,
CheckGoVersion: checkGoVersion,
DebugInfoDirectories: conf.DebugInfoDirectories,
},
})
@ -818,12 +826,17 @@ func getPackageDir(pkg []string) string {
}
func attachCmd(cmd *cobra.Command, args []string) {
pid, err := strconv.Atoi(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Invalid pid: %s\n", args[0])
os.Exit(1)
var pid int
if len(args) > 0 {
var err error
pid, err = strconv.Atoi(args[0])
if err != nil {
fmt.Fprintf(os.Stderr, "Invalid pid: %s\n", args[0])
os.Exit(1)
}
args = args[1:]
}
os.Exit(execute(pid, args[1:], conf, "", debugger.ExecutingOther, args, buildFlags))
os.Exit(execute(pid, args, conf, "", debugger.ExecutingOther, args, buildFlags))
}
func coreCmd(cmd *cobra.Command, args []string) {
@ -1005,22 +1018,25 @@ func execute(attachPid int, processArgs []string, conf *config.Config, coreFile
CheckLocalConnUser: checkLocalConnUser,
DisconnectChan: disconnectChan,
Debugger: debugger.Config{
AttachPid: attachPid,
WorkingDir: workingDir,
Backend: backend,
CoreFile: coreFile,
Foreground: headless && tty == "",
Packages: dlvArgs,
BuildFlags: buildFlags,
ExecuteKind: kind,
DebugInfoDirectories: conf.DebugInfoDirectories,
CheckGoVersion: checkGoVersion,
TTY: tty,
Stdin: redirects[0],
Stdout: proc.OutputRedirect{Path: redirects[1]},
Stderr: proc.OutputRedirect{Path: redirects[2]},
DisableASLR: disableASLR,
RrOnProcessPid: rrOnProcessPid,
AttachPid: attachPid,
WorkingDir: workingDir,
Backend: backend,
CoreFile: coreFile,
Foreground: headless && tty == "",
Packages: dlvArgs,
BuildFlags: buildFlags,
ExecuteKind: kind,
DebugInfoDirectories: conf.DebugInfoDirectories,
CheckGoVersion: checkGoVersion,
TTY: tty,
Stdin: redirects[0],
Stdout: proc.OutputRedirect{Path: redirects[1]},
Stderr: proc.OutputRedirect{Path: redirects[2]},
DisableASLR: disableASLR,
RrOnProcessPid: rrOnProcessPid,
AttachWaitFor: attachWaitFor,
AttachWaitForInterval: attachWaitForInterval,
AttachWaitForDuration: attachWaitForDuration,
},
})
default:

@ -588,7 +588,7 @@ func LLDBLaunch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs [
// Path is path to the target's executable, path only needs to be specified
// for some stubs that do not provide an automated way of determining it
// (for example debugserver).
func LLDBAttach(pid int, path string, debugInfoDirs []string) (*proc.TargetGroup, error) {
func LLDBAttach(pid int, path string, waitFor *proc.WaitFor, debugInfoDirs []string) (*proc.TargetGroup, error) {
if runtime.GOOS == "windows" {
return nil, ErrUnsupportedOS
}
@ -609,12 +609,28 @@ func LLDBAttach(pid int, path string, debugInfoDirs []string) (*proc.TargetGroup
if err != nil {
return nil, err
}
args := []string{"-R", fmt.Sprintf("127.0.0.1:%d", listener.Addr().(*net.TCPAddr).Port), "--attach=" + strconv.Itoa(pid)}
args := []string{"-R", fmt.Sprintf("127.0.0.1:%d", listener.Addr().(*net.TCPAddr).Port)}
if waitFor.Valid() {
duration := int(waitFor.Duration.Seconds())
if duration == 0 && waitFor.Duration != 0 {
// If duration is below the (second) resolution of debugserver pass 1
// second (0 means infinite).
duration = 1
}
args = append(args, "--waitfor="+waitFor.Name, fmt.Sprintf("--waitfor-interval=%d", waitFor.Interval.Microseconds()), fmt.Sprintf("--waitfor-duration=%d", duration))
} else {
args = append(args, "--attach="+strconv.Itoa(pid))
}
if canUnmaskSignals(debugserverExecutable) {
args = append(args, "--unmask-signals")
}
process = commandLogger(debugserverExecutable, args...)
} else {
if waitFor.Valid() {
return nil, proc.ErrWaitForNotImplemented
}
if _, err = exec.LookPath("lldb-server"); err != nil {
return nil, &ErrBackendUnavailable{}
}

@ -2,6 +2,7 @@ package proc
import (
"sync"
"time"
"github.com/go-delve/delve/pkg/elfwriter"
"github.com/go-delve/delve/pkg/proc/internal/ebpf"
@ -142,3 +143,10 @@ func (cctx *ContinueOnceContext) GetManualStopRequested() bool {
defer cctx.StopMu.Unlock()
return cctx.manualStopRequested
}
// WaitFor is passed to native.Attach and gdbserver.LLDBAttach to wait for a
// process to start before attaching.
type WaitFor struct {
Name string
Interval, Duration time.Duration
}

@ -21,10 +21,14 @@ func Launch(_ []string, _ string, _ proc.LaunchFlags, _ []string, _ string, _ st
}
// Attach returns ErrNativeBackendDisabled.
func Attach(_ int, _ []string) (*proc.TargetGroup, error) {
func Attach(_ int, _ *proc.WaitFor, _ []string) (*proc.TargetGroup, error) {
return nil, ErrNativeBackendDisabled
}
func waitForSearchProcess(string, map[int]struct{}) (int, error) {
return 0, proc.ErrWaitForNotImplemented
}
// waitStatus is a synonym for the platform-specific WaitStatus
type waitStatus struct{}

@ -1,8 +1,10 @@
package native
import (
"errors"
"os"
"runtime"
"time"
"github.com/go-delve/delve/pkg/proc"
)
@ -69,6 +71,23 @@ func newChildProcess(dbp *nativeProcess, pid int) *nativeProcess {
}
}
// WaitFor waits for a process as specified by waitFor.
func WaitFor(waitFor *proc.WaitFor) (int, error) {
t0 := time.Now()
seen := make(map[int]struct{})
for (waitFor.Duration == 0) || (time.Since(t0) < waitFor.Duration) {
pid, err := waitForSearchProcess(waitFor.Name, seen)
if err != nil {
return 0, err
}
if pid != 0 {
return pid, nil
}
time.Sleep(waitFor.Interval)
}
return 0, errors.New("waitfor duration expired")
}
// BinInfo will return the binary info struct associated with this process.
func (dbp *nativeProcess) BinInfo() *proc.BinaryInfo {
return dbp.bi

@ -136,8 +136,15 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ strin
return tgt, err
}
func waitForSearchProcess(string, map[int]struct{}) (int, error) {
return 0, proc.ErrWaitForNotImplemented
}
// Attach to an existing process with the given PID.
func Attach(pid int, _ []string) (*proc.TargetGroup, error) {
func Attach(pid int, waitFor *proc.WaitFor, _ []string) (*proc.TargetGroup, error) {
if waitFor.Valid() {
return nil, proc.ErrWaitForNotImplemented
}
if err := macutil.CheckRosetta(); err != nil {
return nil, err
}

@ -3,6 +3,7 @@ package native
// #cgo LDFLAGS: -lprocstat
// #include <stdlib.h>
// #include "proc_freebsd.h"
// #include <sys/sysctl.h>
import "C"
import (
"fmt"
@ -14,6 +15,7 @@ import (
sys "golang.org/x/sys/unix"
"github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/pkg/proc/internal/ebpf"
@ -121,7 +123,15 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []str
// Attach to an existing process with the given PID. Once attached, if
// the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in.
func Attach(pid int, debugInfoDirs []string) (*proc.TargetGroup, error) {
func Attach(pid int, waitFor *proc.WaitFor, debugInfoDirs []string) (*proc.TargetGroup, error) {
if waitFor.Valid() {
var err error
pid, err = WaitFor(waitFor)
if err != nil {
return nil, err
}
}
dbp := newProcess(pid)
var err error
@ -142,6 +152,31 @@ func Attach(pid int, debugInfoDirs []string) (*proc.TargetGroup, error) {
return tgt, nil
}
func waitForSearchProcess(pfx string, seen map[int]struct{}) (int, error) {
log := logflags.DebuggerLogger()
ps := C.procstat_open_sysctl()
defer C.procstat_close(ps)
var cnt C.uint
procs := C.procstat_getprocs(ps, C.KERN_PROC_PROC, 0, &cnt)
defer C.procstat_freeprocs(ps, procs)
proc := procs
for i := 0; i < int(cnt); i++ {
if _, isseen := seen[int(proc.ki_pid)]; isseen {
continue
}
seen[int(proc.ki_pid)] = struct{}{}
argv := strings.Join(getCmdLineInternal(ps, proc), " ")
log.Debugf("waitfor: new process %q", argv)
if strings.HasPrefix(argv, pfx) {
return int(proc.ki_pid), nil
}
proc = (*C.struct_kinfo_proc)(unsafe.Pointer(uintptr(unsafe.Pointer(proc)) + unsafe.Sizeof(*proc)))
}
return 0, nil
}
func initialize(dbp *nativeProcess) (string, error) {
comm, _ := C.find_command_name(C.int(dbp.pid))
defer C.free(unsafe.Pointer(comm))
@ -230,7 +265,17 @@ func findExecutable(path string, pid int) string {
func getCmdLine(pid int) string {
ps := C.procstat_open_sysctl()
kp := C.kinfo_getproc(C.int(pid))
goargv := getCmdLineInternal(ps, kp)
C.free(unsafe.Pointer(kp))
C.procstat_close(ps)
return strings.Join(goargv, " ")
}
func getCmdLineInternal(ps *C.struct_procstat, kp *C.struct_kinfo_proc) []string {
argv := C.procstat_getargv(ps, kp, 0)
if argv == nil {
return nil
}
goargv := []string{}
for {
arg := *argv
@ -240,9 +285,8 @@ func getCmdLine(pid int) string {
argv = (**C.char)(unsafe.Pointer(uintptr(unsafe.Pointer(argv)) + unsafe.Sizeof(*argv)))
goargv = append(goargv, C.GoString(arg))
}
C.free(unsafe.Pointer(kp))
C.procstat_close(ps)
return strings.Join(goargv, " ")
C.procstat_freeargv(ps)
return goargv
}
func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) {

@ -19,6 +19,7 @@ import (
sys "golang.org/x/sys/unix"
"github.com/go-delve/delve/pkg/logflags"
"github.com/go-delve/delve/pkg/proc"
"github.com/go-delve/delve/pkg/proc/internal/ebpf"
"github.com/go-delve/delve/pkg/proc/linutil"
@ -141,7 +142,15 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs []str
// Attach to an existing process with the given PID. Once attached, if
// the DWARF information cannot be found in the binary, Delve will look
// for external debug files in the directories passed in.
func Attach(pid int, debugInfoDirs []string) (*proc.TargetGroup, error) {
func Attach(pid int, waitFor *proc.WaitFor, debugInfoDirs []string) (*proc.TargetGroup, error) {
if waitFor.Valid() {
var err error
pid, err = WaitFor(waitFor)
if err != nil {
return nil, err
}
}
dbp := newProcess(pid)
var err error
@ -169,6 +178,53 @@ func Attach(pid int, debugInfoDirs []string) (*proc.TargetGroup, error) {
return tgt, nil
}
func isProcDir(name string) bool {
for _, ch := range name {
if ch < '0' || ch > '9' {
return false
}
}
return true
}
func waitForSearchProcess(pfx string, seen map[int]struct{}) (int, error) {
log := logflags.DebuggerLogger()
des, err := os.ReadDir("/proc")
if err != nil {
log.Errorf("error reading proc: %v", err)
return 0, nil
}
for _, de := range des {
if !de.IsDir() {
continue
}
name := de.Name()
if !isProcDir(name) {
continue
}
pid, _ := strconv.Atoi(name)
if _, isseen := seen[pid]; isseen {
continue
}
seen[pid] = struct{}{}
buf, err := os.ReadFile(filepath.Join("/proc", name, "cmdline"))
if err != nil {
// probably we just don't have permissions
continue
}
for i := range buf {
if buf[i] == 0 {
buf[i] = ' '
}
}
log.Debugf("waitfor: new process %q", string(buf))
if strings.HasPrefix(string(buf), pfx) {
return pid, nil
}
}
return 0, nil
}
func initialize(dbp *nativeProcess) (string, error) {
comm, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/comm", dbp.pid))
if err == nil {

@ -3,6 +3,7 @@ package native
import (
"fmt"
"os"
"strings"
"syscall"
"unicode/utf16"
"unsafe"
@ -89,7 +90,7 @@ func initialize(dbp *nativeProcess) (string, error) {
return "", proc.ErrProcessExited{Pid: dbp.pid, Status: exitCode}
}
cmdline := dbp.getCmdLine()
cmdline := getCmdLine(dbp.os.hProcess)
// Suspend all threads so that the call to _ContinueDebugEvent will
// not resume the target.
@ -145,7 +146,7 @@ func findExePath(pid int) (string, error) {
var debugPrivilegeRequested = false
// Attach to an existing process with the given PID.
func Attach(pid int, _ []string) (*proc.TargetGroup, error) {
func Attach(pid int, waitFor *proc.WaitFor, _ []string) (*proc.TargetGroup, error) {
var aperr error
if !debugPrivilegeRequested {
debugPrivilegeRequested = true
@ -156,6 +157,14 @@ func Attach(pid int, _ []string) (*proc.TargetGroup, error) {
aperr = acquireDebugPrivilege()
}
if waitFor.Valid() {
var err error
pid, err = WaitFor(waitFor)
if err != nil {
return nil, err
}
}
dbp := newProcess(pid)
var err error
dbp.execPtraceFunc(func() {
@ -214,6 +223,43 @@ func acquireDebugPrivilege() error {
return nil
}
func waitForSearchProcess(pfx string, seen map[int]struct{}) (int, error) {
log := logflags.DebuggerLogger()
handle, err := sys.CreateToolhelp32Snapshot(sys.TH32CS_SNAPPROCESS, 0)
if err != nil {
return 0, fmt.Errorf("could not get process list: %v", err)
}
defer sys.CloseHandle(handle)
var entry sys.ProcessEntry32
entry.Size = uint32(unsafe.Sizeof(entry))
err = sys.Process32First(handle, &entry)
if err != nil {
return 0, fmt.Errorf("could not get process list: %v", err)
}
for err = sys.Process32First(handle, &entry); err == nil; err = sys.Process32Next(handle, &entry) {
if _, isseen := seen[int(entry.ProcessID)]; isseen {
continue
}
seen[int(entry.ProcessID)] = struct{}{}
hProcess, err := sys.OpenProcess(sys.PROCESS_QUERY_INFORMATION|sys.PROCESS_VM_READ, false, entry.ProcessID)
if err != nil {
continue
}
cmdline := getCmdLine(syscall.Handle(hProcess))
sys.CloseHandle(hProcess)
log.Debugf("waitfor: new process %q", cmdline)
if strings.HasPrefix(cmdline, pfx) {
return int(entry.ProcessID), nil
}
}
return 0, nil
}
// kill kills the process.
func (dbp *nativeProcess) kill() error {
if dbp.exited {
@ -695,22 +741,22 @@ type _NTUnicodeString struct {
Buffer uintptr
}
func (dbp *nativeProcess) getCmdLine() string {
func getCmdLine(hProcess syscall.Handle) string {
logger := logflags.DebuggerLogger()
var info _PROCESS_BASIC_INFORMATION
err := sys.NtQueryInformationProcess(sys.Handle(dbp.os.hProcess), sys.ProcessBasicInformation, unsafe.Pointer(&info), uint32(unsafe.Sizeof(info)), nil)
err := sys.NtQueryInformationProcess(sys.Handle(hProcess), sys.ProcessBasicInformation, unsafe.Pointer(&info), uint32(unsafe.Sizeof(info)), nil)
if err != nil {
logger.Errorf("NtQueryInformationProcess: %v", err)
return ""
}
var peb _PEB
err = _ReadProcessMemory(dbp.os.hProcess, info.PebBaseAddress, (*byte)(unsafe.Pointer(&peb)), unsafe.Sizeof(peb), nil)
err = _ReadProcessMemory(hProcess, info.PebBaseAddress, (*byte)(unsafe.Pointer(&peb)), unsafe.Sizeof(peb), nil)
if err != nil {
logger.Errorf("Reading PEB: %v", err)
return ""
}
var upp _RTL_USER_PROCESS_PARAMETERS
err = _ReadProcessMemory(dbp.os.hProcess, peb.ProcessParameters, (*byte)(unsafe.Pointer(&upp)), unsafe.Sizeof(upp), nil)
err = _ReadProcessMemory(hProcess, peb.ProcessParameters, (*byte)(unsafe.Pointer(&upp)), unsafe.Sizeof(upp), nil)
if err != nil {
logger.Errorf("Reading ProcessParameters: %v", err)
return ""
@ -720,7 +766,7 @@ func (dbp *nativeProcess) getCmdLine() string {
return ""
}
buf := make([]byte, upp.CommandLine.Length)
err = _ReadProcessMemory(dbp.os.hProcess, upp.CommandLine.Buffer, &buf[0], uintptr(len(buf)), nil)
err = _ReadProcessMemory(hProcess, upp.CommandLine.Buffer, &buf[0], uintptr(len(buf)), nil)
if err != nil {
logger.Errorf("Reading CommandLine: %v", err)
return ""

@ -20,6 +20,7 @@ import (
"sort"
"strconv"
"strings"
"sync"
"testing"
"text/tabwriter"
"time"
@ -2912,13 +2913,13 @@ func TestAttachDetach(t *testing.T) {
switch testBackend {
case "native":
p, err = native.Attach(cmd.Process.Pid, []string{})
p, err = native.Attach(cmd.Process.Pid, nil, []string{})
case "lldb":
path := ""
if runtime.GOOS == "darwin" {
path = fixture.Path
}
p, err = gdbserial.LLDBAttach(cmd.Process.Pid, path, []string{})
p, err = gdbserial.LLDBAttach(cmd.Process.Pid, path, nil, []string{})
default:
err = fmt.Errorf("unknown backend %q", testBackend)
}
@ -6168,3 +6169,91 @@ func TestReadTargetArguments(t *testing.T) {
}
})
}
func testWaitForSetup(t *testing.T, mu *sync.Mutex, started *bool) (*exec.Cmd, *proc.WaitFor) {
var buildFlags protest.BuildFlags
if buildMode == "pie" {
buildFlags |= protest.BuildModePIE
}
fixture := protest.BuildFixture("loopprog", buildFlags)
cmd := exec.Command(fixture.Path)
go func() {
time.Sleep(2 * time.Second)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
assertNoError(cmd.Start(), t, "starting fixture")
mu.Lock()
*started = true
mu.Unlock()
}()
waitFor := &proc.WaitFor{Name: fixture.Path, Interval: 100 * time.Millisecond, Duration: 10 * time.Second}
return cmd, waitFor
}
func TestWaitFor(t *testing.T) {
skipOn(t, "waitfor implementation is delegated to debugserver", "darwin")
var mu sync.Mutex
started := false
cmd, waitFor := testWaitForSetup(t, &mu, &started)
pid, err := native.WaitFor(waitFor)
assertNoError(err, t, "waitFor.Wait()")
if pid != cmd.Process.Pid {
t.Errorf("pid mismatch, expected %d got %d", pid, cmd.Process.Pid)
}
cmd.Process.Kill()
cmd.Wait()
}
func TestWaitForAttach(t *testing.T) {
if testBackend == "lldb" && runtime.GOOS == "linux" {
bs, _ := ioutil.ReadFile("/proc/sys/kernel/yama/ptrace_scope")
if bs == nil || strings.TrimSpace(string(bs)) != "0" {
t.Logf("can not run TestAttachDetach: %v\n", bs)
return
}
}
if testBackend == "rr" {
return
}
var mu sync.Mutex
started := false
cmd, waitFor := testWaitForSetup(t, &mu, &started)
var p *proc.TargetGroup
var err error
switch testBackend {
case "native":
p, err = native.Attach(0, waitFor, []string{})
case "lldb":
path := ""
if runtime.GOOS == "darwin" {
path = waitFor.Name
}
p, err = gdbserial.LLDBAttach(0, path, waitFor, []string{})
default:
err = fmt.Errorf("unknown backend %q", testBackend)
}
assertNoError(err, t, "Attach")
mu.Lock()
if !started {
t.Fatalf("attach succeeded but started is false")
}
mu.Unlock()
p.Detach(true)
cmd.Wait()
}

@ -87,7 +87,7 @@ func TestSignalDeath(t *testing.T) {
assertNoError(err, t, "StdoutPipe")
cmd.Stderr = os.Stderr
assertNoError(cmd.Start(), t, "starting fixture")
p, err := native.Attach(cmd.Process.Pid, []string{})
p, err := native.Attach(cmd.Process.Pid, nil, []string{})
assertNoError(err, t, "Attach")
stdout.Close() // target will receive SIGPIPE later on
err = p.Continue()

@ -654,3 +654,9 @@ func (*dummyRecordingManipulation) ClearCheckpoint(int) error { return ErrNotRec
func (*dummyRecordingManipulation) Restart(*ContinueOnceContext, string) (Thread, error) {
return nil, ErrNotRecorded
}
var ErrWaitForNotImplemented = errors.New("waitfor not implemented")
func (waitFor *WaitFor) Valid() bool {
return waitFor != nil && waitFor.Name != ""
}

@ -104,6 +104,15 @@ type Config struct {
// AttachPid is the PID of an existing process to which the debugger should
// attach.
AttachPid int
// If AttachWaitFor is set the debugger will wait for a process with a name
// starting with WaitFor and attach to it.
AttachWaitFor string
// AttachWaitForInterval is the time (in milliseconds) that the debugger
// waits between checks for WaitFor.
AttachWaitForInterval float64
// AttachWaitForDuration is the time (in milliseconds) that the debugger
// waits for WaitFor.
AttachWaitForDuration float64
// CoreFile specifies the path to the core dump to open.
CoreFile string
@ -163,14 +172,22 @@ func New(config *Config, processArgs []string) (*Debugger, error) {
// Create the process by either attaching or launching.
switch {
case d.config.AttachPid > 0:
case d.config.AttachPid > 0 || d.config.AttachWaitFor != "":
d.log.Infof("attaching to pid %d", d.config.AttachPid)
path := ""
if len(d.processArgs) > 0 {
path = d.processArgs[0]
}
var waitFor *proc.WaitFor
if d.config.AttachWaitFor != "" {
waitFor = &proc.WaitFor{
Name: d.config.AttachWaitFor,
Interval: time.Duration(d.config.AttachWaitForInterval * float64(time.Millisecond)),
Duration: time.Duration(d.config.AttachWaitForDuration * float64(time.Millisecond)),
}
}
var err error
d.target, err = d.Attach(d.config.AttachPid, path)
d.target, err = d.Attach(d.config.AttachPid, path, waitFor)
if err != nil {
err = go11DecodeErrorCheck(err)
err = noDebugErrorWarning(err)
@ -345,17 +362,17 @@ func (d *Debugger) recordingRun(run func() (string, error)) (*proc.TargetGroup,
}
// Attach will attach to the process specified by 'pid'.
func (d *Debugger) Attach(pid int, path string) (*proc.TargetGroup, error) {
func (d *Debugger) Attach(pid int, path string, waitFor *proc.WaitFor) (*proc.TargetGroup, error) {
switch d.config.Backend {
case "native":
return native.Attach(pid, d.config.DebugInfoDirectories)
return native.Attach(pid, waitFor, d.config.DebugInfoDirectories)
case "lldb":
return betterGdbserialLaunchError(gdbserial.LLDBAttach(pid, path, d.config.DebugInfoDirectories))
return betterGdbserialLaunchError(gdbserial.LLDBAttach(pid, path, waitFor, d.config.DebugInfoDirectories))
case "default":
if runtime.GOOS == "darwin" {
return betterGdbserialLaunchError(gdbserial.LLDBAttach(pid, path, d.config.DebugInfoDirectories))
return betterGdbserialLaunchError(gdbserial.LLDBAttach(pid, path, waitFor, d.config.DebugInfoDirectories))
}
return native.Attach(pid, d.config.DebugInfoDirectories)
return native.Attach(pid, waitFor, d.config.DebugInfoDirectories)
default:
return nil, fmt.Errorf("unknown backend %q", d.config.Backend)
}