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
output events 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).
SemLock objects cannot be pickled across the spawn boundary 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:
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
debugpyAttach event (existing extension to DAP) so the client is notified and can optionally attach manually.
- Forward child stdout/stderr as DAP
output events tagged with source.pid (already in the DAP spec's OutputEvent).
- 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
output events.
- ptvsd#1659 partial fix: fd-level capture for
launch when children share parent fds; never extended to spawn.
- 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=PIPE and forked child is being debugged.
- debugpy#1717 debugpy does not send
output events of category stdout to 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 / ProcessPoolExecutor with spawn on Linux/macOS
- Non-VSCode DAP clients (Neovim/nvim-dap, JetBrains, CLI) that have no access to
onDidWriteTerminalData
Checklist
Summary
Add a
subProcessAttachflag (defaulttrue) that, when set tofalse, decouples subprocess tracking from adapter injection. WithsubProcess: true, subProcessAttach: false, debugpy tracks child processes, emitsdebugpyAttachevents, and forwards stdout/stderr as DAPoutputevents but does not inject pydevd, does not suspend the child.This collapses two long-standing pain points into a single orthogonal flag:
spawn-mode multiprocessing).outputevents without full adapter injection.Current behavior the forced binary choice
subProcessis currently all-or-nothing:subProcesstruefalseThere is no middle ground. For
spawn-mode multiprocessing (PyTorch,mp.spawn, custom plugin/trial runners),subProcess: truebreaks the child process entirely: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).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: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
subProcessboolean into two independent concerns:Or programmatically:
Behavior matrix:
subProcesssubProcessAttachfalsetruetrue(default)truefalsedebugpyAttachevent +outputevents, no injection, no suspensionWith
subProcessAttach: false, the adapter should:debugpyAttachevent (existing extension to DAP) so the client is notified and can optionally attach manually.outputevents tagged withsource.pid(already in the DAP spec'sOutputEvent).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
debugpyAttachevent.Why this covers both missing features
Spawn-mode notification without breakage:
The
debugpyAttachevent already carries a fully-formed attach config with the child PID. WithsubProcessAttach: 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
outputevents requires only an OS-level pipe before the child's Python runtime initializes — no pydevd, no tracing overhead. This replacesonDidWriteTerminalDatawith a proper, PID-tagged, per-stream DAP event visible to all clients.Prior art / related issues
__main__unpickling bug with spawn."console": "integratedTerminal"(i.e. fall back to PTY).outputevents.launchwhen children share parent fds; never extended tospawn.subProcess: false.stdout=PIPEand forked child is being debugged.outputevents of categorystdoutto non-VSCode DAP clients in some configurations.Who this helps
Any setup using
spawnstart method that needs child visibility without full adapter injection:mp.spawnmultiprocessing.Pool/ProcessPoolExecutorwithspawnon Linux/macOSonDidWriteTerminalDataChecklist
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).