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):

878878

raise 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+881948

def _compute_returncode(status):

882949

if os.WIFSIGNALED(status):

883950

# The child process died because of a signal.