]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-141473: Fix subprocess.Popen.communicate to send input to stdin upon a subsequent...
authorArtur Jamro <artur.jamro@gmail.com>
Sat, 29 Nov 2025 02:04:52 +0000 (03:04 +0100)
committerGitHub <noreply@github.com>
Sat, 29 Nov 2025 02:04:52 +0000 (18:04 -0800)
* 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.

---------

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 1aade881745f21f83dbe852cc14d22341d10a1b7..43da804b62beb15aa41aba43ee964a3ac099e007 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 79251bd531022374144169b6f0acae6822d0b65d..4955f77b60381fc69432d08eab5bb26407e85e17 100644 (file)
@@ -2105,7 +2105,7 @@ class Popen:
                 input_view = memoryview(self._input)
 
             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 f0e350c71f60eaeca4df50c8357b6f19a0aac2fc..9792d7c8983cd3b25602851bf91437109e403213 100644 (file)
@@ -1643,6 +1643,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.assertStartsWith(stdout, "spam")
+            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.