Skip to content

Commit 3d8b224

Browse files
graingert1st1JelleZijlstra
authored
GH-94182: Run the PidfdChildWatcher on the running loop (#94184)
There is no reason for this watcher to be attached to any particular loop. This should make it safe to use regardless of the lifetime of the event loop running in the main thread (relative to other loops). Co-authored-by: Yury Selivanov <[email protected]> Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 27ce45d commit 3d8b224

File tree

3 files changed

+57
-42
lines changed

3 files changed

+57
-42
lines changed

Lib/asyncio/unix_events.py

Lines changed: 12 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -912,46 +912,29 @@ class PidfdChildWatcher(AbstractChildWatcher):
912912
recent (5.3+) kernels.
913913
"""
914914

915-
def __init__(self):
916-
self._loop = None
917-
self._callbacks = {}
918-
919915
def __enter__(self):
920916
return self
921917

922918
def __exit__(self, exc_type, exc_value, exc_traceback):
923919
pass
924920

925921
def is_active(self):
926-
return self._loop is not None and self._loop.is_running()
922+
return True
927923

928924
def close(self):
929-
self.attach_loop(None)
925+
pass
930926

931927
def attach_loop(self, loop):
932-
if self._loop is not None and loop is None and self._callbacks:
933-
warnings.warn(
934-
'A loop is being detached '
935-
'from a child watcher with pending handlers',
936-
RuntimeWarning)
937-
for pidfd, _, _ in self._callbacks.values():
938-
self._loop._remove_reader(pidfd)
939-
os.close(pidfd)
940-
self._callbacks.clear()
941-
self._loop = loop
928+
pass
942929

943930
def add_child_handler(self, pid, callback, *args):
944-
existing = self._callbacks.get(pid)
945-
if existing is not None:
946-
self._callbacks[pid] = existing[0], callback, args
947-
else:
948-
pidfd = os.pidfd_open(pid)
949-
self._loop._add_reader(pidfd, self._do_wait, pid)
950-
self._callbacks[pid] = pidfd, callback, args
931+
loop = events.get_running_loop()
932+
pidfd = os.pidfd_open(pid)
933+
loop._add_reader(pidfd, self._do_wait, pid, pidfd, callback, args)
951934

952-
def _do_wait(self, pid):
953-
pidfd, callback, args = self._callbacks.pop(pid)
954-
self._loop._remove_reader(pidfd)
935+
def _do_wait(self, pid, pidfd, callback, args):
936+
loop = events.get_running_loop()
937+
loop._remove_reader(pidfd)
955938
try:
956939
_, status = os.waitpid(pid, 0)
957940
except ChildProcessError:
@@ -969,12 +952,9 @@ def _do_wait(self, pid):
969952
callback(pid, returncode, *args)
970953

971954
def remove_child_handler(self, pid):
972-
try:
973-
pidfd, _, _ = self._callbacks.pop(pid)
974-
except KeyError:
975-
return False
976-
self._loop._remove_reader(pidfd)
977-
os.close(pidfd)
955+
# asyncio never calls remove_child_handler() !!!
956+
# The method is no-op but is implemented because
957+
# abstract base classes require it.
978958
return True
979959

980960

Lib/test/test_asyncio/test_subprocess.py

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import sys
55
import unittest
66
import warnings
7+
import functools
78
from unittest import mock
89

910
import asyncio
@@ -30,6 +31,19 @@
3031
'sys.stdout.buffer.write(data)'))]
3132

3233

34+
@functools.cache
35+
def _has_pidfd_support():
36+
if not hasattr(os, 'pidfd_open'):
37+
return False
38+
39+
try:
40+
os.close(os.pidfd_open(os.getpid()))
41+
except OSError:
42+
return False
43+
44+
return True
45+
46+
3347
def tearDownModule():
3448
asyncio.set_event_loop_policy(None)
3549

@@ -708,17 +722,8 @@ class SubprocessFastWatcherTests(SubprocessWatcherMixin,
708722

709723
Watcher = unix_events.FastChildWatcher
710724

711-
def has_pidfd_support():
712-
if not hasattr(os, 'pidfd_open'):
713-
return False
714-
try:
715-
os.close(os.pidfd_open(os.getpid()))
716-
except OSError:
717-
return False
718-
return True
719-
720725
@unittest.skipUnless(
721-
has_pidfd_support(),
726+
_has_pidfd_support(),
722727
"operating system does not support pidfds",
723728
)
724729
class SubprocessPidfdWatcherTests(SubprocessWatcherMixin,
@@ -751,6 +756,35 @@ async def execute():
751756
mock.call.__exit__(RuntimeError, mock.ANY, mock.ANY),
752757
])
753758

759+
760+
@unittest.skipUnless(
761+
_has_pidfd_support(),
762+
"operating system does not support pidfds",
763+
)
764+
def test_create_subprocess_with_pidfd(self):
765+
async def in_thread():
766+
proc = await asyncio.create_subprocess_exec(
767+
*PROGRAM_CAT,
768+
stdin=subprocess.PIPE,
769+
stdout=subprocess.PIPE,
770+
)
771+
stdout, stderr = await proc.communicate(b"some data")
772+
return proc.returncode, stdout
773+
774+
async def main():
775+
# asyncio.Runner did not call asyncio.set_event_loop()
776+
with self.assertRaises(RuntimeError):
777+
asyncio.get_event_loop_policy().get_event_loop()
778+
return await asyncio.to_thread(asyncio.run, in_thread())
779+
780+
asyncio.set_child_watcher(asyncio.PidfdChildWatcher())
781+
try:
782+
with asyncio.Runner(loop_factory=asyncio.new_event_loop) as runner:
783+
returncode, stdout = runner.run(main())
784+
self.assertEqual(returncode, 0)
785+
self.assertEqual(stdout, b'some data')
786+
finally:
787+
asyncio.set_child_watcher(None)
754788
else:
755789
# Windows
756790
class SubprocessProactorTests(SubprocessMixin, test_utils.TestCase):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
run the :class:`asyncio.PidfdChildWatcher` on the running loop, this allows event loops to run subprocesses when there is no default event loop running on the main thread

0 commit comments

Comments
 (0)