fix: kill the whole process tree on Windows spawn-test timeout (#13)
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 20s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 8s
CI / Lint (ruff) (pull_request) Successful in 7s
CI / Tests (py3.10 / ubuntu-latest) (pull_request) Successful in 8s
CI / Tests (py3.12 / windows-latest) (pull_request) Successful in 20s
CI / Tests (py3.12 / ubuntu-latest) (pull_request) Successful in 9s
CI / Tests (py3.13 / ubuntu-latest) (pull_request) Successful in 8s
Popen.kill() only terminated the direct child, so runner-style commands (npx -> node -> server) leaked the real server process on every Windows spawn test. taskkill /PID <pid> /T /F walks the descendant tree. Also sets CREATE_NO_WINDOW on the spawned test process so the windowed exe doesn't flash a console per test. Closes #13 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+22
-1
@@ -1581,6 +1581,23 @@ def server_log_path(name: str) -> Path | None:
|
||||
_STDERR_CAP = 4096 # bytes
|
||||
|
||||
|
||||
def _kill_process_tree_windows(pid: int) -> None:
|
||||
"""
|
||||
Kill `pid` and its whole descendant tree via `taskkill /T /F` (issue #13).
|
||||
|
||||
Popen.kill() only terminates the direct child; runner-style commands
|
||||
(npx → node → server, cmd → real process) leave the actual server alive,
|
||||
leaking a process on every Windows spawn test. taskkill walks the tree.
|
||||
"""
|
||||
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0) # no console flash from the GUI exe
|
||||
with contextlib.suppress(OSError):
|
||||
subprocess.run(
|
||||
["taskkill", "/PID", str(pid), "/T", "/F"],
|
||||
capture_output=True,
|
||||
creationflags=flags,
|
||||
)
|
||||
|
||||
|
||||
def spawn_test(data: dict, timeout: float = 3.0) -> dict:
|
||||
"""
|
||||
Attempt to start a stdio server and observe it for `timeout` seconds.
|
||||
@@ -1675,6 +1692,10 @@ def _spawn_test_impl(data: dict, timeout: float) -> dict:
|
||||
)
|
||||
if os.name != "nt":
|
||||
popen_kwargs["start_new_session"] = True # own process group → clean kill
|
||||
else:
|
||||
# The packaged app is windowed (console=False); without this every
|
||||
# spawn test of a console server flashes a console window.
|
||||
popen_kwargs["creationflags"] = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(args_list, **popen_kwargs)
|
||||
@@ -1696,7 +1717,7 @@ def _spawn_test_impl(data: dict, timeout: float) -> dict:
|
||||
if os.name != "nt":
|
||||
os.killpg(os.getpgid(proc.pid), _signal.SIGKILL)
|
||||
else:
|
||||
proc.kill() # best-effort on Windows
|
||||
_kill_process_tree_windows(proc.pid)
|
||||
except OSError:
|
||||
pass
|
||||
with contextlib.suppress(subprocess.TimeoutExpired):
|
||||
|
||||
Reference in New Issue
Block a user