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:
parent
2b785f293b
commit
dc5d8de320
@ -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
|
||||
|
2
Makefile
2
Makefile
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user