Skip to content

Commit ef474cf

Browse files
gh-103847: fix cancellation safety of asyncio.create_subprocess_exec (#140805)
1 parent fbebca2 commit ef474cf

File tree

3 files changed

+51
-1
lines changed

3 files changed

+51
-1
lines changed

Lib/asyncio/base_subprocess.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def __init__(self, loop, protocol, args, shell,
2626
self._pending_calls = collections.deque()
2727
self._pipes = {}
2828
self._finished = False
29+
self._pipes_connected = False
2930

3031
if stdin == subprocess.PIPE:
3132
self._pipes[0] = None
@@ -213,6 +214,7 @@ async def _connect_pipes(self, waiter):
213214
else:
214215
if waiter is not None and not waiter.cancelled():
215216
waiter.set_result(None)
217+
self._pipes_connected = True
216218

217219
def _call(self, cb, *data):
218220
if self._pending_calls is not None:
@@ -256,6 +258,15 @@ def _try_finish(self):
256258
assert not self._finished
257259
if self._returncode is None:
258260
return
261+
if not self._pipes_connected:
262+
# self._pipes_connected can be False if not all pipes were connected
263+
# because either the process failed to start or the self._connect_pipes task
264+
# got cancelled. In this broken state we consider all pipes disconnected and
265+
# to avoid hanging forever in self._wait as otherwise _exit_waiters
266+
# would never be woken up, we wake them up here.
267+
for waiter in self._exit_waiters:
268+
if not waiter.cancelled():
269+
waiter.set_result(self._returncode)
259270
if all(p is not None and p.disconnected
260271
for p in self._pipes.values()):
261272
self._finished = True

Lib/test/test_asyncio/test_subprocess.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from asyncio import subprocess
1212
from test.test_asyncio import utils as test_utils
1313
from test import support
14-
from test.support import os_helper
14+
from test.support import os_helper, warnings_helper, gc_collect
1515

1616
if not support.has_subprocess_support:
1717
raise unittest.SkipTest("test module requires subprocess")
@@ -879,6 +879,44 @@ async def main():
879879

880880
self.loop.run_until_complete(main())
881881

882+
@warnings_helper.ignore_warnings(category=ResourceWarning)
883+
def test_subprocess_read_pipe_cancelled(self):
884+
async def main():
885+
loop = asyncio.get_running_loop()
886+
loop.connect_read_pipe = mock.AsyncMock(side_effect=asyncio.CancelledError)
887+
with self.assertRaises(asyncio.CancelledError):
888+
await asyncio.create_subprocess_exec(*PROGRAM_BLOCKED, stderr=asyncio.subprocess.PIPE)
889+
890+
asyncio.run(main())
891+
gc_collect()
892+
893+
@warnings_helper.ignore_warnings(category=ResourceWarning)
894+
def test_subprocess_write_pipe_cancelled(self):
895+
async def main():
896+
loop = asyncio.get_running_loop()
897+
loop.connect_write_pipe = mock.AsyncMock(side_effect=asyncio.CancelledError)
898+
with self.assertRaises(asyncio.CancelledError):
899+
await asyncio.create_subprocess_exec(*PROGRAM_BLOCKED, stdin=asyncio.subprocess.PIPE)
900+
901+
asyncio.run(main())
902+
gc_collect()
903+
904+
@warnings_helper.ignore_warnings(category=ResourceWarning)
905+
def test_subprocess_read_write_pipe_cancelled(self):
906+
async def main():
907+
loop = asyncio.get_running_loop()
908+
loop.connect_read_pipe = mock.AsyncMock(side_effect=asyncio.CancelledError)
909+
loop.connect_write_pipe = mock.AsyncMock(side_effect=asyncio.CancelledError)
910+
with self.assertRaises(asyncio.CancelledError):
911+
await asyncio.create_subprocess_exec(
912+
*PROGRAM_BLOCKED,
913+
stdin=asyncio.subprocess.PIPE,
914+
stdout=asyncio.subprocess.PIPE,
915+
stderr=asyncio.subprocess.PIPE,
916+
)
917+
918+
asyncio.run(main())
919+
gc_collect()
882920

883921
if sys.platform != 'win32':
884922
# Unix
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix hang when cancelling process created by :func:`asyncio.create_subprocess_exec` or :func:`asyncio.create_subprocess_shell`. Patch by Kumar Aditya.

0 commit comments

Comments
 (0)