From 67acab0e386e589509a3806587996084e9f2e5b2 Mon Sep 17 00:00:00 2001 From: aarzilli Date: Tue, 18 Jul 2023 09:55:59 +0200 Subject: [PATCH] proc: add waitfor option to attach 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. --- Documentation/backend_test_health.md | 2 + Documentation/usage/dlv_attach.md | 7 ++- Makefile | 2 +- cmd/dlv/cmds/commands.go | 70 +++++++++++++-------- pkg/proc/gdbserial/gdbserver.go | 20 +++++- pkg/proc/interface.go | 8 +++ pkg/proc/native/nonative_darwin.go | 6 +- pkg/proc/native/proc.go | 19 ++++++ pkg/proc/native/proc_darwin.go | 9 ++- pkg/proc/native/proc_freebsd.go | 52 ++++++++++++++-- pkg/proc/native/proc_linux.go | 58 ++++++++++++++++- pkg/proc/native/proc_windows.go | 60 +++++++++++++++--- pkg/proc/proc_test.go | 93 +++++++++++++++++++++++++++- pkg/proc/proc_unix_test.go | 2 +- pkg/proc/target.go | 6 ++ service/debugger/debugger.go | 31 +++++++--- 16 files changed, 389 insertions(+), 56 deletions(-) diff --git a/Documentation/backend_test_health.md b/Documentation/backend_test_health.md index 72527ad1db..2a6661d91e 100644 --- a/Documentation/backend_test_health.md +++ b/Documentation/backend_test_health.md @@ -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 diff --git a/Documentation/usage/dlv_attach.md b/Documentation/usage/dlv_attach.md index 3b9c22de2b..ac9164e24f 100644 --- a/Documentation/usage/dlv_attach.md +++ b/Documentation/usage/dlv_attach.md @@ -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 diff --git a/Makefile b/Makefile index fef7998a18..fd9672e1ef 100644 --- a/Makefile +++ b/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) diff --git a/cmd/dlv/cmds/commands.go b/cmd/dlv/cmds/commands.go index b8d2d7eb72..dbb4bcdbb8 100644 --- a/cmd/dlv/cmds/commands.go +++ b/cmd/dlv/cmds/commands.go @@ -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: diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go index b51545a044..bb57f3d587 100644 --- a/pkg/proc/gdbserial/gdbserver.go +++ b/pkg/proc/gdbserial/gdbserver.go @@ -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{} } diff --git a/pkg/proc/interface.go b/pkg/proc/interface.go index d2a0ef3550..84bd80ab1c 100644 --- a/pkg/proc/interface.go +++ b/pkg/proc/interface.go @@ -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 +} diff --git a/pkg/proc/native/nonative_darwin.go b/pkg/proc/native/nonative_darwin.go index 616fe869eb..0fa7be5275 100644 --- a/pkg/proc/native/nonative_darwin.go +++ b/pkg/proc/native/nonative_darwin.go @@ -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{} diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go index cd84391f2d..aeb4b23f85 100644 --- a/pkg/proc/native/proc.go +++ b/pkg/proc/native/proc.go @@ -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 diff --git a/pkg/proc/native/proc_darwin.go b/pkg/proc/native/proc_darwin.go index 7a2bbb9b57..6332734487 100644 --- a/pkg/proc/native/proc_darwin.go +++ b/pkg/proc/native/proc_darwin.go @@ -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 } diff --git a/pkg/proc/native/proc_freebsd.go b/pkg/proc/native/proc_freebsd.go index 23f64ed6cc..2d04b92f04 100644 --- a/pkg/proc/native/proc_freebsd.go +++ b/pkg/proc/native/proc_freebsd.go @@ -3,6 +3,7 @@ package native // #cgo LDFLAGS: -lprocstat // #include // #include "proc_freebsd.h" +// #include 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) { diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go index b61e663d51..30a90d2682 100644 --- a/pkg/proc/native/proc_linux.go +++ b/pkg/proc/native/proc_linux.go @@ -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 { diff --git a/pkg/proc/native/proc_windows.go b/pkg/proc/native/proc_windows.go index 9cbf326430..73e4a7a4f9 100644 --- a/pkg/proc/native/proc_windows.go +++ b/pkg/proc/native/proc_windows.go @@ -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 "" diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index 4e5d5ca987..fb77c44b12 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -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() +} diff --git a/pkg/proc/proc_unix_test.go b/pkg/proc/proc_unix_test.go index 0404995bec..5cc68acb1d 100644 --- a/pkg/proc/proc_unix_test.go +++ b/pkg/proc/proc_unix_test.go @@ -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() diff --git a/pkg/proc/target.go b/pkg/proc/target.go index a79769536f..a612f9addb 100644 --- a/pkg/proc/target.go +++ b/pkg/proc/target.go @@ -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 != "" +} diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index d8608cb1f9..78a7792903 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -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) }