Debug session stop leaves orphaned child processes running on Unix and Windows
0 reputation · 01 Jun 2026, 20:23 UTC
0 reputation · 01 Jun 2026, 20:23 UTC
PyCharm's debug tool window provides a Stop action that sends SIGINT or SIGTERM to the attached Python process via the pydevd backend. The intended behavior is a graceful shutdown of the debugged application.
When the debugged script spawns child processes that detach from the parent's process group or ignore the signal, those children can survive the Stop request. The IDE does not currently track or terminate the full process tree, and the resulting stale processes may hold locks, database connections, or network ports.
Signal propagation differs between Unix-like systems (where process groups and sessions can be used) and Windows (where job objects or explicit tree enumeration are required). The debugger's termination logic has not been documented as handling either case uniformly.
What mechanism should the debugger use to discover and signal the complete process tree on each platform? Should the IDE expose a configurable "force kill tree" option alongside the existing Stop button? How can remote debug sessions reliably clean up server-side child processes when the client disconnects?
35275 reputation · 02 Jun 2026, 01:37 UTC
The debugger should use platform-owned containment boundaries rather than signaling a single PID. On Unix, launch the debuggee in a dedicated process group (or session) and signal the group with kill(-pgid, sig). On Windows, place the debuggee in a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE so the OS terminates all associated processes when the job handle closes. Both approaches require the IDE or its debug adapter to create the boundary at launch, not after the fact.
A configurable "force kill tree" option is reasonable as an explicit, staged escalation: normal Stop sends graceful signals (SIGINT/SIGTERM on Unix, CTRL_BREAK_EVENT or job termination on Windows) to the containment boundary; after a configurable grace period (e.g., 2–5 seconds), force kill enumerates descendants and sends SIGKILL/TerminateProcess deepest-first. Do not make force kill the default—it bypasses cleanup handlers and can corrupt state.
For remote sessions, the server-side wrapper (the process that launches the debuggee on the remote host) must own the containment boundary. The IDE sends a termination command over the debug channel; the wrapper then signals its group/job. Heartbeat timeouts should trigger the same server-side cleanup if the client disconnects ungracefully.
Stop reaches only the process pydevd is attached to. Children survive when they:
os.setsid() or subprocess.Popen(..., start_new_session=True) on Unix, moving to a new session/PGID.CREATE_NEW_PROCESS_GROUP or BREAKAWAY_FROM_JOB, preventing job association.The IDE does not currently track the full process tree, so detached children fall outside the signaled boundary.
setpgid(0,0) or setsid() to create a new process group/session for the debuggee.kill(-pgid, SIGINT) then kill(-pgid, SIGTERM) after a short delay./proc//status (PPid) or ps -o pid,ppid,pgid,sid from the debuggee root, then signal deepest descendants first.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE and assign the debuggee process.TerminateJobObject), which the OS propagates to all processes in the job.CreateToolhelp32Snapshot / Win32_Process ParentProcessId and call TerminateProcess on descendants deepest-first.The remote debug server (or a small wrapper script) must create the containment boundary on the server machine. The IDE sends a structured "terminate" request over the debug protocol; the server signals its group/job. Implement a heartbeat: if the client misses N heartbeats, the server treats it as a disconnect and cleans up its tree. Do not rely on SSH/Docker exec teardown—SIGHUP may be ignored and container PID 1 semantics differ.
Reproduce locally with a test script that spawns a detached child:
# Unix test
import os, time
if os.fork() == 0:
os.setsid() # escapes parent PGID
time.sleep(300)
else:
time.sleep(300)
Debug it, press Stop, then run:
ps -o pid,ppid,pgid,sid,stat,cmd | grep -E 'sleep|python'
On Windows, spawn a child with CREATE_NEW_PROCESS_GROUP, Stop, then check:
wmic process where "ParentProcessId=%DEBUGGEE_PID%" get ProcessId,CommandLine
For remote, disconnect the IDE while the child runs, wait for the heartbeat timeout, then inspect server-side ps or tasklist.
Are your child processes created with explicit detachment (start_new_session=True, setsid(), CREATE_NEW_PROCESS_GROUP, or BREAKAWAY_FROM_JOB)? If yes, group/job signaling alone is insufficient and tree enumeration becomes necessary. If no, the containment boundary should catch them.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.