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

1"""Watch ``LILBEE_PARENT_PID`` and shut down when the parent process exits.""" 

2 

3from __future__ import annotations 

4 

5import asyncio 

6import logging 

7import os 

8import threading 

9import time 

10from collections.abc import Callable 

11 

12import psutil 

13 

14log = logging.getLogger(__name__) 

15 

16POLL_INTERVAL_SECS = 2.0 

17PARENT_PID_ENV = "LILBEE_PARENT_PID" 

18 

19 

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 

35 

36 

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

48 

49 

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.""" 

57 

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

63 

64 thread = threading.Thread(target=_loop, daemon=True, name="lilbee-parent-monitor") 

65 thread.start() 

66 return thread