mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
gh-87512: Fix subprocess using timeout= on Windows blocking with a large input= (GH-142058)
On Windows, Popen._communicate() previously wrote to stdin synchronously, which could block indefinitely if the subprocess didn't consume input= quickly and the pipe buffer filled up. The timeout= parameter was only checked when joining the reader threads, not during the stdin write. This change moves the Windows stdin writing to a background thread (similar to how stdout/stderr are read in threads), allowing the timeout to be properly enforced. If timeout expires, TimeoutExpired is raised promptly and the writer thread continues in the background. Subsequent calls to communicate() will join the existing writer thread. Adds test_communicate_timeout_large_input to verify that TimeoutExpired is raised promptly when communicate() is called with large input and a timeout, even when the subprocess doesn't consume stdin quickly. This test already passed on POSIX (where select() is used) but failed on Windows where the stdin write blocks without checking the timeout. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
923056b2d4
commit
5b1862bdd8
3 changed files with 82 additions and 2 deletions
|
|
@ -1034,6 +1034,62 @@ def test_communicate_timeout_large_output(self):
|
|||
(stdout, _) = p.communicate()
|
||||
self.assertEqual(len(stdout), 4 * 64 * 1024)
|
||||
|
||||
def test_communicate_timeout_large_input(self):
|
||||
# Test that timeout is enforced when writing large input to a
|
||||
# slow-to-read subprocess, and that partial input is preserved
|
||||
# for continuation after timeout (gh-141473).
|
||||
#
|
||||
# This is a regression test for Windows matching POSIX behavior.
|
||||
# On POSIX, select() is used to multiplex I/O with timeout checking.
|
||||
# On Windows, stdin writing must also honor the timeout rather than
|
||||
# blocking indefinitely when the pipe buffer fills.
|
||||
|
||||
# Input larger than typical pipe buffer (4-64KB on Windows)
|
||||
input_data = b"x" * (128 * 1024)
|
||||
|
||||
p = subprocess.Popen(
|
||||
[sys.executable, "-c",
|
||||
"import sys, time; "
|
||||
"time.sleep(30); " # Don't read stdin for a long time
|
||||
"sys.stdout.buffer.write(sys.stdin.buffer.read())"],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
|
||||
try:
|
||||
timeout = 0.2
|
||||
start = time.monotonic()
|
||||
try:
|
||||
p.communicate(input_data, timeout=timeout)
|
||||
# If we get here without TimeoutExpired, the timeout was ignored
|
||||
elapsed = time.monotonic() - start
|
||||
self.fail(
|
||||
f"TimeoutExpired not raised. communicate() completed in "
|
||||
f"{elapsed:.2f}s, but subprocess sleeps for 30s. "
|
||||
"Stdin writing blocked without enforcing timeout.")
|
||||
except subprocess.TimeoutExpired:
|
||||
elapsed = time.monotonic() - start
|
||||
|
||||
# Timeout should occur close to the specified timeout value,
|
||||
# not after waiting for the subprocess to finish sleeping.
|
||||
# Allow generous margin for slow CI, but must be well under
|
||||
# the subprocess sleep time.
|
||||
self.assertLess(elapsed, 5.0,
|
||||
f"TimeoutExpired raised after {elapsed:.2f}s; expected ~{timeout}s. "
|
||||
"Stdin writing blocked without checking timeout.")
|
||||
|
||||
# After timeout, continue communication. The remaining input
|
||||
# should be sent and we should receive all data back.
|
||||
stdout, stderr = p.communicate()
|
||||
|
||||
# Verify all input was eventually received by the subprocess
|
||||
self.assertEqual(len(stdout), len(input_data),
|
||||
f"Expected {len(input_data)} bytes output but got {len(stdout)}")
|
||||
self.assertEqual(stdout, input_data)
|
||||
finally:
|
||||
p.kill()
|
||||
p.wait()
|
||||
|
||||
# Test for the fd leak reported in http://bugs.python.org/issue2791.
|
||||
def test_communicate_pipe_fd_leak(self):
|
||||
for stdin_pipe in (False, True):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue