fix(tools): harden spawn_local Windows job detachment - #42868
fix(tools): harden spawn_local Windows job detachment#42868VerbalChainsaw wants to merge 1 commit into
Conversation
|
cc @teknium1 — this is the agent-path half of the same fix you landed in #40909 for the gateway watcher. The agent's |
|
Positive verification — clean review, no issues found. Reviewed the full diff: the change correctly replaces The static source-text regression tests are a smart approach — they catch a revert of the helper call on any CI platform, not just Windows. The integration test with job-object setup is thorough and deterministic. One note: the test file docstring references |
7b6a5c3 to
b8377c3
Compare
|
Addressed the review nit (commit |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review: Comment. Large fix (490 lines) for Windows background subprocess detachment. Due to size and cross-platform nature, recommend thorough manual review before merge.
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing the local terminal(background=true) path; the premise is still present on current main: tools/process_registry.py:762-775 sends only windows_hide_flags() to its local Popen, and tools/terminal_tool.py:2360-2380 routes local background jobs through that method.
Problems
- The proposed use of
windows_detach_popen_kwargs()needs the established breakaway-denied fallback.hermes_cli/_subprocess_compat.py:138-144documents thatCREATE_BREAKAWAY_FROM_JOBcan raisePermissionError/OSError;hermes_cli/gateway.py:854-870catches that failure and retries withwindows_detach_flags_without_breakaway(). The PR diff adds no equivalent retry, so a restrictive parent job changes this path from a best-effort background launch into a launch failure.
Suggested changes
- Add the Windows-only retry and a mocked-Popen regression test for it.
- Consider replacing the two duplicate source-text checks with behavioral assertions on the kwargs passed to
Popen; the current tests unnecessarily constrain equivalent future refactors.
Automated hermes-sweeper review.
| # job object — the child then dies when the parent's job is | ||
| # torn down. Regression tests: | ||
| # test_process_spawn_local_detached.py::test_spawn_local_child_breaks_away_from_parent_job | ||
| # (load-bearing: deterministic, sub-second via IsProcessInJob) |
There was a problem hiding this comment.
windows_detach_popen_kwargs() includes CREATE_BREAKAWAY_FROM_JOB, which can raise OSError when the parent job disallows breakaway (hermes_cli/_subprocess_compat.py:138-144). Please mirror the retry-without-breakaway fallback already used by hermes_cli/gateway.py:854-870; otherwise this turns some Windows background launches into immediate failures.
spawn_local (the pipe-backed, non-PTY local background path) passed only windows_hide_flags() (CREATE_NO_WINDOW), so the child stayed inside the parent's Windows job object and was reaped when that job was torn down. The primary spawn now goes through windows_detach_popen_kwargs(), so the child breaks away from the owning job on Windows and gets start_new_session=True on POSIX (the session behavior main already had, now supplied by the helper, so the hard-coded kwarg is dropped and there is no duplicate). A restrictive job can reject CREATE_BREAKAWAY_FROM_JOB with OSError, so retry once with windows_detach_flags_without_breakaway(), mirroring gateway_windows._spawn_detached. POSIX errors and a Windows dual failure propagate, so spawn_local never registers a session that falsely claims a process started, and the command is never logged. The post-spawn setup-failure cleanup now tree-terminates on Windows too (via _terminate_host_pid), since the detached child owns descendants that a bare proc.kill() would orphan. Adds close_fds=True. Scope: the pipe-backed, non-PTY path only. use_pty=True uses pywinpty's separate spawn and is untouched.
b8377c3 to
0042653
Compare
|
Hardened the Windows job-detachment path and validated the final diff against current
|
What does this PR do?
Uses Windows job breakaway for the primary
spawn_localattempt so pipe-backed background children can survive owning-job teardown, while retaining a job-bound compatibility fallback when breakaway is denied.ProcessRegistry.spawn_local(the pipe-backed, non-PTY local path) passed onlywindows_hide_flags()(CREATE_NO_WINDOW), leaving the child inside the parent's job object.Change
windows_detach_popen_kwargs(). On Windows it breaks the child away from the owning job (CREATE_BREAKAWAY_FROM_JOBplus detached flags) so it survives that job's teardown. On POSIX it isstart_new_session=True(the session behavior currentmainalready had, now supplied by the helper, so the hard-coded kwarg is dropped and there is no duplicate).OSError), retry once withwindows_detach_flags_without_breakaway(). This preserves launch compatibility, but the child stays job-bound and cannot be guaranteed to survive the owning job's teardown.spawn_localnever registers a session that falsely claims a process started, and nothing (command, env, cwd) is logged.proc.kill()would orphan. Addsclose_fds=True.Scope: the pipe-backed, non-PTY path only.
use_pty=True(pywinpty) is untouched.Tests
Popenbehavioral suite: primary breakaway; breakaway-denied retry (argv and every non-creationflagskwarg preserved, and the flag sets differ by exactly the breakaway bit); dual-failure surfacing (the fallback's exception propagates and no session is registered); post-setup-failure tree-kill; POSIX single-session; POSIX error not retried.ctypes, no pywin32): assigns the test process to aBREAKAWAY_OKjob and asserts the realspawn_localchild is not a job member, with output capture verified. It passes on this branch and reproduces the failure on pristinemain.