diff --git a/hermes_cli/dashboard_procs.py b/hermes_cli/dashboard_procs.py index f0bce19595..8f04b8a802 100644 --- a/hermes_cli/dashboard_procs.py +++ b/hermes_cli/dashboard_procs.py @@ -1,8 +1,7 @@ """Dashboard process-hygiene helpers — extracted from ``hermes_cli/main.py``. -Helpers that STAY in ``hermes_cli.main`` are reached through the lazy ``_m()`` -reference so monkeypatches on ``hermes_cli.main.`` keep working and -imports stay one-way (main.py imports this module, never the reverse). +Helpers that STAY in ``hermes_cli.main`` are reached through the lazy ``_m()`` reference so +monkeypatches on ``hermes_cli.main.`` keep working and imports stay one-way. """ import os @@ -10,14 +9,15 @@ import subprocess import sys from pathlib import Path -# Cmdline substrings identifying the long-lived server. ``hermes serve`` is the -# same server under the headless name the desktop app spawns; it is reaped on -# update for the same frontend/backend-mismatch reason as ``dashboard``. +# Cmdline substrings identifying the long-lived server. ``hermes serve`` is the same server +# under the headless name the desktop app spawns; it is reaped on update for the same +# frontend/backend-mismatch reason as ``dashboard``. _DASHBOARD_PATTERNS = tuple( f"{launcher} {cmd}" for cmd in ("dashboard", "serve") for launcher in ("hermes", "hermes_cli.main", "hermes_cli/main.py") ) +_PS_RUN_KWARGS = dict(capture_output=True, text=True, encoding="utf-8", errors="replace") def _m(): @@ -34,13 +34,11 @@ def _empty_result() -> dict[str, list]: def _scan_dashboard_processes(*, exclude_pids: set[int] | None = None) -> list[tuple[int, str]]: """Return matching ``dashboard``/``serve`` processes with their cmdlines. - ``hermes update`` swaps files on disk while a forgotten dashboard keeps the - old Python backend in memory against the new JS bundle — a silent mismatch - (new auth headers → every API call 401s). *exclude_pids* must never be - returned: Hermes Desktop sets ``HERMES_DESKTOP_CHILD_PID`` on the backend - it spawns so an auto-update never kills the backend it manages itself. - - Returns an empty list on any scan error (missing ps/wmic, timeout, etc.). + ``hermes update`` swaps files on disk while a forgotten dashboard keeps the old Python + backend in memory against the new JS bundle — a silent mismatch (new auth headers → every + API call 401s). *exclude_pids* must never be returned: Hermes Desktop sets + ``HERMES_DESKTOP_CHILD_PID`` on the backend it spawns so an auto-update never kills the + backend it manages itself. Empty list on any scan error (missing ps/wmic, timeout, ...). """ self_pid = os.getpid() found: list[tuple[int, str]] = [] @@ -51,11 +49,10 @@ def _scan_dashboard_processes(*, exclude_pids: set[int] | None = None) -> list[t try: if sys.platform == "win32": - # errors="ignore": wmic may emit the system code page; a decode - # error would leave stdout=None. bounded_probe_run (not run()): - # run()'s post-timeout cleanup joins pipe readers unbounded and a - # conhost descendant holding duplicated handles wedges it forever. - # It also passes CREATE_NO_WINDOW for the pythonw.exe backend. + # errors="ignore": wmic may emit the system code page; a decode error would leave + # stdout=None. bounded_probe_run (not run()): run()'s post-timeout cleanup joins + # pipe readers unbounded and a conhost descendant holding duplicated handles + # wedges it forever. It also passes CREATE_NO_WINDOW for the pythonw.exe backend. from hermes_cli._subprocess_compat import bounded_probe_run result = bounded_probe_run( @@ -77,12 +74,9 @@ def _scan_dashboard_processes(*, exclude_pids: set[int] | None = None) -> list[t pass else: # ps (not `pgrep -f "hermes.*dashboard"`) keeps us consistent with - # gateway._scan_gateway_pids and avoids a greedy regex matching - # unrelated cmdlines that merely contain both words. - result = subprocess.run( - ["ps", "-A", "-o", "pid=,command="], - capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=10, - ) + # gateway._scan_gateway_pids and avoids a greedy regex matching unrelated + # cmdlines that merely contain both words. + result = subprocess.run(["ps", "-A", "-o", "pid=,command="], timeout=10, **_PS_RUN_KWARGS) if result.returncode == 0: for line in getattr(result, "stdout", "").split("\n"): parts = line.strip().split(None, 1) @@ -99,9 +93,9 @@ def _scan_dashboard_processes(*, exclude_pids: set[int] | None = None) -> list[t found = [proc for proc in found if proc[0] not in exclude_pids] # Spawn-ledger augmentation: substring patterns miss profiled launches - # (`hermes --profile p serve ...`). Every serve/dashboard registers itself - # in the spawn ledger with live-verified (pid, create_time) — positive - # identity. Add entries the scan missed, preferring the ledger's full argv. + # (`hermes --profile p serve ...`). Every serve/dashboard registers itself in the spawn + # ledger with live-verified (pid, create_time) — positive identity. Add entries the scan + # missed, preferring the ledger's full argv. try: from hermes_cli.process_identity import ledger_entries @@ -163,11 +157,10 @@ def _profile_flag_value(argv: list[str]) -> str | None: def _is_ephemeral_port_zero_backend(argv: list[str]) -> bool: """True for Desktop-style ``serve|dashboard --port 0`` backends. - Ephemeral-port backends are owned by Hermes Desktop (or are PPID-1 orphans - of a prior update respawn). Replaying them after ``hermes update`` - multiplies listening backends because ``--port 0`` always binds a fresh - port. Covers both ``serve`` and the legacy ``dashboard --no-open`` - fallback older Desktop runtimes use. + Ephemeral-port backends are owned by Hermes Desktop (or are PPID-1 orphans of a prior + update respawn). Replaying them after ``hermes update`` multiplies listening backends + because ``--port 0`` always binds a fresh port. Covers ``serve`` and the legacy + ``dashboard --no-open`` fallback older Desktop runtimes use. """ if _dashboard_subcommand_index(argv) is None: return False @@ -206,17 +199,16 @@ def _resolved_home(home: str) -> Path: def _normalized_home_for_compare(home: str) -> str: - """Install-identity key for *home*: symlinked / differently-spelled roots - compare equal (same normalization as ``home:`` respawn keys).""" + """Install-identity key for *home*: symlinked / differently-spelled roots compare equal.""" return os.path.normcase(str(_resolved_home(home))) def _profile_key_for_respawn(argv: list[str], hermes_home: str | None = None) -> str: """Stable owner key: ``HERMES_HOME`` when known, else ``--profile`` / ``-p``. - ``HERMES_HOME`` ending in ``profiles/`` → ``profile:`` so it - shares a cap with an explicit ``--profile``; other homes keep a resolved - ``home:`` key so unrelated installs never collapse together. + ``HERMES_HOME`` ending in ``profiles/`` → ``profile:`` so it shares a cap with + an explicit ``--profile``; other homes keep a resolved ``home:`` key so unrelated installs + never collapse together. """ if hermes_home: parts = _resolved_home(hermes_home).parts @@ -233,19 +225,18 @@ def _filter_dashboard_respawn_candidates( ) -> list[list[str]]: """Select which killed manual backends to respawn after ``hermes update``. - Candidates are ``(pid, argv, hermes_home)``; *own_home* (default - ``get_hermes_home()``) is a parameter so tests can pin it. Rules: - 1. Never resurrect Desktop ephemeral ``--port 0`` backends — Desktop owns - their lifecycle; they are the PPID-1 orphans that multiplied across updates. - 2. Never replay a backend from a **foreign** ``HERMES_HOME``: the respawn - is argv-only (no ``env=``), so it would come back on the *updating* - install's home and steal the foreign install's fixed port, leaving its - supervisor to crash-loop on ``EADDRINUSE``. Unreadable (``None``) stays eligible. + Candidates are ``(pid, argv, hermes_home)``; *own_home* (default ``get_hermes_home()``) + is a parameter so tests can pin it. Rules: + 1. Never resurrect Desktop ephemeral ``--port 0`` backends — Desktop owns their lifecycle. + 2. Never replay a backend from a **foreign** ``HERMES_HOME``: the respawn is argv-only + (no ``env=``), so it would come back on the *updating* install's home and steal the + foreign install's fixed port, leaving its supervisor to crash-loop on ``EADDRINUSE``. + Unreadable (``None``) stays eligible. 3. Dedupe by normalized cmdline. 4. At most one backend per profile / home. Does **not** blanket-skip PPID-1: a prior update respawn detaches with - ``start_new_session=True``, so fixed-port manual backends sit under init - and must stay eligible next update. + ``start_new_session=True``, so fixed-port manual backends sit under init and must stay + eligible next update. """ if own_home is None: try: @@ -277,11 +268,7 @@ def _filter_dashboard_respawn_candidates( def _exclude_pids_from_env() -> set[int]: - """PIDs Desktop marks as live backends (``HERMES_DESKTOP_CHILD_PID``). - - Desktop may manage several backends (one per active profile) and passes - them comma-separated; a lone int still parses for back-compat. - """ + """PIDs Desktop marks as live backends (``HERMES_DESKTOP_CHILD_PID``, comma-separated or a lone int).""" out: set[int] = set() for part in os.environ.get("HERMES_DESKTOP_CHILD_PID", "").split(","): part = part.strip() @@ -299,8 +286,8 @@ def _kill_pids_windows(pids: list[int], killed: list[int], failed: list[tuple[in from gateway.status import get_process_start_time from hermes_cli._subprocess_compat import pid_is_hermes, windows_hide_flags - # Capture identity immediately after discovery: a PID reused before the - # destructive action fails the start-time check. + # Capture identity immediately after discovery: a PID reused before the destructive + # action fails the start-time check. pid_start_times = {pid: get_process_start_time(pid) for pid in pids} for pid in pids: try: @@ -330,6 +317,8 @@ def _kill_pids_posix(pids: list[int], killed: list[int], failed: list[tuple[int, import signal as _signal import time as _time + from gateway.status import _pid_exists + def _send(pid: int, sig) -> None: try: os.kill(pid, sig) @@ -348,7 +337,6 @@ def _kill_pids_posix(pids: list[int], killed: list[int], failed: list[tuple[int, while pending and _time.monotonic() < deadline: _time.sleep(0.1) # os.kill(pid, 0) is NOT a no-op on Windows; use the portable check. - from gateway.status import _pid_exists alive = [p for p in pending if _pid_exists(p)] killed.extend(p for p in pending if p not in alive) pending = alive @@ -366,22 +354,20 @@ def _kill_stale_dashboard_processes( """Kill running ``hermes dashboard`` / ``hermes serve`` processes. Called at the end of ``hermes update`` (default ``reason``) and from - ``hermes dashboard --stop``; after an update the running process serves - stale Python against a fresh JS bundle. POSIX: SIGTERM, ~3s grace, SIGKILL - survivors. Windows: ``taskkill /F``. + ``hermes dashboard --stop``. POSIX: SIGTERM, ~3s grace, SIGKILL survivors. Windows: + ``taskkill /F``. - With ``restart_managed`` (update path only — ``--stop`` never restarts) a - detected ``hermes-dashboard.service`` is restarted through systemd, any - other killed PID owned by a systemd unit has that unit restarted after the - kill (systemd treats our SIGTERM as a clean stop, so ``Restart=on-failure`` - never fires), and manual PIDs are respawned from their captured argv. - *already_restarted_units* (no ``.service`` suffix) were restarted by the + With ``restart_managed`` (update path only — ``--stop`` never restarts) a detected + ``hermes-dashboard.service`` is restarted through systemd, any other killed PID owned by a + systemd unit has that unit restarted after the kill (systemd treats our SIGTERM as a clean + stop, so ``Restart=on-failure`` never fires), and manual PIDs are respawned from their + captured argv. *already_restarted_units* (no ``.service`` suffix) were restarted by the caller already; PIDs they own are left untouched, not killed twice. """ if restart_managed and _m()._restart_managed_dashboard_service(reason): - # The dashboard unit is handled but every OTHER backend is not (a host - # may also run hermes-serve.service hosting tui_gateway): record the - # unit as handled (the filter below drops PIDs it owns) and keep going. + # The dashboard unit is handled but every OTHER backend is not (a host may also run + # hermes-serve.service hosting tui_gateway): record the unit as handled (the filter + # below drops PIDs it owns) and keep going. _dash_unit = getattr(_m(), "_DASHBOARD_SYSTEMD_UNIT", "hermes-dashboard.service") already_restarted_units = set(already_restarted_units or ()) | { str(_dash_unit).removesuffix(".service") @@ -389,17 +375,17 @@ def _kill_stale_dashboard_processes( exclude = _exclude_pids_from_env() if restart_managed: - # An SSH-owned backend belongs to an attached Desktop client even when - # the updater runs from an unrelated shell; killing it strands that - # client's fixed SSH port-forward. Same ownership records as the reaper. + # An SSH-owned backend belongs to an attached Desktop client even when the updater + # runs from an unrelated shell; killing it strands that client's fixed SSH + # port-forward. Same ownership records as the reaper. exclude |= _lock_owned_serve_pids() pids = _m()._find_stale_dashboard_pids(exclude_pids=exclude or None) if not pids: return _empty_result() - # Snapshot systemd cgroup/unit and argv BEFORE killing (the cgroup - # disappears with the process). Linux + update path only. + # Snapshot systemd cgroup/unit and argv BEFORE killing (the cgroup disappears with the + # process). Linux + update path only. pid_cgroup: dict[int, str | None] = {} pid_service: dict[int, str | None] = {} pid_cmdline: dict[int, list[str]] = {} @@ -409,8 +395,8 @@ def _kill_stale_dashboard_processes( pid_cgroup[pid] = _m()._get_pid_cgroup_path(pid) pid_service[pid] = _m()._get_systemd_service_for_pid(pid) if not pid_service[pid]: - # Manual process: keep exact argv + HERMES_HOME for the - # post-update respawn and its per-profile cap. + # Manual process: keep exact argv + HERMES_HOME for the post-update respawn + # and its per-profile cap. cmdline = _m()._dashboard_cmdline_for_pid(pid) if cmdline: pid_cmdline[pid] = cmdline @@ -428,10 +414,7 @@ def _kill_stale_dashboard_processes( killed: list[int] = [] failed: list[tuple[int, str]] = [] - if sys.platform == "win32": - _kill_pids_windows(pids, killed, failed) - else: - _kill_pids_posix(pids, killed, failed) + (_kill_pids_windows if sys.platform == "win32" else _kill_pids_posix)(pids, killed, failed) for pid in killed: print(f" ✓ stopped PID {pid}") @@ -452,11 +435,10 @@ def _restart_killed_backends( killed: list[int], pid_service: dict[int, str | None], pid_cgroup: dict[int, str | None], pid_cmdline: dict[int, list[str]], pid_home: dict[int, str | None], ) -> list[int]: - """Update path: restart systemd-owned units, respawn manual argv. + """Update path: restart systemd-owned units, respawn manual argv. Returns PIDs not brought back. - Respawns are detached, headless, logged to logs/dashboard-restart.log; - Desktop ``--port 0`` backends are filtered out and duplicates collapse to - one per profile. Returns the PIDs that were not brought back. + Respawns are detached, headless, logged to logs/dashboard-restart.log; Desktop ``--port 0`` + backends are filtered out and duplicates collapse to one per profile. """ unrecovered: list[int] = [] failed_restarts: list[tuple[str, str]] = [] @@ -505,17 +487,16 @@ def _detect_concurrent_hermes_instances( ) -> list[tuple[int, str]]: """Find other live processes whose .exe is one of our entry-point shims. - Windows blocks DELETE/REPLACE on a running .exe (and RENAME when opened - without ``FILE_SHARE_DELETE``); Desktop spawns ``hermes.EXE`` as a backend - child, so the update's quarantine rename fails with ``[WinError 32]``. + Windows blocks DELETE/REPLACE on a running .exe (and RENAME when opened without + ``FILE_SHARE_DELETE``); Desktop spawns ``hermes.EXE`` as a backend child, so the update's + quarantine rename fails with ``[WinError 32]``. - Returns ``(pid, process_name)`` for processes whose ``exe`` matches a venv - shim (``hermes.exe`` / ``hermes-gateway.exe``). Excludes our own PID and - every *shim* ancestor: the setuptools launcher is a separate native process - from the ``python.exe`` it loads, so otherwise every update reports its own - launcher. ``proc.parents()`` (whole chain at once) because a per-hop loop - bailed on the first AccessDenied. Only shim ancestors are excluded so a - second hermes.exe under a non-Hermes parent (Desktop child) is still flagged. + Returns ``(pid, process_name)`` for processes whose ``exe`` matches a venv shim. Excludes + our own PID and every *shim* ancestor: the setuptools launcher is a separate native + process from the ``python.exe`` it loads, so otherwise every update reports its own + launcher. ``proc.parents()`` (whole chain at once) because a per-hop loop bailed on the + first AccessDenied. Only shim ancestors are excluded so a second hermes.exe under a + non-Hermes parent (Desktop child) is still flagged. Empty off-Windows, without psutil, or with no other instances. Never raises. """ @@ -568,10 +549,11 @@ def _detect_concurrent_hermes_instances( def _is_desktop_local_serve_cmdline(command: str) -> bool: - """True for the Desktop-local serve shape ``hermes serve [--isolated] - --host 127.0.0.1 --port 0``. Long-lived headless serves (``--host - --port 9119``) must never match — those are operator-managed - remote backends that legitimately run with ppid 1 under launchd/nohup.""" + """True for the Desktop-local serve shape ``hermes serve [--isolated] --host 127.0.0.1 --port 0``. + + Long-lived headless serves (``--host --port 9119``) must never match — + those are operator-managed remote backends that legitimately run with ppid 1. + """ cmd = command.lower() if "serve" not in cmd or ("hermes" not in cmd and "hermes_cli" not in cmd): return False @@ -584,15 +566,11 @@ def _is_desktop_local_serve_cmdline(command: str) -> bool: def _process_ppid(pid: int) -> int | None: - """Best-effort parent pid lookup. None on failure (and always on Windows, - where orphan reap is handled by the desktop tree-kill).""" + """Best-effort parent pid; None on failure (and always on Windows, where the desktop tree-kill reaps orphans).""" try: if sys.platform == "win32": return None - result = subprocess.run( - ["ps", "-o", "ppid=", "-p", str(pid)], - capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=5, - ) + result = subprocess.run(["ps", "-o", "ppid=", "-p", str(pid)], timeout=5, **_PS_RUN_KWARGS) if result.returncode != 0 or not result.stdout: return None return int(result.stdout.strip().split()[0]) @@ -601,13 +579,12 @@ def _process_ppid(pid: int) -> int | None: # --- SSH remote-backend lock ownership ------------------------------------- -# ``backend.lock.json`` is written by the Desktop SSH runtime on the *remote* -# host for every ``hermes serve`` it spawns (apps/desktop/electron/ -# remote-lifecycle.ts). A backend another client/machine started is legitimate -# and lock-owned even with no parent here (sshd exited → ppid 1). The reap must -# NEVER kill a PID a valid lock claims — that once killed a production backend. -# Schema constants mirror the writer; a mismatched record is simply ignored -# (the reap only ever *spares*). +# ``backend.lock.json`` is written by the Desktop SSH runtime on the *remote* host for every +# ``hermes serve`` it spawns (apps/desktop/electron/remote-lifecycle.ts). A backend another +# client/machine started is legitimate and lock-owned even with no parent here (sshd exited → +# ppid 1). The reap must NEVER kill a PID a valid lock claims — that once killed a production +# backend. Schema constants mirror the writer; a mismatched record is simply ignored (the reap +# only ever *spares*). _LOCKFILE_SCHEMA_VERSION = 2 _PROTOCOL_VERSION = 1 _REMOTE_LOCK_SUBDIR = "desktop-ssh" @@ -627,8 +604,8 @@ def _is_hex(value: object, length: int) -> bool: def _valid_lockfile_payload(parsed: object, ownership_id: str) -> bool: """Validate a parsed ``backend.lock.json`` body, mirroring readLockfile(). - An invalid lock is "no ownership claim", which never causes a kill — the - reap only ever *adds* lock-owned PIDs to its spare-set. + An invalid lock is "no ownership claim", which never causes a kill — the reap only ever + *adds* lock-owned PIDs to its spare-set. """ if ( not isinstance(parsed, dict) @@ -650,16 +627,18 @@ def _valid_lockfile_payload(parsed: object, ownership_id: str) -> bool: value = parsed.get(field) if not isinstance(value, str) or len(value) > 1024: return False - # logPath is ``{lock_root}/{ownershipId}/{spawnNonce}.log``. Only the - # suffix is checked so a relocated HERMES_HOME doesn't falsely reject a - # legitimate remote-owned backend (a false reject re-introduces the kill). + # logPath is ``{lock_root}/{ownershipId}/{spawnNonce}.log``. Only the suffix is checked so + # a relocated HERMES_HOME doesn't falsely reject a legitimate remote-owned backend (a false + # reject re-introduces the kill). return parsed["logPath"].endswith(f"/{ownership_id}/{parsed['spawnNonce']}.log") def _lock_owned_serve_pids(base_dir: Path | None = None) -> set[int]: - """PIDs claimed by valid ``{hermes_home}/desktop-ssh//backend.lock.json`` - records — legitimately owned (incl. SSH backends other clients started) and - spared by the reap. Best-effort: a bad record contributes no PID; never raises.""" + """PIDs claimed by valid ``{hermes_home}/desktop-ssh//backend.lock.json`` records. + + Legitimately owned (incl. SSH backends other clients started) and spared by the reap. + Best-effort: a bad record contributes no PID; never raises. + """ import json root = base_dir if base_dir is not None else _hermes_home_dir() / _REMOTE_LOCK_SUBDIR @@ -695,8 +674,8 @@ def _lock_owned_serve_pids(base_dir: Path | None = None) -> set[int]: return owned -# Grace window before an orphaned-looking backend may be reaped. Covers the -# gap between process start and the Desktop client writing backend.lock.json. +# Grace window before an orphaned-looking backend may be reaped: covers the gap between +# process start and the Desktop client writing backend.lock.json. _REAP_MIN_AGE_SECONDS = 180.0 @@ -715,22 +694,19 @@ def _reap_orphaned_desktop_local_serves( ) -> dict[str, list]: """Kill leftover Desktop-local ``hermes serve`` backends with no parent. - When Electron dies uncleanly, ``serve --host 127.0.0.1 --port 0`` children - get reparented to pid 1 with their MCP trees alive; each Desktop boot then - stacks a fresh backend on the corpses until EMFILE. The parent-death - watchdog (HERMES_PARENT_PID) prevents *future* orphans; this clears - *already* orphaned ones when a new Desktop backend starts. + When Electron dies uncleanly, ``serve --host 127.0.0.1 --port 0`` children get reparented + to pid 1 with their MCP trees alive; each Desktop boot then stacks a fresh backend on the + corpses until EMFILE. The parent-death watchdog (HERMES_PARENT_PID) prevents *future* + orphans; this clears *already* orphaned ones when a new Desktop backend starts. - A candidate is reaped only if ALL hold: Desktop-local shape (never a - fixed-port remote serve); ppid 1 (or 0 on some supervisors); not self / - parent / a HERMES_DESKTOP_CHILD_PID; not claimed by a valid - ``backend.lock.json`` (SSH backends other clients started legitimately sit - at ppid 1 — killing them is an incident, not cleanup); older than - ``_REAP_MIN_AGE_SECONDS`` with a determinable age — Desktop writes the lock - only after HERMES_BACKEND_READY, so during concurrent multi-profile startup - a live sibling is briefly unowned and indistinguishable from a corpse - (mutual-reap storm); a real corpse just waits for a later scan. - Best-effort; failures never raise to the caller. + A candidate is reaped only if ALL hold: Desktop-local shape (never a fixed-port remote + serve); ppid 1 (or 0 on some supervisors); not self / parent / a HERMES_DESKTOP_CHILD_PID; + not claimed by a valid ``backend.lock.json`` (SSH backends other clients started + legitimately sit at ppid 1 — killing them is an incident, not cleanup); older than + ``_REAP_MIN_AGE_SECONDS`` with a determinable age — Desktop writes the lock only after + HERMES_BACKEND_READY, so during concurrent multi-profile startup a live sibling is briefly + unowned and indistinguishable from a corpse (mutual-reap storm); a real corpse just waits + for a later scan. Best-effort; failures never raise to the caller. """ import signal as _signal import time as _time @@ -795,8 +771,8 @@ def _reap_orphaned_desktop_local_serves( except OSError: failed.append(pid) - # Brief grace, then SIGKILL survivors. psutil.pid_exists rather than - # os.kill(pid, 0), which is a Windows footgun the linter blocks everywhere. + # Brief grace, then SIGKILL survivors. psutil.pid_exists rather than os.kill(pid, 0), + # which is a Windows footgun the linter blocks everywhere. sleep_fn(1.5) import psutil