Coverage for src / lilbee / parent_monitor.py: 100%
39 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-29 19:16 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-04-29 19:16 +0000
1"""Watch ``LILBEE_PARENT_PID`` and shut down when the parent process exits."""
3from __future__ import annotations
5import asyncio
6import logging
7import os
8import threading
9import time
10from collections.abc import Callable
12import psutil
14log = logging.getLogger(__name__)
16POLL_INTERVAL_SECS = 2.0
17PARENT_PID_ENV = "LILBEE_PARENT_PID"
20def parse_parent_pid(env: dict[str, str] | None = None) -> int | None:
21 """Return a valid parent PID from the env, or None if unset/garbage."""
22 src = env if env is not None else os.environ
23 raw = src.get(PARENT_PID_ENV)
24 if not raw:
25 return None
26 try:
27 pid = int(raw)
28 except ValueError:
29 log.warning("%s=%r is not an integer; skipping parent-death monitor", PARENT_PID_ENV, raw)
30 return None
31 if pid <= 0:
32 log.warning("%s=%d is non-positive; skipping parent-death monitor", PARENT_PID_ENV, pid)
33 return None
34 return pid
37async def watch_parent_async(
38 parent_pid: int,
39 on_death: Callable[[], None],
40 *,
41 poll_interval_secs: float = POLL_INTERVAL_SECS,
42) -> None:
43 """Poll *parent_pid* until it disappears, then call *on_death* once."""
44 while psutil.pid_exists(parent_pid):
45 await asyncio.sleep(poll_interval_secs)
46 log.info("%s=%d is no longer alive; triggering shutdown", PARENT_PID_ENV, parent_pid)
47 on_death()
50def watch_parent_thread(
51 parent_pid: int,
52 on_death: Callable[[], None],
53 *,
54 poll_interval_secs: float = POLL_INTERVAL_SECS,
55) -> threading.Thread:
56 """Spawn a daemon thread that fires *on_death* when *parent_pid* exits."""
58 def _loop() -> None:
59 while psutil.pid_exists(parent_pid):
60 time.sleep(poll_interval_secs)
61 log.info("%s=%d is no longer alive; triggering shutdown", PARENT_PID_ENV, parent_pid)
62 on_death()
64 thread = threading.Thread(target=_loop, daemon=True, name="lilbee-parent-monitor")
65 thread.start()
66 return thread