diff --git a/pkg/proc/core/core.go b/pkg/proc/core/core.go index 734dbd4159..358c0b085f 100644 --- a/pkg/proc/core/core.go +++ b/pkg/proc/core/core.go @@ -227,7 +227,7 @@ func OpenCore(corePath, exePath string, debugInfoDirs []string) (*proc.TargetGro DisableAsyncPreempt: false, CanDump: false, }) - _, err = addTarget(p, p.pid, currentThread, exePath, proc.StopAttached) + _, err = addTarget(p, p.pid, currentThread, exePath, proc.StopAttached, "") return grp, err } diff --git a/pkg/proc/gdbserial/gdbserver.go b/pkg/proc/gdbserial/gdbserver.go index 2bb871991e..b51545a044 100644 --- a/pkg/proc/gdbserial/gdbserver.go +++ b/pkg/proc/gdbserial/gdbserver.go @@ -278,7 +278,7 @@ func newProcess(process *os.Process) *gdbProcess { } // Listen waits for a connection from the stub. -func (p *gdbProcess) Listen(listener net.Listener, path string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) { +func (p *gdbProcess) Listen(listener net.Listener, path, cmdline string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) { acceptChan := make(chan net.Conn) go func() { @@ -292,7 +292,7 @@ func (p *gdbProcess) Listen(listener net.Listener, path string, pid int, debugIn if conn == nil { return nil, errors.New("could not connect") } - return p.Connect(conn, path, pid, debugInfoDirs, stopReason) + return p.Connect(conn, path, cmdline, pid, debugInfoDirs, stopReason) case status := <-p.waitChan: listener.Close() return nil, fmt.Errorf("stub exited while waiting for connection: %v", status) @@ -300,11 +300,11 @@ func (p *gdbProcess) Listen(listener net.Listener, path string, pid int, debugIn } // Dial attempts to connect to the stub. -func (p *gdbProcess) Dial(addr string, path string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) { +func (p *gdbProcess) Dial(addr string, path, cmdline string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) { for { conn, err := net.Dial("tcp", addr) if err == nil { - return p.Connect(conn, path, pid, debugInfoDirs, stopReason) + return p.Connect(conn, path, cmdline, pid, debugInfoDirs, stopReason) } select { case status := <-p.waitChan: @@ -321,7 +321,7 @@ func (p *gdbProcess) Dial(addr string, path string, pid int, debugInfoDirs []str // program and the PID of the target process, both are optional, however // some stubs do not provide ways to determine path and pid automatically // and Connect will be unable to function without knowing them. -func (p *gdbProcess) Connect(conn net.Conn, path string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) { +func (p *gdbProcess) Connect(conn net.Conn, path, cmdline string, pid int, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) { p.conn.conn = conn p.conn.pid = pid err := p.conn.handshake(p.regnames) @@ -345,7 +345,7 @@ func (p *gdbProcess) Connect(conn net.Conn, path string, pid int, debugInfoDirs p.gcmdok = false } - tgt, err := p.initialize(path, debugInfoDirs, stopReason) + tgt, err := p.initialize(path, cmdline, debugInfoDirs, stopReason) if err != nil { return nil, err } @@ -569,9 +569,9 @@ func LLDBLaunch(cmd []string, wd string, flags proc.LaunchFlags, debugInfoDirs [ var grp *proc.TargetGroup if listener != nil { - grp, err = p.Listen(listener, cmd[0], 0, debugInfoDirs, proc.StopLaunched) + grp, err = p.Listen(listener, cmd[0], strings.Join(cmd, " "), 0, debugInfoDirs, proc.StopLaunched) } else { - grp, err = p.Dial(port, cmd[0], 0, debugInfoDirs, proc.StopLaunched) + grp, err = p.Dial(port, cmd[0], strings.Join(cmd, " "), 0, debugInfoDirs, proc.StopLaunched) } if p.conn.pid != 0 && foreground && isatty.IsTerminal(os.Stdin.Fd()) { // Make the target process the controlling process of the tty if it is a foreground process. @@ -635,9 +635,9 @@ func LLDBAttach(pid int, path string, debugInfoDirs []string) (*proc.TargetGroup var grp *proc.TargetGroup if listener != nil { - grp, err = p.Listen(listener, path, pid, debugInfoDirs, proc.StopAttached) + grp, err = p.Listen(listener, path, "", pid, debugInfoDirs, proc.StopAttached) } else { - grp, err = p.Dial(port, path, pid, debugInfoDirs, proc.StopAttached) + grp, err = p.Dial(port, path, "", pid, debugInfoDirs, proc.StopAttached) } return grp, err } @@ -673,7 +673,7 @@ func (p *gdbProcess) EntryPoint() (uint64, error) { // initialize uses qProcessInfo to load the inferior's PID and // executable path. This command is not supported by all stubs and not all // stubs will report both the PID and executable path. -func (p *gdbProcess) initialize(path string, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) { +func (p *gdbProcess) initialize(path, cmdline string, debugInfoDirs []string, stopReason proc.StopReason) (*proc.TargetGroup, error) { var err error if path == "" { // If we are attaching to a running process and the user didn't specify @@ -730,7 +730,7 @@ func (p *gdbProcess) initialize(path string, debugInfoDirs []string, stopReason StopReason: stopReason, CanDump: runtime.GOOS == "darwin", }) - _, err = addTarget(p, p.conn.pid, p.currentThread, path, stopReason) + _, err = addTarget(p, p.conn.pid, p.currentThread, path, stopReason, cmdline) if err != nil { p.Detach(true) return nil, err diff --git a/pkg/proc/gdbserial/rr.go b/pkg/proc/gdbserial/rr.go index ca70850b62..ed381cdd2b 100644 --- a/pkg/proc/gdbserial/rr.go +++ b/pkg/proc/gdbserial/rr.go @@ -124,7 +124,7 @@ func Record(cmd []string, wd string, quiet bool, redirects [3]string) (tracedir // Replay starts an instance of rr in replay mode, with the specified trace // directory, and connects to it. -func Replay(tracedir string, quiet, deleteOnDetach bool, debugInfoDirs []string, rrOnProcessPid int) (*proc.TargetGroup, error) { +func Replay(tracedir string, quiet, deleteOnDetach bool, debugInfoDirs []string, rrOnProcessPid int, cmdline string) (*proc.TargetGroup, error) { if err := checkRRAvailable(); err != nil { return nil, err } @@ -168,7 +168,7 @@ func Replay(tracedir string, quiet, deleteOnDetach bool, debugInfoDirs []string, safeRemoveAll(p.tracedir) } } - tgt, err := p.Dial(init.port, init.exe, 0, debugInfoDirs, proc.StopLaunched) + tgt, err := p.Dial(init.port, init.exe, cmdline, 0, debugInfoDirs, proc.StopLaunched) if err != nil { rrcmd.Process.Kill() return nil, err @@ -293,7 +293,7 @@ func RecordAndReplay(cmd []string, wd string, quiet bool, debugInfoDirs []string if tracedir == "" { return nil, "", err } - t, err := Replay(tracedir, quiet, true, debugInfoDirs, 0) + t, err := Replay(tracedir, quiet, true, debugInfoDirs, 0, strings.Join(cmd, " ")) return t, tracedir, err } diff --git a/pkg/proc/native/nonative_darwin.go b/pkg/proc/native/nonative_darwin.go index d0f7b19a51..b60ee32d54 100644 --- a/pkg/proc/native/nonative_darwin.go +++ b/pkg/proc/native/nonative_darwin.go @@ -142,4 +142,4 @@ func (t *nativeThread) SoftExc() bool { panic(ErrNativeBackendDisabled) } -func initialize(dbp *nativeProcess) error { return nil } +func initialize(dbp *nativeProcess) (string, error) { return "", nil } diff --git a/pkg/proc/native/proc.go b/pkg/proc/native/proc.go index 69423f29dc..c886202b99 100644 --- a/pkg/proc/native/proc.go +++ b/pkg/proc/native/proc.go @@ -207,12 +207,14 @@ func (procgrp *processGroup) procForThread(tid int) *nativeProcess { return nil } -func (procgrp *processGroup) add(p *nativeProcess, pid int, currentThread proc.Thread, path string, stopReason proc.StopReason) (*proc.Target, error) { - tgt, err := procgrp.addTarget(p, pid, currentThread, path, stopReason) +func (procgrp *processGroup) add(p *nativeProcess, pid int, currentThread proc.Thread, path string, stopReason proc.StopReason, cmdline string) (*proc.Target, error) { + tgt, err := procgrp.addTarget(p, pid, currentThread, path, stopReason, cmdline) if err != nil { return nil, err } - procgrp.procs = append(procgrp.procs, p) + if tgt != nil { + procgrp.procs = append(procgrp.procs, p) + } return tgt, nil } @@ -285,20 +287,24 @@ func (dbp *nativeProcess) FindBreakpoint(pc uint64, adjustPC bool) (*proc.Breakp return nil, false } -func (dbp *nativeProcess) initializeBasic() error { - if err := initialize(dbp); err != nil { - return err +func (dbp *nativeProcess) initializeBasic() (string, error) { + cmdline, err := initialize(dbp) + if err != nil { + return "", err } if err := dbp.updateThreadList(); err != nil { - return err + return "", err } - return nil + return cmdline, nil } // initialize will ensure that all relevant information is loaded // so the process is ready to be debugged. func (dbp *nativeProcess) initialize(path string, debugInfoDirs []string) (*proc.TargetGroup, error) { - dbp.initializeBasic() + cmdline, err := dbp.initializeBasic() + if err != nil { + return nil, err + } stopReason := proc.StopLaunched if !dbp.childProcess { stopReason = proc.StopAttached @@ -321,7 +327,7 @@ func (dbp *nativeProcess) initialize(path string, debugInfoDirs []string) (*proc CanDump: runtime.GOOS == "linux" || runtime.GOOS == "freebsd" || (runtime.GOOS == "windows" && runtime.GOARCH == "amd64"), }) procgrp.addTarget = addTarget - tgt, err := procgrp.add(dbp, dbp.pid, dbp.memthread, path, stopReason) + tgt, err := procgrp.add(dbp, dbp.pid, dbp.memthread, path, stopReason, cmdline) if err != nil { return nil, err } diff --git a/pkg/proc/native/proc_darwin.go b/pkg/proc/native/proc_darwin.go index c8eca1da2e..83eb2dbc7d 100644 --- a/pkg/proc/native/proc_darwin.go +++ b/pkg/proc/native/proc_darwin.go @@ -489,4 +489,4 @@ func (dbp *nativeProcess) GetBufferedTracepoints() []ebpf.RawUProbeParams { panic("not implemented") } -func initialize(dbp *nativeProcess) error { return nil } +func initialize(dbp *nativeProcess) (string, error) { return "", nil } diff --git a/pkg/proc/native/proc_freebsd.c b/pkg/proc/native/proc_freebsd.c index 887aa4df1c..8716f614ed 100644 --- a/pkg/proc/native/proc_freebsd.c +++ b/pkg/proc/native/proc_freebsd.c @@ -2,11 +2,7 @@ #include #include #include -#include -#include -#include -#include #include #include #include diff --git a/pkg/proc/native/proc_freebsd.go b/pkg/proc/native/proc_freebsd.go index 40581e5ae9..51f4889658 100644 --- a/pkg/proc/native/proc_freebsd.go +++ b/pkg/proc/native/proc_freebsd.go @@ -142,12 +142,12 @@ func Attach(pid int, debugInfoDirs []string) (*proc.TargetGroup, error) { return tgt, nil } -func initialize(dbp *nativeProcess) error { +func initialize(dbp *nativeProcess) (string, error) { comm, _ := C.find_command_name(C.int(dbp.pid)) defer C.free(unsafe.Pointer(comm)) comm_str := C.GoString(comm) dbp.os.comm = strings.ReplaceAll(string(comm_str), "%", "%%") - return nil + return getCmdLine(dbp.pid), nil } // kill kills the target process. @@ -227,6 +227,24 @@ func findExecutable(path string, pid int) string { return path } +func getCmdLine(pid int) string { + ps := C.procstat_open_sysctl() + kp := C.kinfo_getproc(C.int(pid)) + argv := C.procstat_getargv(ps, kp, 0) + goargv := []string{} + for { + arg := *argv + if arg == nil { + break + } + 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, " ") +} + func trapWait(procgrp *processGroup, pid int) (*nativeThread, error) { return procgrp.procs[0].trapWaitInternal(pid, trapWaitNormal) } diff --git a/pkg/proc/native/proc_freebsd.h b/pkg/proc/native/proc_freebsd.h index 76bbc38309..645c3e1035 100644 --- a/pkg/proc/native/proc_freebsd.h +++ b/pkg/proc/native/proc_freebsd.h @@ -1,4 +1,7 @@ #include +#include +#include +#include char * find_command_name(int pid); char * find_executable(int pid); diff --git a/pkg/proc/native/proc_linux.go b/pkg/proc/native/proc_linux.go index e549928b58..a05ecb24f7 100644 --- a/pkg/proc/native/proc_linux.go +++ b/pkg/proc/native/proc_linux.go @@ -169,7 +169,7 @@ func Attach(pid int, debugInfoDirs []string) (*proc.TargetGroup, error) { return tgt, nil } -func initialize(dbp *nativeProcess) error { +func initialize(dbp *nativeProcess) (string, error) { comm, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/comm", dbp.pid)) if err == nil { // removes newline character @@ -179,22 +179,22 @@ func initialize(dbp *nativeProcess) error { if comm == nil || len(comm) <= 0 { stat, err := ioutil.ReadFile(fmt.Sprintf("/proc/%d/stat", dbp.pid)) if err != nil { - return fmt.Errorf("could not read proc stat: %v", err) + return "", fmt.Errorf("could not read proc stat: %v", err) } expr := fmt.Sprintf("%d\\s*\\((.*)\\)", dbp.pid) rexp, err := regexp.Compile(expr) if err != nil { - return fmt.Errorf("regexp compile error: %v", err) + return "", fmt.Errorf("regexp compile error: %v", err) } match := rexp.FindSubmatch(stat) if match == nil { - return fmt.Errorf("no match found using regexp '%s' in /proc/%d/stat", expr, dbp.pid) + return "", fmt.Errorf("no match found using regexp '%s' in /proc/%d/stat", expr, dbp.pid) } comm = match[1] } dbp.os.comm = strings.ReplaceAll(string(comm), "%", "%%") - return nil + return getCmdLine(dbp.pid), nil } func (dbp *nativeProcess) GetBufferedTracepoints() []ebpf.RawUProbeParams { @@ -460,8 +460,8 @@ func trapWaitInternal(procgrp *processGroup, pid int, options trapWaitOptions) ( } dbp = newChildProcess(procgrp.procs[0], wpid) dbp.followExec = true - dbp.initializeBasic() - _, err := procgrp.add(dbp, dbp.pid, dbp.memthread, findExecutable("", dbp.pid), proc.StopLaunched) + cmdline, _ := dbp.initializeBasic() + tgt, err := procgrp.add(dbp, dbp.pid, dbp.memthread, findExecutable("", dbp.pid), proc.StopLaunched, cmdline) if err != nil { _ = dbp.Detach(false) return nil, err @@ -469,12 +469,16 @@ func trapWaitInternal(procgrp *processGroup, pid int, options trapWaitOptions) ( if halt { return nil, nil } - // TODO(aarzilli): if we want to give users the ability to stop the target - // group on exec here is where we should return - err = dbp.threads[dbp.pid].Continue() - if err != nil { - return nil, err + if tgt != nil { + // If tgt is nil we decided we are not interested in debugging this + // process, and we have already detached from it. + err = dbp.threads[dbp.pid].Continue() + if err != nil { + return nil, err + } } + //TODO(aarzilli): if we want to give users the ability to stop the target + //group on exec here is where we should return continue } if th == nil { @@ -917,3 +921,17 @@ func (dbp *nativeProcess) FollowExec(v bool) error { func killProcess(pid int) error { return sys.Kill(pid, sys.SIGINT) } + +func getCmdLine(pid int) string { + buf, _ := ioutil.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + args := strings.SplitN(string(buf), "\x00", -1) + for i := range args { + if strings.Contains(args[i], " ") { + args[i] = strconv.Quote(args[i]) + } + } + if len(args) > 0 && args[len(args)-1] == "" { + args = args[:len(args)-1] + } + return strings.Join(args, " ") +} diff --git a/pkg/proc/native/proc_windows.go b/pkg/proc/native/proc_windows.go index 705fe6e500..318533e83b 100644 --- a/pkg/proc/native/proc_windows.go +++ b/pkg/proc/native/proc_windows.go @@ -4,10 +4,12 @@ import ( "fmt" "os" "syscall" + "unicode/utf16" "unsafe" sys "golang.org/x/sys/windows" + "github.com/go-delve/delve/pkg/logflags" "github.com/go-delve/delve/pkg/proc" "github.com/go-delve/delve/pkg/proc/internal/ebpf" ) @@ -68,7 +70,7 @@ func Launch(cmd []string, wd string, flags proc.LaunchFlags, _ []string, _ strin return tgt, nil } -func initialize(dbp *nativeProcess) error { +func initialize(dbp *nativeProcess) (string, error) { // It should not actually be possible for the // call to waitForDebugEvent to fail, since Windows // will always fire a CREATE_PROCESS_DEBUG_EVENT event @@ -80,19 +82,22 @@ func initialize(dbp *nativeProcess) error { tid, exitCode, err = dbp.waitForDebugEvent(waitBlocking) }) if err != nil { - return err + return "", err } if tid == 0 { dbp.postExit() - return proc.ErrProcessExited{Pid: dbp.pid, Status: exitCode} + return "", proc.ErrProcessExited{Pid: dbp.pid, Status: exitCode} } + + cmdline := dbp.getCmdLine() + // Suspend all threads so that the call to _ContinueDebugEvent will // not resume the target. for _, thread := range dbp.threads { if !thread.os.dbgUiRemoteBreakIn { _, err := _SuspendThread(thread.os.hThread) if err != nil { - return err + return "", err } } } @@ -100,10 +105,10 @@ func initialize(dbp *nativeProcess) error { dbp.execPtraceFunc(func() { err = _ContinueDebugEvent(uint32(dbp.pid), uint32(dbp.os.breakThread), _DBG_CONTINUE) }) - return err + return cmdline, err } -// findExePath searches for process pid, and returns its executable path. +// findExePath searches for process pid, and returns its executable path func findExePath(pid int) (string, error) { // Original code suggested different approach (see below). // Maybe it could be useful in the future. @@ -604,6 +609,129 @@ func (dbp *nativeProcess) GetBufferedTracepoints() []ebpf.RawUProbeParams { return nil } +type _PROCESS_BASIC_INFORMATION struct { + ExitStatus sys.NTStatus + PebBaseAddress uintptr + AffinityMask uintptr + BasePriority int32 + UniqueProcessId uintptr + InheritedFromUniqueProcessId uintptr +} + +type _PEB struct { + reserved1 [2]byte + BeingDebugged byte + BitField byte + reserved3 uintptr + ImageBaseAddress uintptr + Ldr uintptr + ProcessParameters uintptr + reserved4 [3]uintptr + AtlThunkSListPtr uintptr + reserved5 uintptr + reserved6 uint32 + reserved7 uintptr + reserved8 uint32 + AtlThunkSListPtr32 uint32 + reserved9 [45]uintptr + reserved10 [96]byte + PostProcessInitRoutine uintptr + reserved11 [128]byte + reserved12 [1]uintptr + SessionId uint32 +} + +type _RTL_USER_PROCESS_PARAMETERS struct { + MaximumLength, Length uint32 + + Flags, DebugFlags uint32 + + ConsoleHandle sys.Handle + ConsoleFlags uint32 + StandardInput, StandardOutput, StandardError sys.Handle + + CurrentDirectory struct { + DosPath _NTUnicodeString + Handle sys.Handle + } + + DllPath _NTUnicodeString + ImagePathName _NTUnicodeString + CommandLine _NTUnicodeString + Environment unsafe.Pointer + + StartingX, StartingY, CountX, CountY, CountCharsX, CountCharsY, FillAttribute uint32 + + WindowFlags, ShowWindowFlags uint32 + WindowTitle, DesktopInfo, ShellInfo, RuntimeData _NTUnicodeString + CurrentDirectories [32]struct { + Flags uint16 + Length uint16 + TimeStamp uint32 + DosPath _NTString + } + + EnvironmentSize, EnvironmentVersion uintptr + + PackageDependencyData uintptr + ProcessGroupId uint32 + LoaderThreads uint32 + + RedirectionDllName _NTUnicodeString + HeapPartitionName _NTUnicodeString + DefaultThreadpoolCpuSetMasks uintptr + DefaultThreadpoolCpuSetMaskCount uint32 +} + +type _NTString struct { + Length uint16 + MaximumLength uint16 + Buffer uintptr +} + +type _NTUnicodeString struct { + Length uint16 + MaximumLength uint16 + Buffer uintptr +} + +func (dbp *nativeProcess) getCmdLine() 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) + 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) + 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) + if err != nil { + logger.Errorf("Reading ProcessParameters: %v", err) + return "" + } + if upp.CommandLine.Length%2 != 0 { + logger.Errorf("CommandLine length not a multiple of 2") + return "" + } + buf := make([]byte, upp.CommandLine.Length) + err = _ReadProcessMemory(dbp.os.hProcess, upp.CommandLine.Buffer, &buf[0], uintptr(len(buf)), nil) + if err != nil { + logger.Errorf("Reading CommandLine: %v", err) + return "" + } + utf16buf := make([]uint16, len(buf)/2) + for i := 0; i < len(buf); i += 2 { + utf16buf[i/2] = uint16(buf[i+1])<<8 + uint16(buf[i]) + } + return string(utf16.Decode(utf16buf)) +} + func killProcess(pid int) error { p, err := os.FindProcess(pid) if err != nil { diff --git a/pkg/proc/proc_test.go b/pkg/proc/proc_test.go index bcb53bd901..989edd5a2c 100644 --- a/pkg/proc/proc_test.go +++ b/pkg/proc/proc_test.go @@ -172,6 +172,17 @@ func assertLineNumber(p *proc.Target, t *testing.T, lineno int, descr string) (s return f, l } +func assertFunctionName(p *proc.Target, t *testing.T, fnname string, descr string) { + pc := currentPC(p, t) + f, l, fn := p.BinInfo().PCToLine(pc) + if fn == nil { + t.Fatalf("%s expected function %s got %s:%d", descr, fnname, f, l) + } + if fn.Name != fnname { + t.Fatalf("%s expected function %s got %s %s:%d", descr, fnname, fn.Name, f, l) + } +} + func TestExit(t *testing.T) { protest.AllowRecording(t) withTestProcess("continuetestprog", t, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { @@ -6117,3 +6128,43 @@ func TestStepShadowConcurrentBreakpoint(t *testing.T) { } }) } + +func TestFollowExecRegexFilter(t *testing.T) { + skipUnlessOn(t, "follow exec only supported on linux", "linux") + withTestProcessArgs("spawn", t, ".", []string{"spawn", "3"}, 0, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { + grp.LogicalBreakpoints[1] = &proc.LogicalBreakpoint{LogicalID: 1, Set: proc.SetBreakpoint{FunctionName: "main.traceme1"}, HitCount: make(map[int64]uint64)} + grp.LogicalBreakpoints[2] = &proc.LogicalBreakpoint{LogicalID: 2, Set: proc.SetBreakpoint{FunctionName: "main.traceme2"}, HitCount: make(map[int64]uint64)} + grp.LogicalBreakpoints[3] = &proc.LogicalBreakpoint{LogicalID: 3, Set: proc.SetBreakpoint{FunctionName: "main.traceme3"}, HitCount: make(map[int64]uint64)} + + assertNoError(grp.EnableBreakpoint(grp.LogicalBreakpoints[1]), t, "EnableBreakpoint(main.traceme1)") + assertNoError(grp.EnableBreakpoint(grp.LogicalBreakpoints[3]), t, "EnableBreakpoint(main.traceme3)") + + assertNoError(grp.FollowExec(true, "spawn.* child C1"), t, "FollowExec") + + assertNoError(grp.Continue(), t, "Continue 1") + assertFunctionName(grp.Selected, t, "main.traceme1", "Program did not continue to the expected location (1)") + assertNoError(grp.Continue(), t, "Continue 2") + assertFunctionName(grp.Selected, t, "main.traceme2", "Program did not continue to the expected location (2)") + assertNoError(grp.Continue(), t, "Continue 3") + assertFunctionName(grp.Selected, t, "main.traceme3", "Program did not continue to the expected location (3)") + err := grp.Continue() + if err != nil { + _, isexited := err.(proc.ErrProcessExited) + if !isexited { + assertNoError(err, t, "Continue 4") + } + } else { + t.Fatal("process did not exit after 4 continues") + } + }) +} + +func TestReadTargetArguments(t *testing.T) { + protest.AllowRecording(t) + withTestProcessArgs("restartargs", t, ".", []string{"one", "two", "three"}, 0, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) { + t.Logf("command line: %q\n", p.CmdLine) + if !strings.HasSuffix(p.CmdLine, " one two three") { + t.Fatalf("wrong command line") + } + }) +} diff --git a/pkg/proc/target.go b/pkg/proc/target.go index 05bae74f97..079a99d955 100644 --- a/pkg/proc/target.go +++ b/pkg/proc/target.go @@ -41,7 +41,8 @@ type Target struct { proc ProcessInternal recman RecordingManipulationInternal - pid int + pid int + CmdLine string // StopReason describes the reason why the target process is stopped. // A process could be stopped for multiple simultaneous reasons, in which @@ -160,7 +161,7 @@ func DisableAsyncPreemptEnv() []string { // newTarget returns an initialized Target object. // The p argument can optionally implement the RecordingManipulation interface. -func (grp *TargetGroup) newTarget(p ProcessInternal, pid int, currentThread Thread, path string) (*Target, error) { +func (grp *TargetGroup) newTarget(p ProcessInternal, pid int, currentThread Thread, path, cmdline string) (*Target, error) { entryPoint, err := p.EntryPoint() if err != nil { return nil, err @@ -182,6 +183,7 @@ func (grp *TargetGroup) newTarget(p ProcessInternal, pid int, currentThread Thre fncallForG: make(map[int64]*callInjection), currentThread: currentThread, pid: pid, + CmdLine: cmdline, } if recman, ok := p.(RecordingManipulationInternal); ok { diff --git a/pkg/proc/target_group.go b/pkg/proc/target_group.go index 5a746399e1..e388310c3c 100644 --- a/pkg/proc/target_group.go +++ b/pkg/proc/target_group.go @@ -2,8 +2,8 @@ package proc import ( "bytes" - "errors" "fmt" + "regexp" "strings" "github.com/go-delve/delve/pkg/logflags" @@ -20,6 +20,7 @@ type TargetGroup struct { targets []*Target Selected *Target followExecEnabled bool + followExecRegex *regexp.Regexp RecordingManipulation recman RecordingManipulationInternal @@ -48,7 +49,7 @@ type NewTargetGroupConfig struct { CanDump bool // Can create core dumps (must implement ProcessInternal.MemoryMap) } -type AddTargetFunc func(ProcessInternal, int, Thread, string, StopReason) (*Target, error) +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) { @@ -86,17 +87,29 @@ func Restart(grp, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error)) } } if oldgrp.followExecEnabled { - grp.FollowExec(true, "") + 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) (*Target, error) { - t, err := grp.newTarget(p, pid, currentThread, path) +func (grp *TargetGroup) addTarget(p ProcessInternal, pid int, currentThread Thread, path string, stopReason StopReason, cmdline string) (*Target, error) { + logger := logflags.DebuggerLogger() + t, err := grp.newTarget(p, pid, currentThread, path, cmdline) if err != nil { return nil, err } t.StopReason = stopReason - //TODO(aarzilli): check if the target's command line matches the regex + if grp.followExecRegex != nil && len(grp.targets) > 0 { + if !grp.followExecRegex.MatchString(cmdline) { + logger.Debugf("Detaching from child target %d %q", t.Pid(), t.CmdLine) + t.detach(false) + return nil, nil + } + } + logger.Debugf("Adding target %d %q", t.Pid(), t.CmdLine) if t.partOfGroup { panic("internal error: target is already part of group") } @@ -108,7 +121,7 @@ func (grp *TargetGroup) addTarget(p ProcessInternal, pid int, currentThread Thre if grp.Selected == nil { grp.Selected = t } - logger := logflags.DebuggerLogger() + t.Breakpoints().Logical = grp.LogicalBreakpoints for _, lbp := range grp.LogicalBreakpoints { if lbp.LogicalID < 0 { continue @@ -340,8 +353,13 @@ func (grp *TargetGroup) DisableBreakpoint(lbp *LogicalBreakpoint) error { // 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 { - if regex != "" { - return errors.New("regex not implemented") + 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() { diff --git a/pkg/terminal/command.go b/pkg/terminal/command.go index 52a846df0c..f7661e80d2 100644 --- a/pkg/terminal/command.go +++ b/pkg/terminal/command.go @@ -2553,7 +2553,7 @@ func printcontext(t *Term, state *api.DebuggerState) { if state.Pid != t.oldPid { if t.oldPid != 0 { - fmt.Fprintf(t.stdout, "Switch target process from %d to %d\n", t.oldPid, state.Pid) + fmt.Fprintf(t.stdout, "Switch target process from %d to %d (%s)\n", t.oldPid, state.Pid, state.TargetCommandLine) } t.oldPid = state.Pid } diff --git a/service/api/conversions.go b/service/api/conversions.go index f386c69b41..0d66749eb6 100644 --- a/service/api/conversions.go +++ b/service/api/conversions.go @@ -454,9 +454,9 @@ func ConvertDumpState(dumpState *proc.DumpState) *DumpState { // ConvertTarget converts a proc.Target into a api.Target. func ConvertTarget(tgt *proc.Target, convertThreadBreakpoint func(proc.Thread) *Breakpoint) *Target { - //TODO(aarzilli): copy command line here return &Target{ Pid: tgt.Pid(), + CmdLine: tgt.CmdLine, CurrentThread: ConvertThread(tgt.CurrentThread(), convertThreadBreakpoint(tgt.CurrentThread())), } } diff --git a/service/api/types.go b/service/api/types.go index 7c5de83796..8c48842ae9 100644 --- a/service/api/types.go +++ b/service/api/types.go @@ -19,6 +19,8 @@ var ErrNotExecutable = errors.New("not an executable file") type DebuggerState struct { // PID of the process we are debugging. Pid int + // Command line of the process we are debugging. + TargetCommandLine string // Running is true if the process is running and no other information can be collected. Running bool // Recording is true if the process is currently being recorded and no other diff --git a/service/debugger/debugger.go b/service/debugger/debugger.go index 320adf1b5a..8fddfa8fa3 100644 --- a/service/debugger/debugger.go +++ b/service/debugger/debugger.go @@ -176,7 +176,7 @@ func New(config *Config, processArgs []string) (*Debugger, error) { switch d.config.Backend { case "rr": d.log.Infof("opening trace %s", d.config.CoreFile) - d.target, err = gdbserial.Replay(d.config.CoreFile, false, false, d.config.DebugInfoDirectories, d.config.RrOnProcessPid) + d.target, err = gdbserial.Replay(d.config.CoreFile, false, false, d.config.DebugInfoDirectories, d.config.RrOnProcessPid, "") default: d.log.Infof("opening core file %s (executable %s)", d.config.CoreFile, d.processArgs[0]) d.target, err = core.OpenCore(d.config.CoreFile, d.processArgs[0], d.config.DebugInfoDirectories) @@ -335,7 +335,7 @@ func (d *Debugger) recordingRun(run func() (string, error)) (*proc.TargetGroup, return nil, err } - return gdbserial.Replay(tracedir, false, true, d.config.DebugInfoDirectories, 0) + return gdbserial.Replay(tracedir, false, true, d.config.DebugInfoDirectories, 0, strings.Join(d.processArgs, " ")) } // Attach will attach to the process specified by 'pid'. @@ -568,6 +568,7 @@ func (d *Debugger) state(retLoadCfg *proc.LoadConfig, withBreakpointInfo bool) ( state = &api.DebuggerState{ Pid: tgt.Pid(), + TargetCommandLine: tgt.CmdLine, SelectedGoroutine: goroutine, Exited: exited, }