""" Child process reaper (Linux/Unix). When third-party libraries spawn subprocesses and do not `wait()` them properly, the exited children become zombie processes () until the parent reaps. This module installs a SIGCHLD handler that reaps all exited children with os.waitpid(-1, WNOHANG) to prevent zombie accumulation in long-running services. """ from __future__ import annotations import errno import os import signal from typing import Callable, Optional, Union _InstalledHandler = Union[int, Callable[[int, object], None], None] def install_child_reaper(log: Optional[Callable[[str], None]] = None) -> bool: """ Install a SIGCHLD handler to reap zombie processes. Returns True if a handler was installed, otherwise False. Safe to call multiple times; it will simply re-install the handler. """ # Windows has no SIGCHLD; only do this on POSIX. if os.name != "posix": return False if not hasattr(signal, "SIGCHLD"): return False try: old_handler: _InstalledHandler = signal.getsignal(signal.SIGCHLD) except Exception: old_handler = None def _log(msg: str) -> None: if log: try: log(msg) except Exception: pass def _reap_all_children() -> None: # Reap all already-exited child processes (non-blocking). while True: try: pid, _status = os.waitpid(-1, os.WNOHANG) except ChildProcessError: # No child processes. return except OSError as e: if e.errno == errno.ECHILD: return # Any other error: stop to avoid spinning. _log(f"[CHILD-REAPER] waitpid failed: {e}") return if pid == 0: return def _handler(signum: int, frame) -> None: # 1) First reap everything we can to prevent zombies. _reap_all_children() # 2) Chain previous handler (if it was a Python callable). try: if callable(old_handler): old_handler(signum, frame) except Exception: # Never let exceptions escape a signal handler. pass try: signal.signal(signal.SIGCHLD, _handler) return True except Exception as e: _log(f"[CHILD-REAPER] failed to install SIGCHLD handler: {e}") return False