Repository navigation
Feature request: subProcessAttach: false — track child processes and emit DAP events without injecting the adapter #2034
Description
Summary
Add a subProcessAttach flag (default true) that, when set to false, decouples subprocess tracking from adapter injection. With subProcess: true, subProcessAttach: false, debugpy tracks child processes, emits debugpyAttach events, and forwards stdout/stderr as DAP output events but does not inject pydevd, does not suspend the child.
This collapses two long-standing pain points into a single orthogonal flag:
- No way to be notified of child spawns without triggering a full attach (and all the breakage that comes with it in
spawn-mode multiprocessing). - No way to receive child stdout/stderr as DAP
outputevents without full adapter injection.
Current behavior the forced binary choice
subProcess is currently all-or-nothing:
subProcess |
effect |
|---|---|
true |
track + inject pydevd + suspend child waiting for client |
false |
silence everything, no events, no output, no visibility |
There is no middle ground. For spawn-mode multiprocessing (PyTorch, mp.spawn, custom plugin/trial runners), subProcess: true breaks the child process entirely:
- The semaphore tracker helper (
from multiprocessing.semaphore_tracker import main) is intercepted first and reported as the subprocess to attach the actual worker is never reached (ptvsd#1887, still skipped in the current test suite). SemLockobjects cannot be pickled across thespawnboundary after debugpy patches__main__imports (ptvsd#2108).- Any custom IPC layer (pipes,
mp.Pipe, shared-memory tensors) breaks because the child is suspended waiting for a DAP client the parent never intends to connect.
The only escape is subProcess: false, which makes child output completely invisible to the DAP layer. The only workaround we have found is intercepting raw PTY bytes via the VSCode extension API:
vscode.window.onDidWriteTerminalData(e => {
collectOutput(e.data); // no PID, no stdout/stderr distinction, VSCode-only
});This is unacceptable as a permanent solution: it is unstructured, conflates all processes sharing the terminal, provides no process identity, and is unavailable to non-VSCode DAP clients.
Proposed solution
Split the single subProcess boolean into two independent concerns:
// launch.json
{
"subProcess": true, // keep tracking children (existing flag)
"subProcessAttach": false // new: don't inject pydevd, don't suspend
}Or programmatically:
debugpy.configure(subprocess=True, subprocess_attach=False)Behavior matrix:
subProcess |
subProcessAttach |
result |
|---|---|---|
false |
--- | existing: silence everything |
true |
true (default) |
existing: track + inject + suspend |
true |
false |
new: track + debugpyAttach event + output events, no injection, no suspension |
With subProcessAttach: false, the adapter should:
- Detect child spawns via the existing pydevd subprocess hook.
- Emit a
debugpyAttachevent (existing extension to DAP) so the client is notified and can optionally attach manually. - Forward child stdout/stderr as DAP
outputevents tagged withsource.pid(already in the DAP spec'sOutputEvent). - Not inject pydevd into the child.
- Not suspend the child.
The child runs freely. The parent's IPC layer is unaffected. A DAP client that wants to attach can still do so on receiving the debugpyAttach event.
Why this covers both missing features
Spawn-mode notification without breakage:
The debugpyAttach event already carries a fully-formed attach config with the child PID. With subProcessAttach: false, this event is emitted but no automatic attach follows, the client decides what to do. This is the "notify but don't attach" mode that spawn-mode frameworks need.
Child output in the DAP layer:
With the adapter already aware of the child (via the subprocess hook), forwarding its stdout/stderr as output events requires only an OS-level pipe before the child's Python runtime initializes — no pydevd, no tracing overhead. This replaces onDidWriteTerminalData with a proper, PID-tagged, per-stream DAP event visible to all clients.
Prior art / related issues
- ptvsd#1887 (2019) spawn + semaphore tracker false-positive; test still skipped on Linux.
- ptvsd#2108 (2020)
__main__unpickling bug with spawn. - ptvsd#1658 debug console missing output from multiprocessing children; workaround was
"console": "integratedTerminal"(i.e. fall back to PTY). - ptvsd#1585 output dropped when child exits before adapter flushes
outputevents. - ptvsd#1659 partial fix: fd-level capture for
launchwhen children share parent fds; never extended tospawn. - debugpy#42 subprocess hangs suspended when parent PID can't be resolved.
- debugpy#81 subprocess always waits for client; only escape is
subProcess: false. - debugpy#264 parent hangs when
stdout=PIPEand forked child is being debugged. - debugpy#1717 debugpy does not send
outputevents of categorystdoutto non-VSCode DAP clients in some configurations.
Who this helps
Any setup using spawn start method that needs child visibility without full adapter injection:
- PyTorch distributed /
mp.spawn - Custom ML trial runners, plugin systems, hyperparameter search frameworks
multiprocessing.Pool/ProcessPoolExecutorwithspawnon Linux/macOS- Non-VSCode DAP clients (Neovim/nvim-dap, JetBrains, CLI) that have no access to
onDidWriteTerminalData
Checklist
- This is not a duplicate. no existing issue proposes decoupling tracking from injection via a
subProcessAttachflag. -
subProcess: trueis not a viable workaround (breaks spawn-mode IPC). -
subProcess: falseis not a viable workaround (no events, no output visibility). -
onDidWriteTerminalDatais not a viable workaround (no DAP, no PID, VSCode-only).
Activity
[-]Feature request: subProcess: "notify". Emit event on child spawn without auto-attaching[/-][+]Feature request: subProcessAttach: false — track child processes and emit DAP events without injecting the adapter[/+]