moebiusT7/book-ocr-studio
0
1"""Linux subprocess supervisor: release owned descendants when the caller exits.2 3A private pipe, not a PID/name scan, defines ownership. Closing the caller's4write end (including SIGKILL) asks the independent supervisor to stop its group.5Normal child exit and SIGTERM also clean up remaining group members.6"""7import json8import os9import select10import signal11import subprocess12import sys13import time14import weakref15from pathlib import Path16 17 18def _close(fd):19 try: os.close(fd)20 except OSError: pass21 22 23class OwnedPopen(subprocess.Popen):24 def __init__(self, args, **kwargs):25 if os.name != 'posix' or not sys.platform.startswith('linux'):26 raise RuntimeError('Owned process supervision requires Linux')27 if kwargs.pop('shell', False) or kwargs.get('preexec_fn') or kwargs.get('pass_fds'):28 raise ValueError('OwnedPopen requires direct argv without preexec_fn/pass_fds')29 reader, writer = os.pipe2(os.O_CLOEXEC)30 self._pipe_finalizer = None31 try:32 kwargs['start_new_session'] = True33 kwargs['pass_fds'] = (reader,)34 super().__init__([sys.executable, str(Path(__file__).resolve()), '--watch',35 str(reader), json.dumps([os.fspath(a) for a in args])], **kwargs)36 self._pipe_finalizer = weakref.finalize(self, _close, writer)37 except BaseException:38 _close(writer)39 raise40 finally:41 _close(reader)42 43 def wait(self, timeout=None):44 result = super().wait(timeout=timeout)45 finalizer = getattr(self, '_pipe_finalizer', None)46 if finalizer: finalizer()47 return result48 49 50def _watch(reader, args):51 stopping = False52 def stop(signum, frame):53 nonlocal stopping54 stopping = True55 for sig in (signal.SIGTERM, signal.SIGINT, signal.SIGHUP):56 signal.signal(sig, stop)57 child = None58 result = 159 try:60 # Avoid starting a model if the caller already vanished during spawn.61 if select.select([reader], [], [], 0)[0] and not os.read(reader, 1):62 return 12563 child = subprocess.Popen(args, start_new_session=True, close_fds=True)64 while not stopping:65 code = child.poll()66 if code is not None:67 result = code if code >= 0 else 128-code68 break69 if select.select([reader], [], [], .1)[0] and not os.read(reader, 1):70 stopping = True71 if stopping: result = 14372 finally:73 if child:74 # Only the process group created above is signalled. External API75 # servers and other applications are never selected by name.76 try: os.killpg(child.pid, signal.SIGTERM)77 except ProcessLookupError: pass78 try: child.wait(timeout=2)79 except subprocess.TimeoutExpired: pass80 # The leader may exit before a model runner/grandchild does.81 time.sleep(.1)82 try: os.killpg(child.pid, signal.SIGKILL)83 except ProcessLookupError: pass84 child.wait()85 _close(reader)86 return result87 88 89if __name__ == '__main__':90 if len(sys.argv) != 4 or sys.argv[1] != '--watch':91 raise SystemExit('Internal owned-process supervisor')92 raise SystemExit(_watch(int(sys.argv[2]), json.loads(sys.argv[3])))93 