Skip to content

Commit dbb885b

Browse files
committed
Decouple the job-reap test's child from the server's stdin pipe
The grandchild was freezing at interpreter startup for as long as the server sat in its blocking stdin read: CPython queries fd 0 during startup, the child's inherited stdin was the server's stdin pipe (a synchronous file object), and Windows serializes that query behind the server's pending ReadFile. The child only thawed when the client closed stdin at shutdown - at which point the job-handle close correctly reaped it before it could connect. Spawning the child with stdin=DEVNULL removes the coupling, so it boots immediately and the kill-on-job-close contract can be observed. Also reverts the previous commit's base-interpreter bypass: the venv launcher was not involved (the freeze reproduces identically with the base interpreter). The same stdin coupling applies to any Windows stdio server that spawns Python children without redirecting their stdin.
1 parent 1dc0f35 commit dbb885b

1 file changed

Lines changed: 11 additions & 11 deletions

File tree

tests/transports/stdio/test_windows.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -82,20 +82,20 @@ async def test_a_gracefully_exited_servers_child_is_reaped_when_the_job_handle_c
8282
# still cold-starting — long before it can Popen the child (job membership
8383
# is inherited at CreateProcess, never acquired retroactively).
8484
#
85-
# The child is spawned through the base interpreter, not `sys.executable`:
86-
# in launcher-wrapped venvs (uv's `python.exe` is a trampoline that runs
87-
# the real interpreter inside its own Job machinery) the extra launcher
88-
# layer proved fatal to grandchildren on CI runners — they booted and then
89-
# died tracelessly inside the launcher's private job. The contract under
90-
# test is unchanged: the child still inherits the SDK's Job at
91-
# CreateProcess. After stdin EOF ends the server, it reports the child's
92-
# `poll()` status — `None` means the child was alive when the server
93-
# exited; an exit or NTSTATUS code names whatever killed it.
85+
# The child's stdin must be decoupled from the server's (DEVNULL): CPython
86+
# startup queries fd 0, the server's stdin is a synchronous pipe object,
87+
# and Windows serializes that query behind the server's pending blocking
88+
# `sys.stdin.read()` — with an inherited stdin the child freezes at
89+
# interpreter startup until the next inbound byte or EOF. (The same goes
90+
# for any Windows stdio server spawning Python children without
91+
# redirecting their stdin.) After stdin EOF ends the server, it reports
92+
# the child's `poll()` status — `None` means the child was alive when the
93+
# server exited; an exit or NTSTATUS code names whatever killed it.
9494
server = (
9595
f"import socket, subprocess, sys\n"
96-
f"exe = getattr(sys, '_base_executable', None) or sys.executable\n"
9796
f"try:\n"
98-
f" p = subprocess.Popen([exe, '-c', {child!r}], stderr=sys.stderr)\n"
97+
f" p = subprocess.Popen([sys.executable, '-c', {child!r}], "
98+
f"stdin=subprocess.DEVNULL, stderr=sys.stderr)\n"
9999
f"except BaseException as exc:\n"
100100
f" print(exc, file=sys.stderr, flush=True)\n"
101101
f" raise\n"

0 commit comments

Comments
 (0)