]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-145541: Fix `InvalidStateError` in `BaseSubprocessTransport._call_connection_lost...
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Mon, 9 Mar 2026 14:07:23 +0000 (15:07 +0100)
committerGitHub <noreply@github.com>
Mon, 9 Mar 2026 14:07:23 +0000 (19:37 +0530)
Lib/asyncio/base_subprocess.py
Lib/test/test_asyncio/test_subprocess.py
Misc/NEWS.d/next/Library/2026-03-05-19-01-28.gh-issue-145551.gItPRl.rst [new file with mode: 0644]

index 321a4e5d5d18fb102b0c2439352bedb6c30db93e..224b1883808a412eee585c05d5ae4a996104680a 100644 (file)
@@ -265,7 +265,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
             # to avoid hanging forever in self._wait as otherwise _exit_waiters
             # would never be woken up, we wake them up here.
             for waiter in self._exit_waiters:
-                if not waiter.cancelled():
+                if not waiter.done():
                     waiter.set_result(self._returncode)
         if all(p is not None and p.disconnected
                for p in self._pipes.values()):
@@ -278,7 +278,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport):
         finally:
             # wake up futures waiting for wait()
             for waiter in self._exit_waiters:
-                if not waiter.cancelled():
+                if not waiter.done():
                     waiter.set_result(self._returncode)
             self._exit_waiters = None
             self._loop = None
index bf301740741ae7577fc1a2ad488a1ce927588927..c08eb7cf2615680903744d3253b3521cec560c5e 100644 (file)
@@ -111,6 +111,37 @@ class SubprocessTransportTests(test_utils.TestCase):
         )
         transport.close()
 
+    def test_proc_exited_no_invalid_state_error_on_exit_waiters(self):
+        # gh-145541: when _connect_pipes hasn't completed (so
+        # _pipes_connected is False) and the process exits, _try_finish()
+        # sets the result on exit waiters. Then _call_connection_lost() must
+        # not call set_result() again on the same waiters.
+        self.loop.set_exception_handler(
+            lambda loop, context: self.fail(
+                f"unexpected exception: {context}")
+        )
+        waiter = self.loop.create_future()
+        transport, protocol = self.create_transport(waiter)
+
+        # Simulate a waiter registered via _wait() before the process exits.
+        exit_waiter = self.loop.create_future()
+        transport._exit_waiters.append(exit_waiter)
+
+        # _connect_pipes hasn't completed, so _pipes_connected is False.
+        self.assertFalse(transport._pipes_connected)
+
+        # Simulate process exit. _try_finish() will set the result on
+        # exit_waiter because _pipes_connected is False, and then schedule
+        # _call_connection_lost() because _pipes is empty (vacuously all
+        # disconnected). _call_connection_lost() must skip exit_waiter
+        # because it's already done.
+        transport._process_exited(6)
+        self.loop.run_until_complete(waiter)
+
+        self.assertEqual(exit_waiter.result(), 6)
+
+        transport.close()
+
 
 class SubprocessMixin:
 
diff --git a/Misc/NEWS.d/next/Library/2026-03-05-19-01-28.gh-issue-145551.gItPRl.rst b/Misc/NEWS.d/next/Library/2026-03-05-19-01-28.gh-issue-145551.gItPRl.rst
new file mode 100644 (file)
index 0000000..15b70d7
--- /dev/null
@@ -0,0 +1 @@
+Fix InvalidStateError when cancelling process created by :func:`asyncio.create_subprocess_exec` or :func:`asyncio.create_subprocess_shell`. Patch by Daan De Meyer.