closes bpo-38692: Add a pidfd child process watcher to asyncio. (GH-1… · python/cpython@3ccdd9b
@@ -878,6 +878,73 @@ def __exit__(self, a, b, c):
878878raise NotImplementedError()
879879880880881+class PidfdChildWatcher(AbstractChildWatcher):
882+"""Child watcher implementation using Linux's pid file descriptors.
883+884+ This child watcher polls process file descriptors (pidfds) to await child
885+ process termination. In some respects, PidfdChildWatcher is a "Goldilocks"
886+ child watcher implementation. It doesn't require signals or threads, doesn't
887+ interfere with any processes launched outside the event loop, and scales
888+ linearly with the number of subprocesses launched by the event loop. The
889+ main disadvantage is that pidfds are specific to Linux, and only work on
890+ recent (5.3+) kernels.
891+ """
892+893+def __init__(self):
894+self._loop = None
895+self._callbacks = {}
896+897+def __enter__(self):
898+return self
899+900+def __exit__(self, exc_type, exc_value, exc_traceback):
901+pass
902+903+def is_active(self):
904+return self._loop is not None and self._loop.is_running()
905+906+def close(self):
907+self.attach_loop(None)
908+909+def attach_loop(self, loop):
910+if self._loop is not None and loop is None and self._callbacks:
911+warnings.warn(
912+'A loop is being detached '
913+'from a child watcher with pending handlers',
914+RuntimeWarning)
915+for pidfd, _, _ in self._callbacks.values():
916+self._loop._remove_reader(pidfd)
917+os.close(pidfd)
918+self._callbacks.clear()
919+self._loop = loop
920+921+def add_child_handler(self, pid, callback, *args):
922+existing = self._callbacks.get(pid)
923+if existing is not None:
924+self._callbacks[pid] = existing[0], callback, args
925+else:
926+pidfd = os.pidfd_open(pid)
927+self._loop._add_reader(pidfd, self._do_wait, pid)
928+self._callbacks[pid] = pidfd, callback, args
929+930+def _do_wait(self, pid):
931+pidfd, callback, args = self._callbacks.pop(pid)
932+self._loop._remove_reader(pidfd)
933+_, status = os.waitpid(pid, 0)
934+os.close(pidfd)
935+returncode = _compute_returncode(status)
936+callback(pid, returncode, *args)
937+938+def remove_child_handler(self, pid):
939+try:
940+pidfd, _, _ = self._callbacks.pop(pid)
941+except KeyError:
942+return False
943+self._loop._remove_reader(pidfd)
944+os.close(pidfd)
945+return True
946+947+881948def _compute_returncode(status):
882949if os.WIFSIGNALED(status):
883950# The child process died because of a signal.