]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-141473: Fix subprocess.Popen.communicate to send input to stdin upon a...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Sat, 29 Nov 2025 06:53:00 +0000 (07:53 +0100)
committerGitHub <noreply@github.com>
Sat, 29 Nov 2025 06:53:00 +0000 (06:53 +0000)
* 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 526d7a8bb47bd8ff58c829c30384cd70cc5d0747)

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>
Doc/library/subprocess.rst
Lib/subprocess.py
Lib/test/test_subprocess.py
Misc/NEWS.d/next/Library/2025-11-27-20-16-38.gh-issue-141473.Wq4xVN.rst [new file with mode: 0644]

index 4390f00fe7cd5590e2274d4a12714fe7b1fbdeaf..45dfd9ed025813709cc4f0c48e5f0dd374f9bd95 100644 (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
    :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
    cleanup properly a well-behaved application should kill the child process and
index 0f308489efc71177b626530f53cfbe85dbccf67f..08a0335d413b241ec02f9274bf0b87d81b7cf204 100644 (file)
@@ -2111,7 +2111,7 @@ class Popen:
                     input_view = self._input.cast("b")  # byte input required
 
             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)
                 if self.stdout and not self.stdout.closed:
                     selector.register(self.stdout, selectors.EVENT_READ)
index d14c4546347c3bc6666c158089c4418dca452f0b..25cc105ccb0d42e6b0ed9747051655b11040e10e 100644 (file)
@@ -1684,6 +1684,40 @@ class ProcessTestCase(BaseTestCase):
 
             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):
     def run_python(self, code, **kwargs):
diff --git a/Misc/NEWS.d/next/Library/2025-11-27-20-16-38.gh-issue-141473.Wq4xVN.rst b/Misc/NEWS.d/next/Library/2025-11-27-20-16-38.gh-issue-141473.Wq4xVN.rst
new file mode 100644 (file)
index 0000000..f6aa592
--- /dev/null
@@ -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.