"""Stop one isolated runner session, including orphaned Mineflayer children.""" from __future__ import annotations import os import signal import subprocess import psutil def stop_runner(proc: subprocess.Popen | None, timeout: float = 10) -> None: """Only pass children launched with start_new_session=True on POSIX.""" if proc is None: return members = [] if os.name == "posix": for child in psutil.process_iter(["pid"]): try: if os.getpgid(child.pid) == proc.pid: members.append(child) except (ProcessLookupError, PermissionError): pass try: os.killpg(proc.pid, signal.SIGTERM) except ProcessLookupError: return elif proc.poll() is None: proc.terminate() try: proc.wait(timeout=timeout) except subprocess.TimeoutExpired: if os.name != "posix": proc.kill() finally: # The session leader may have exited through os._exit(75), leaving its # Node child holding port 3000/3001. Its group still belongs to this # runner, even when proc.poll() reports an exit. if os.name == "posix": try: os.killpg(proc.pid, signal.SIGKILL) except ProcessLookupError: pass # Sending a signal is asynchronous. Wait for those exact session # members so the next runner cannot race their socket cleanup. psutil.wait_procs(members, timeout=2) try: proc.wait(timeout=5) except subprocess.TimeoutExpired: pass