Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions _fixtures/nongochild/child.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@echo off
echo Hello World
2 changes: 2 additions & 0 deletions _fixtures/nongochild/child.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/bin/sh
echo "Hello World"
27 changes: 27 additions & 0 deletions _fixtures/nongochild/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"os"
"os/exec"
"path/filepath"
"runtime"
)

func main() {
file := "child.sh"
if runtime.GOOS == "windows" {
// This path is not actually exercised currently because prog_windows.go
// doesn't handle ErrBadBinaryInfo.
file = "child.bat"
}
file, err := filepath.Abs(file)
if err != nil {
panic(err)
}

output, err := exec.Command(file).Output()
if err != nil {
panic(err)
}
os.Stdout.Write(output)
}
9 changes: 8 additions & 1 deletion pkg/proc/native/proc_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,14 @@ func trapWaitInternal(procgrp *processGroup, pid int, options trapWaitOptions) (
cmdline, _ := dbp.initializeBasic()
tgt, err := procgrp.add(dbp, dbp.pid, dbp.memthread, findExecutable("", dbp.pid), proc.StopLaunched, cmdline)
if err != nil {
return nil, err
if errors.As(err, new(*proc.ErrBadBinaryInfo)) {
// If we are unable to load the binary info, log the error
// but don't return it. This allows the client to continue
// debugging the parent process.
logflags.DebuggerLogger().Warnf("Child target %d %q cannot be debugged: %v", pid, cmdline, err)
} else {
return nil, err
}
Comment on lines +521 to +528
Copy link
Contributor Author

@firelizzard18 firelizzard18 Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Continuing from #4171 (comment)

Ok, you are right but then the problem is that you are restarting the process.

I still don't see how this is different from the follow-exec-disabled case. The calls to Process.EntryPoint or BinaryInfo.LoadBinaryInfo are literally the only difference between the follow-exec-disabled and unable-to-load-binary-info paths (excluding read-only operations) so unless those function calls somehow cause the process to be restarted I don't see how that's possible. Unless the follow-exec-disabled case is also restarting the process, but if that's the case then it's not caused by my changes.

I was not able to reproduce the TestBadAttachRequest failure locally. Based on the stack traces of the exceptions, I am guessing the failure comes down to TargetGroup.addTarget being called from other places, so I moved the "log and ignore" code here. Which means it's platform specific. I found what I believe to be the equivalent code for Windows so I added a TODO, but I was unable to identify an equivalent block of code for darwin or freebsd. And I don't want to make changes that I can't test myself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see from TestFollowExec that follow-exec appears not to be implemented on darwin or freebsd, which explains why I couldn't find it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still don't see how this is different from the follow-exec-disabled case

It is the same but that path detaches from the child process, which restarts. Which means that by the time you see the process event that child will have made some progress executing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, are you saying "the process resumes execution and thus cannot be debugged from the entry point"? I misunderstood, I thought you were saying "the process is stopped and relaunched" or something along those lines. Though I suppose if the process hasn't executed any code there's not much of a difference.

Which means that by the time you see the process event that child will have made some progress executing.

I think this is just something I/we will have to live with. Having process events is better than not having process events, so I think this is worth implementing as-is. Maybe someone thinks up a better solution down the road. For my purposes, the child resuming is acceptable, because I need to use a non-ptrace debugger anyways, and if I use node's --inspect-brk flag then the part of the child I care about (the javascript, not the runtime) will be paused at entry anyways.

As far as handoff to another debugger, I found PTRACE_SEIZE which might be usable, but apparently it is "fragile and kernel-version dependent". Another thought that occurs to me is having delve act as a gdb rpc server and letting the caller connect that way. But that seems like something that needs a lot more planning before it would be worth considering.

}
if halt {
return nil, nil
Expand Down
1 change: 1 addition & 0 deletions pkg/proc/native/proc_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@ func (procgrp *processGroup) waitForDebugEvent(flags waitForDebugEventFlags) (th
}
tgt, err := procgrp.add(dbp, dbp.pid, dbp.memthread, exe, proc.StopLaunched, getCmdLine(dbp.os.hProcess))
if err != nil {
// TODO: Handle proc.ErrBadBinaryInfo
return 0, err
}
if tgt == nil {
Expand Down
50 changes: 50 additions & 0 deletions pkg/proc/proc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5180,6 +5180,10 @@ func TestFollowExec(t *testing.T) {
pids := map[int]int{}
ns := map[string]int{}

// Collect events
var events []*proc.Event
grp.SetEventsFn(func(e *proc.Event) { events = append(events, e) })

for {
t.Log("Continuing")
err := grp.Continue()
Expand Down Expand Up @@ -5218,6 +5222,15 @@ func TestFollowExec(t *testing.T) {
if finished {
t.Fatalf("breakpoint hit after the last one in a child process")
}

spawned := map[int]*proc.ProcessSpawnedEventDetails{}
for _, event := range events {
if event.Kind == proc.EventProcessSpawned {
details := event.ProcessSpawnedEventDetails
spawned[details.PID] = details
}
}

it := proc.ValidTargets{Group: grp}
for it.Next() {
tgt := it.Target
Expand All @@ -5239,6 +5252,10 @@ func TestFollowExec(t *testing.T) {
}
t.Logf("variable 'n' on target %d: %#v (%v)", tgt.Pid(), nvar, nvar.Value)
ns[constant.StringVal(nvar.Value)]++

if _, ok := spawned[tgt.Pid()]; !ok {
t.Errorf("Expected a process spawned event for target %d", tgt.Pid())
}
}
}
}
Expand Down Expand Up @@ -5266,6 +5283,39 @@ func TestFollowExec(t *testing.T) {
})
}

func TestFollowExecNonGo(t *testing.T) {
skipOn(t, "follow exec not implemented on freebsd", "freebsd")
skipOn(t, "follow exec not implemented on macOS", "darwin")
skipOn(t, "non-Go process handling not implemented on Windows", "windows")
withTestProcessArgs("nongochild/", t, "../../_fixtures/nongochild/", []string{}, 0, func(p *proc.Target, grp *proc.TargetGroup, fixture protest.Fixture) {
assertNoError(grp.FollowExec(true, ""), t, "FollowExec")

// Collect events
var events []*proc.Event
grp.SetEventsFn(func(e *proc.Event) { events = append(events, e) })

// Execute the process
for {
t.Log("Continuing")
err := grp.Continue()
if errors.As(err, &proc.ErrProcessExited{}) {
break
}
}

// Verify that a child was spawned
var spawned *proc.ProcessSpawnedEventDetails
for _, event := range events {
if event.Kind == proc.EventProcessSpawned {
spawned = event.ProcessSpawnedEventDetails
}
}
if spawned == nil {
t.Fatal("Did not log an event for the child")
}
})
}

func TestEscapeCheckUnreadable(t *testing.T) {
// A failure in escapeCheck to dereference a field should not cause
// infinite recursion. See issue #3310.
Expand Down
13 changes: 11 additions & 2 deletions pkg/proc/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ func (pe ErrProcessExited) Error() string {
return fmt.Sprintf("Process %d has exited with status %d", pe.Pid, pe.Status)
}

// ErrBadBinaryInfo indicates that loading binary info for the process failed.
type ErrBadBinaryInfo struct {
Err error
}

func (e *ErrBadBinaryInfo) Error() string {
return e.Err.Error()
}

// StopReason describes the reason why the target process is stopped.
// A process could be stopped for multiple simultaneous reasons, in which
// case only one will be reported.
Expand Down Expand Up @@ -169,11 +178,11 @@ func (grp *TargetGroup) newTarget(p ProcessInternal, pid int, currentThread Thre

err = p.BinInfo().LoadBinaryInfo(path, entryPoint, grp.cfg.DebugInfoDirs)
if err != nil {
return nil, err
return nil, &ErrBadBinaryInfo{Err: err}
}
for _, image := range p.BinInfo().Images {
if image.loadErr != nil {
return nil, image.loadErr
return nil, &ErrBadBinaryInfo{Err: image.loadErr}
}
}

Expand Down
40 changes: 39 additions & 1 deletion pkg/proc/target_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,40 @@ func Restart(grp, oldgrp *TargetGroup, discard func(*LogicalBreakpoint, error))
func (grp *TargetGroup) addTarget(p ProcessInternal, pid int, currentThread Thread, path string, stopReason StopReason, cmdline string) (*Target, error) {
logger := logflags.DebuggerLogger()
if len(grp.targets) > 0 {
willFollow := true
if !grp.followExecEnabled {
logger.Debugf("Detaching from child target (follow-exec disabled) %d %q", pid, cmdline)
return nil, nil
willFollow = false
}
if grp.followExecRegex != nil && !grp.followExecRegex.MatchString(cmdline) {
logger.Debugf("Detaching from child target (follow-exec regex not matched) %d %q", pid, cmdline)
willFollow = false
}

// Notify listeners that a child process was spawned.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you are doing this regardless just do it before the two previous ifs.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to include willFollow in the process spawned event. When I add the DAP start debugging request (which I plan to do in a separate PR), that probably needs to be conditional. If delve did not follow/attach to a child process, sending a start debugging request for that child probably won't work.

//
// We do this regardless of whether the process is debuggable and
// regardless of whether follow-exec is enabled; when run by an IDE in
// DAP mode, we want to give the IDE a chance to handle the spawned
// process.
//
// Defer the call so that - if the process _can_ be debugged - listeners
// are notified after the new target is set up.
if grp.Selected != nil {
if fn := grp.Selected.BinInfo().eventsFn; fn != nil {
defer fn(&Event{
Kind: EventProcessSpawned,
ProcessSpawnedEventDetails: &ProcessSpawnedEventDetails{
PID: pid,
ThreadID: currentThread.ThreadID(),
Cmdline: cmdline,
WillFollow: willFollow,
},
})
}
}

if !willFollow {
return nil, nil
}
}
Expand Down Expand Up @@ -576,6 +604,7 @@ type Event struct {
Kind EventKind
*BinaryInfoDownloadEventDetails
*BreakpointMaterializedEventDetails
*ProcessSpawnedEventDetails
}

type EventKind uint8
Expand All @@ -585,6 +614,7 @@ const (
EventStopped
EventBinaryInfoDownload
EventBreakpointMaterialized
EventProcessSpawned
)

// BinaryInfoDownloadEventDetails describes the details of a BinaryInfoDownloadEvent
Expand All @@ -596,3 +626,11 @@ type BinaryInfoDownloadEventDetails struct {
type BreakpointMaterializedEventDetails struct {
Breakpoint *LogicalBreakpoint
}

// ProcessSpawnedEventDetails describes the details of a ProcessSpawnedEvent
type ProcessSpawnedEventDetails struct {
PID int
ThreadID int
Cmdline string
WillFollow bool
}
2 changes: 2 additions & 0 deletions pkg/terminal/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ func New(client service.Client, conf *config.Config) *Term {
}

fmt.Fprintf(t.stdout, "Breakpoint %d materialized at %s:%d%s\n", bp.ID, file, bp.Line, extra)
case api.EventProcessSpawned:
fmt.Fprintf(t.stdout, "Spawned new process '%s' (%d)\n", event.Cmdline, event.ProcessSpawnedEventDetails.PID)
}
})
}
Expand Down
9 changes: 9 additions & 0 deletions service/api/conversions.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,5 +479,14 @@ func ConvertEvent(event *proc.Event) *Event {
}
}

if event.ProcessSpawnedEventDetails != nil {
r.ProcessSpawnedEventDetails = &ProcessSpawnedEventDetails{
PID: event.ProcessSpawnedEventDetails.PID,
ThreadID: event.ProcessSpawnedEventDetails.ThreadID,
Cmdline: event.ProcessSpawnedEventDetails.Cmdline,
WillFollow: event.ProcessSpawnedEventDetails.WillFollow,
}
}

return r
}
10 changes: 10 additions & 0 deletions service/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,7 @@ type Event struct {
Kind EventKind
*BinaryInfoDownloadEventDetails
*BreakpointMaterializedEventDetails
*ProcessSpawnedEventDetails
}

type EventKind uint8
Expand All @@ -703,6 +704,7 @@ const (
EventStopped
EventBinaryInfoDownload
EventBreakpointMaterialized
EventProcessSpawned
)

// BinaryInfoDownloadEventDetails describes the details of a BinaryInfoDownloadEvent
Expand All @@ -714,3 +716,11 @@ type BinaryInfoDownloadEventDetails struct {
type BreakpointMaterializedEventDetails struct {
Breakpoint *Breakpoint
}

// ProcessSpawnedEventDetails describes the details of a ProcessSpawnedEvent
type ProcessSpawnedEventDetails struct {
PID int
ThreadID int
Cmdline string
WillFollow bool
}
9 changes: 9 additions & 0 deletions service/dap/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4102,6 +4102,15 @@ func (s *Session) convertDebuggerEvent(event *proc.Event) {
},
},
})
case proc.EventProcessSpawned:
s.send(&dap.ProcessEvent{
Event: *newEvent("process"),
Body: dap.ProcessEventBody{
Name: event.Cmdline,
SystemProcessId: event.PID,
IsLocalProcess: true,
},
})
}
}

Expand Down
Loading