From a54ad6f2a62c8b4fccee757c02a714995fa30f8e Mon Sep 17 00:00:00 2001 From: Keuin Date: Mon, 5 Jun 2023 02:42:07 +0800 Subject: [PATCH 1/2] gh-105288: wake up exit waiters after sub-process exits --- Lib/asyncio/base_subprocess.py | 14 +++++++++----- .../2023-06-05-02-48-08.gh-issue-105288.Ewnyas.rst | 4 ++++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-06-05-02-48-08.gh-issue-105288.Ewnyas.rst diff --git a/Lib/asyncio/base_subprocess.py b/Lib/asyncio/base_subprocess.py index 4c9b0dd5653c0c..d64c31d535a7dd 100644 --- a/Lib/asyncio/base_subprocess.py +++ b/Lib/asyncio/base_subprocess.py @@ -217,6 +217,14 @@ def _process_exited(self, returncode): self._call(self._protocol.process_exited) self._try_finish() + self._wakeup_exit_waiters() + + def _wakeup_exit_waiters(self): + # wake up futures waiting for wait() + for waiter in self._exit_waiters: + if not waiter.cancelled(): + waiter.set_result(self._returncode) + self._exit_waiters = [] async def _wait(self): """Wait until the process exit and return the process return code. @@ -242,11 +250,7 @@ def _call_connection_lost(self, exc): try: self._protocol.connection_lost(exc) finally: - # wake up futures waiting for wait() - for waiter in self._exit_waiters: - if not waiter.cancelled(): - waiter.set_result(self._returncode) - self._exit_waiters = None + self._wakeup_exit_waiters() self._loop = None self._proc = None self._protocol = None diff --git a/Misc/NEWS.d/next/Library/2023-06-05-02-48-08.gh-issue-105288.Ewnyas.rst b/Misc/NEWS.d/next/Library/2023-06-05-02-48-08.gh-issue-105288.Ewnyas.rst new file mode 100644 index 00000000000000..2df476c9a4d8ac --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-06-05-02-48-08.gh-issue-105288.Ewnyas.rst @@ -0,0 +1,4 @@ +Exit waiters are not resolved after subprocess handled by :class:`asyncio.Task` +exits. This will causes a recent :func:`asyncio.wait` call hang forever if +there are objects that are not :class:`typing.Awaitable` yield from its first +parameter. From de7a41f1920b8e53e7208d0fe7ad8fa6693c88fb Mon Sep 17 00:00:00 2001 From: Keuin Date: Thu, 8 Jun 2023 01:50:59 +0800 Subject: [PATCH 2/2] gh-105288: add test case --- Lib/test/test_asyncio/test_subprocess.py | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Lib/test/test_asyncio/test_subprocess.py b/Lib/test/test_asyncio/test_subprocess.py index eeeca40c15cd28..52e183b08083ef 100644 --- a/Lib/test/test_asyncio/test_subprocess.py +++ b/Lib/test/test_asyncio/test_subprocess.py @@ -1,6 +1,8 @@ import os import signal import sys +import threading +import time import unittest import warnings from unittest import mock @@ -787,6 +789,35 @@ async def main() -> None: self.loop.run_until_complete(main()) + def test_subprocess_wait_not_hang_gh105288(self): + # See https://github.com/python/cpython/issues/105288 + TIMEOUT = 5 + finished = False + + async def whoami(): + proc = await subprocess.create_subprocess_exec('whoami') + await proc.communicate() + + async def main(): + t = asyncio.create_task(whoami()) + await asyncio.wait([0, t]) + + def _main(): + nonlocal finished + try: + # We have to run the event loop in a separate thread, + # to make sure we can fail in the main thread + # and successfully notify the test driver. + self.loop.run_until_complete(main()) + finally: + finished = True + + + threading.Thread(target=_main).start() + time.sleep(TIMEOUT) + if not finished: + self.fail('watchdog timeout, event loop is probably sleeping infinitely') + def test_subprocess_communicate_stdout(self): # See https://github.com/python/cpython/issues/100133 async def get_command_stdout(cmd, *args):