[3.13] gh-141473: Fix subprocess.Popen.communicate to send input to stdin upon a subsequent post-timeout call (GH-141477) (#142060)

* gh-141473: Fix subprocess.Popen.communicate to send input to stdin upon a subsequent post-timeout call (GH-141477)

* gh-141473: Fix subprocess.Popen.communicate to send input to stdin
* Docs: Clarify that `input` is one time only on `communicate()`
* NEWS entry
* Add a regression test.

---------
(cherry picked from commit 526d7a8bb4)

Co-authored-by: Artur Jamro <artur.jamro@gmail.com>
Co-authored-by: Gregory P. Smith <greg@krypto.org>

* no assertStartsWith

---------

Co-authored-by: Artur Jamro <artur.jamro@gmail.com>
Co-authored-by: Gregory P. Smith <greg@krypto.org>
This commit is contained in:
Miss Islington (bot) 2025-11-29 07:53:00 +01:00 committed by GitHub
parent 704bb69bd8
commit 2f3024f066
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 42 additions and 2 deletions

View file

@ -831,7 +831,9 @@ Instances of the :class:`Popen` class have the following methods:
If the process does not terminate after *timeout* seconds, a If the process does not terminate after *timeout* seconds, a
:exc:`TimeoutExpired` exception will be raised. Catching this exception and :exc:`TimeoutExpired` exception will be raised. Catching this exception and
retrying communication will not lose any output. retrying communication will not lose any output. Supplying *input* to a
subsequent post-timeout :meth:`communicate` call is in undefined behavior
and may become an error in the future.
The child process is not killed if the timeout expires, so in order to The child process is not killed if the timeout expires, so in order to
cleanup properly a well-behaved application should kill the child process and cleanup properly a well-behaved application should kill the child process and

View file

@ -2111,7 +2111,7 @@ def _communicate(self, input, endtime, orig_timeout):
input_view = self._input.cast("b") # byte input required input_view = self._input.cast("b") # byte input required
with _PopenSelector() as selector: with _PopenSelector() as selector:
if self.stdin and input: if self.stdin and not self.stdin.closed and self._input:
selector.register(self.stdin, selectors.EVENT_WRITE) selector.register(self.stdin, selectors.EVENT_WRITE)
if self.stdout and not self.stdout.closed: if self.stdout and not self.stdout.closed:
selector.register(self.stdout, selectors.EVENT_READ) selector.register(self.stdout, selectors.EVENT_READ)

View file

@ -1684,6 +1684,40 @@ def test_wait_negative_timeout(self):
self.assertEqual(proc.wait(), 0) self.assertEqual(proc.wait(), 0)
def test_post_timeout_communicate_sends_input(self):
"""GH-141473 regression test; the stdin pipe must close"""
with subprocess.Popen(
[sys.executable, "-uc", """\
import sys
while c := sys.stdin.read(512):
sys.stdout.write(c)
print()
"""],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
) as proc:
try:
data = f"spam{'#'*4096}beans"
proc.communicate(
input=data,
timeout=0,
)
except subprocess.TimeoutExpired as exc:
pass
# Prior to the bugfix, this would hang as the stdin
# pipe to the child had not been closed.
try:
stdout, stderr = proc.communicate(timeout=15)
except subprocess.TimeoutExpired as exc:
self.fail("communicate() hung waiting on child process that should have seen its stdin pipe close and exit")
self.assertEqual(
proc.returncode, 0,
msg=f"STDERR:\n{stderr}\nSTDOUT:\n{stdout}")
self.assertTrue(stdout.startswith("spam"), msg=stdout)
self.assertIn("beans", stdout)
class RunFuncTestCase(BaseTestCase): class RunFuncTestCase(BaseTestCase):
def run_python(self, code, **kwargs): def run_python(self, code, **kwargs):

View file

@ -0,0 +1,4 @@
When :meth:`subprocess.Popen.communicate` was called with *input* and a
*timeout* and is called for a second time after a
:exc:`~subprocess.TimeoutExpired` exception before the process has died, it
should no longer hang.