1616import pickle
1717import threading
1818import asyncio
19+ import os
20+ import time
1921from .user_app_loader import load_user_app
2022
23+ WATCHDOG_INTERVAL_SECONDS = 10.0
2124
2225# ---------------------------------------------
2326# Main process: single, lazily-created pool
@@ -33,7 +36,9 @@ def _get_pool() -> ProcessPoolExecutor:
3336 if _pool is None :
3437 # Single worker process as requested
3538 _pool = ProcessPoolExecutor (
36- max_workers = 1 , initializer = _subprocess_init , initargs = (_user_apps ,)
39+ max_workers = 1 ,
40+ initializer = _subprocess_init ,
41+ initargs = (_user_apps , os .getpid ()),
3742 )
3843 return _pool
3944
@@ -48,7 +53,33 @@ def add_user_app(app_target: str) -> None:
4853# ---------------------------------------------
4954
5055
51- def _subprocess_init (user_apps : list [str ]) -> None :
56+ def _start_parent_watchdog (
57+ parent_pid : int , interval_seconds : float = WATCHDOG_INTERVAL_SECONDS
58+ ) -> None :
59+ """Terminate this process if the parent process exits or PPID changes.
60+
61+ This runs in a background daemon thread so it never blocks pool work.
62+ """
63+
64+ def _watch () -> None :
65+ while True :
66+ # If PPID changed (parent died and we were reparented), exit.
67+ if os .getppid () != parent_pid :
68+ os ._exit (1 )
69+
70+ # Best-effort liveness probe in case PPID was reused.
71+ try :
72+ os .kill (parent_pid , 0 )
73+ except OSError :
74+ os ._exit (1 )
75+
76+ time .sleep (interval_seconds )
77+
78+ threading .Thread (target = _watch , name = "parent-watchdog" , daemon = True ).start ()
79+
80+
81+ def _subprocess_init (user_apps : list [str ], parent_pid : int ) -> None :
82+ _start_parent_watchdog (parent_pid )
5283 for app_target in user_apps :
5384 load_user_app (app_target )
5485
0 commit comments