Fix _communicate_streams_windows to avoid blocking with large input

Move stdin writing to a background thread in _communicate_streams_windows
to avoid blocking indefinitely when writing large input to a pipeline
where the subprocess doesn't consume stdin quickly.

This mirrors the fix made to Popen._communicate() for Windows in
commit 5b1862b (gh-87512).

Add test_pipeline_timeout_large_input to verify that TimeoutExpired
is raised promptly when run_pipeline() is called with large input
and a timeout, even when the first process is slow to consume stdin.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gregory P. Smith using claude.ai/code 2025-11-29 08:41:25 +00:00
parent 9f53a8e883
commit d420f29e2b
No known key found for this signature in database
2 changed files with 86 additions and 18 deletions

View file

@ -2298,6 +2298,39 @@ def test_pipeline_large_data_with_stderr(self):
self.assertGreater(len(result.stderr), stderr_size)
self.assertEqual(result.returncodes, [0, 0])
def test_pipeline_timeout_large_input(self):
"""Test that timeout is enforced with large input to a slow pipeline.
This verifies that run_pipeline() doesn't block indefinitely when
writing large input to a pipeline where the first process is slow
to consume stdin. The timeout should be enforced promptly.
This is particularly important on Windows where stdin writing could
block without proper threading.
"""
# Input larger than typical pipe buffer (64KB)
input_data = 'x' * (128 * 1024)
start = time.monotonic()
with self.assertRaises(subprocess.TimeoutExpired):
subprocess.run_pipeline(
# First process sleeps before reading - simulates slow consumer
[sys.executable, '-c',
'import sys, time; time.sleep(30); print(sys.stdin.read())'],
[sys.executable, '-c',
'import sys; print(len(sys.stdin.read()))'],
input=input_data, capture_output=True, text=True, timeout=0.5
)
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 ~0.5s. "
"Input writing may have blocked without checking timeout.")
def _get_test_grp_name():
for name_group in ('staff', 'nogroup', 'grp', 'nobody', 'nfsnobody'):