https://github.com/python/cpython/commit/ef474cfafbdf3aa383fb1334a7ab95cef9834ced
commit: ef474cfafbdf3aa383fb1334a7ab95cef9834ced
branch: main
author: Kumar Aditya <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2025-11-12T10:47:38+05:30
summary:
gh-103847: fix cancellation safety of `asyncio.create_subprocess_exec` (#140805)
files:
A Misc/NEWS.d/next/Library/2025-10-31-13-57-55.gh-issue-103847.VM7TnW.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 d40af422e614c1..321a4e5d5d18fb 100644
--- a/Lib/asyncio/base_subprocess.py
+++ b/Lib/asyncio/base_subprocess.py
@@ -26,6 +26,7 @@ def __init__(self, loop, protocol, args, shell,
self._pending_calls = collections.deque()
self._pipes = {}
self._finished = False
+ self._pipes_connected = False
if stdin == subprocess.PIPE:
self._pipes[0] = None
@@ -213,6 +214,7 @@ async def _connect_pipes(self, waiter):
else:
if waiter is not None and not waiter.cancelled():
waiter.set_result(None)
+ self._pipes_connected = True
def _call(self, cb, *data):
if self._pending_calls is not None:
@@ -256,6 +258,15 @@ def _try_finish(self):
assert not self._finished
if self._returncode is None:
return
+ if not self._pipes_connected:
+ # self._pipes_connected can be False if not all pipes were
connected
+ # because either the process failed to start or the
self._connect_pipes task
+ # got cancelled. In this broken state we consider all pipes
disconnected and
+ # 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():
+ waiter.set_result(self._returncode)
if all(p is not None and p.disconnected
for p in self._pipes.values()):
self._finished = True
diff --git a/Lib/test/test_asyncio/test_subprocess.py
b/Lib/test/test_asyncio/test_subprocess.py
index 3a17c169c34f12..bf301740741ae7 100644
--- a/Lib/test/test_asyncio/test_subprocess.py
+++ b/Lib/test/test_asyncio/test_subprocess.py
@@ -11,7 +11,7 @@
from asyncio import subprocess
from test.test_asyncio import utils as test_utils
from test import support
-from test.support import os_helper
+from test.support import os_helper, warnings_helper, gc_collect
if not support.has_subprocess_support:
raise unittest.SkipTest("test module requires subprocess")
@@ -879,6 +879,44 @@ async def main():
self.loop.run_until_complete(main())
+ @warnings_helper.ignore_warnings(category=ResourceWarning)
+ def test_subprocess_read_pipe_cancelled(self):
+ async def main():
+ loop = asyncio.get_running_loop()
+ loop.connect_read_pipe =
mock.AsyncMock(side_effect=asyncio.CancelledError)
+ with self.assertRaises(asyncio.CancelledError):
+ await asyncio.create_subprocess_exec(*PROGRAM_BLOCKED,
stderr=asyncio.subprocess.PIPE)
+
+ asyncio.run(main())
+ gc_collect()
+
+ @warnings_helper.ignore_warnings(category=ResourceWarning)
+ def test_subprocess_write_pipe_cancelled(self):
+ async def main():
+ loop = asyncio.get_running_loop()
+ loop.connect_write_pipe =
mock.AsyncMock(side_effect=asyncio.CancelledError)
+ with self.assertRaises(asyncio.CancelledError):
+ await asyncio.create_subprocess_exec(*PROGRAM_BLOCKED,
stdin=asyncio.subprocess.PIPE)
+
+ asyncio.run(main())
+ gc_collect()
+
+ @warnings_helper.ignore_warnings(category=ResourceWarning)
+ def test_subprocess_read_write_pipe_cancelled(self):
+ async def main():
+ loop = asyncio.get_running_loop()
+ loop.connect_read_pipe =
mock.AsyncMock(side_effect=asyncio.CancelledError)
+ loop.connect_write_pipe =
mock.AsyncMock(side_effect=asyncio.CancelledError)
+ with self.assertRaises(asyncio.CancelledError):
+ await asyncio.create_subprocess_exec(
+ *PROGRAM_BLOCKED,
+ stdin=asyncio.subprocess.PIPE,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ )
+
+ asyncio.run(main())
+ gc_collect()
if sys.platform != 'win32':
# Unix
diff --git
a/Misc/NEWS.d/next/Library/2025-10-31-13-57-55.gh-issue-103847.VM7TnW.rst
b/Misc/NEWS.d/next/Library/2025-10-31-13-57-55.gh-issue-103847.VM7TnW.rst
new file mode 100644
index 00000000000000..e14af7d97083d6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-10-31-13-57-55.gh-issue-103847.VM7TnW.rst
@@ -0,0 +1 @@
+Fix hang when cancelling process created by
:func:`asyncio.create_subprocess_exec` or
:func:`asyncio.create_subprocess_shell`. Patch by Kumar Aditya.
_______________________________________________
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]