fix: kill entire CLI process tree on stop/forceStop (Windows) - #2073
fix: kill entire CLI process tree on stop/forceStop (Windows)#2073rinceyuan wants to merge 1 commit into
Conversation
|
@microsoft-github-policy-service agree company=Microsoft |
|
@stephentoub This fixes the Windows process tree leak reported in #1804. Affects both Node.js and Python SDKs — each stop()/forceStop() cycle was orphaning the CLI's child processes. The fix uses \ askkill /T\ on Windows. Manually verified on Windows 11. Happy to add the Go/.NET fixes in a follow-up if desired. |
|
The way this is implemented in the PR currently won’t work because:
There is a small, coherent cross-language change we could accept: add one private “terminate owned runtime tree” operation per SDK, called from the existing owned-process termination point. Its behavior should be: POSIX also needs one small spawn-time change to place the runtime in its own process group/session. Otherwise group termination could kill the host. No public API is needed. Per language, this is approximately:
Each SDK should call that helper from the process-termination section used by This is about the smallest useful implementation across all languages and OSes:
The tests can also be narrow: start a helper process that starts one long-lived child, then verify both disappear after |
|
I'll move this back to draft, but please mark as ready to review if it later becomes ready. |
6fc9770 to
4364b34
Compare
|
@SteveSandersonMS Reworked per your feedback. Single commit, all 6 SDKs: Spawn-time isolation:
Teardown (private helpers, no public API):
Removed the public \processGroup\ option. External-server and in-process (FFI) paths are not affected. |
Add a private kill-process-tree helper to each SDK, called from the existing owned-process termination points in stop() and forceStop(). Spawn-time isolation (POSIX): - Node.js: detached: true - Python: start_new_session=True - Go: SysProcAttr.Setpgid = true - Rust: process_group(0) Teardown: - Windows (all): taskkill /T /F /PID - Node.js/Python/Go (POSIX): kill(-pid, SIGKILL) — process group signal - Rust (POSIX): libc::kill(-pid, SIGKILL) - Java: ProcessHandle.descendants() snapshot + destroyForcibly each - .NET: already uses Kill(entireProcessTree: true) — no change needed No public API changes. External-server and in-process (FFI) paths are not affected. Closes github#1804
4364b34 to
6ffcb43
Compare
|
@SteveSandersonMS Ready for re-review. All 5 points from your feedback are addressed — private helpers in all 6 SDKs, POSIX spawn isolation, no public API, guarded by isExternalServer. Also fixed a Rust compile issue (replaced libc::kill with kill command to avoid adding a new dependency). |
Summary
Fix process tree leak on Windows when
stop()/forceStop()terminates the CLI.On Windows,
ChildProcess.kill()(Node.js) andPopen.terminate()/kill()(Python) only terminate the immediate process viaTerminateProcess(), leaving grandchildren orphaned. Eachcreate_session()/stop()cycle leaks a full copilot process tree (~3-5 processes) that survives until idle timeout or OOM.Changes
client.ts)killProcessTree()helper: usestaskkill /T /F /PIDon Windows, falls back to standard.kill(signal)on Unixclient.py)_kill_process_tree()helper: usestaskkill /Ton Windows,os.killpg()on Unix, with fallback toproc.kill()Validation
tsc --noEmit)ast.parse)taskkill /Tkills both parent and grandchild (see test output below)parent=47632 child=27936 pre:parent alive pre:child alive taskkill OK post:parent DEAD(OK) post:child DEAD(OK)Notes
os.killpg()in Python (requires the child to be a process group leader); falls back gracefully if not applicable.Closes #1804