https://github.com/python/cpython/commit/1564e231aae7afad5b9b19a277d1efff2b840ad2
commit: 1564e231aae7afad5b9b19a277d1efff2b840ad2
branch: main
author: Daan De Meyer <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-03-09T19:37:23+05:30
summary:
gh-145541: Fix `InvalidStateError` in
`BaseSubprocessTransport._call_connection_lost()` (#145554)
files:
A Misc/NEWS.d/next/Library/2026-03-05-19-01-28.gh-issue-145551.gItPRl.rst
M Lib/asyncio/base_subprocess.py
M Lib/test/test_asyncio/test_subprocess.py
diff --git a/Lib/asyncio/base_subprocess.py b/Lib/asyncio/base_subprocess.py
index 321a4e5d5d18fb..224b1883808a41 100644
--- a/Lib/asyncio/base_subprocess.py
+++ b/Lib/asyncio/base_subprocess.py
@@ -265,7 +265,7 @@ def _try_finish(self):
# 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 @@ def _call_connection_lost(self, exc):
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
diff --git a/Lib/test/test_asyncio/test_subprocess.py
b/Lib/test/test_asyncio/test_subprocess.py
index bf301740741ae7..c08eb7cf261568 100644
--- a/Lib/test/test_asyncio/test_subprocess.py
+++ b/Lib/test/test_asyncio/test_subprocess.py
@@ -111,6 +111,37 @@ def test_subprocess_repr(self):
)
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
index 00000000000000..15b70d734ca3b9
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-03-05-19-01-28.gh-issue-145551.gItPRl.rst
@@ -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.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]