From e26f7fc49e86a6a8aaf78409c565bf1ba37af865 Mon Sep 17 00:00:00 2001 From: Smaarn Date: Sun, 12 Jan 2020 15:27:14 +0100 Subject: [PATCH 1/2] Fixed: when receiving a SIGTERM signal, a smooth shutdown procedure should be performed on children processes. --- bazarr.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 99 insertions(+), 7 deletions(-) diff --git a/bazarr.py b/bazarr.py index ae5d88f13..afb9b52c9 100644 --- a/bazarr.py +++ b/bazarr.py @@ -11,6 +11,7 @@ import os import sys import platform import re +import signal from bazarr.get_args import args @@ -39,15 +40,97 @@ check_python_version() dir_name = os.path.dirname(__file__) -def start_bazarr(): +class ProcessRegistry: + + def register(self, process): + pass + + def unregister(self, process): + pass + + +class DaemonStatus(ProcessRegistry): + + def __init__(self): + self.__should_stop = False + self.__processes = set() + + def register(self, process): + self.__processes.add(process) + + def unregister(self, process): + self.__processes.remove(process) + + ''' + Waits all the provided processes for the specified amount of time in seconds. + ''' + @staticmethod + def __wait_for_processes(processes, timeout): + reference_ts = time.time() + elapsed = 0 + remaining_processes = list(processes) + while elapsed < timeout and len(remaining_processes) > 0: + remaining_time = timeout - elapsed + for ep in list(remaining_processes): + if ep.poll() is not None: + remaining_processes.remove(ep) + else: + if remaining_time > 0: + if PY3: + try: + ep.wait(remaining_time) + remaining_processes.remove(ep) + except sp.TimeoutExpired: + pass + else: + ''' + In python 2 there is no such thing as some mechanism to wait with a timeout. + ''' + time.sleep(1) + elapsed = time.time() - reference_ts + remaining_time = timeout - elapsed + return remaining_processes + + ''' + Sends to every single of the specified processes the given signal and (if live_processes is not None) append to it processes which are still alive. + ''' + @staticmethod + def __send_signal(processes, signal_no, live_processes=None): + for ep in processes: + if ep.poll() is None: + if live_processes is not None: + live_processes.append(ep) + try: + ep.send_signal(signal_no) + except Exception as e: + print('Failed sending signal %s to process %s because of an unexpected error: %s' % (signal_no, ep.pid, e)) + return live_processes + + ''' + Flags this instance as should stop and terminates as smoothly as possible children processes. + ''' + def stop(self): + self.__should_stop = True + live_processes = DaemonStatus.__send_signal(self.__processes, signal.SIGINT, list()) + live_processes = DaemonStatus.__wait_for_processes(live_processes, 120) + DaemonStatus.__send_signal(live_processes, signal.SIGTERM) + + def should_stop(self): + return self.__should_stop + + +def start_bazarr(process_registry=ProcessRegistry()): script = [sys.executable, "-u", os.path.normcase(os.path.join(dir_name, 'bazarr', 'main.py'))] + sys.argv[1:] ep = sp.Popen(script, stdout=sp.PIPE, stderr=sp.STDOUT, stdin=sp.PIPE) + process_registry.register(ep) print("Bazarr starting...") try: while True: line = ep.stdout.readline() if line == '' or not line: + # Process ended so let's unregister it + process_registry.unregister(ep) break if PY3: sys.stdout.buffer.write(line) @@ -73,7 +156,7 @@ if __name__ == '__main__': pass - def daemon(): + def daemon(daemonStatus): if os.path.exists(stopfile): try: os.remove(stopfile) @@ -89,12 +172,21 @@ if __name__ == '__main__': except: print('Unable to delete restart file.') else: - start_bazarr() + start_bazarr(daemonStatus) + + + daemonStatus = DaemonStatus() + def shutdown(): + # indicates that everything should stop + daemonStatus.stop() + # emulate a Ctrl C command on itself (bypasses the signal thing but, then, emulates the "Ctrl+C break") + os.kill(os.getpid(), signal.SIGINT) - start_bazarr() + signal.signal(signal.SIGTERM, lambda signal_no, frame: shutdown()) + start_bazarr(daemonStatus) - # Keep the script running forever. - while True: - daemon() + # Keep the script running forever until stop is requested through term or keyboard interrupt + while not daemonStatus.should_stop(): + daemon(daemonStatus) time.sleep(1) From 02c3c98cf80b8f447dcc7561ebc1adcd4213ed48 Mon Sep 17 00:00:00 2001 From: Smaarn Date: Sun, 19 Jan 2020 14:46:27 +0100 Subject: [PATCH 2/2] Fixed: when receiving a SIGTERM signal, a smooth shutdown procedure should be performed on children processes. Reverted the change for Python 2 --- bazarr.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/bazarr.py b/bazarr.py index afb9b52c9..f9228e2be 100644 --- a/bazarr.py +++ b/bazarr.py @@ -156,7 +156,7 @@ if __name__ == '__main__': pass - def daemon(daemonStatus): + def daemon(bazarr_runner = lambda: start_bazarr()): if os.path.exists(stopfile): try: os.remove(stopfile) @@ -172,21 +172,30 @@ if __name__ == '__main__': except: print('Unable to delete restart file.') else: - start_bazarr(daemonStatus) + bazarr_runner() - daemonStatus = DaemonStatus() + bazarr_runner = lambda: start_bazarr() - def shutdown(): - # indicates that everything should stop - daemonStatus.stop() - # emulate a Ctrl C command on itself (bypasses the signal thing but, then, emulates the "Ctrl+C break") - os.kill(os.getpid(), signal.SIGINT) + should_stop = lambda: False - signal.signal(signal.SIGTERM, lambda signal_no, frame: shutdown()) - start_bazarr(daemonStatus) + if PY3: + daemonStatus = DaemonStatus() + + def shutdown(): + # indicates that everything should stop + daemonStatus.stop() + # emulate a Ctrl C command on itself (bypasses the signal thing but, then, emulates the "Ctrl+C break") + os.kill(os.getpid(), signal.SIGINT) + + signal.signal(signal.SIGTERM, lambda signal_no, frame: shutdown()) + + should_stop = lambda: daemonStatus.should_stop() + bazarr_runner = lambda: start_bazarr(daemonStatus) + + bazarr_runner() # Keep the script running forever until stop is requested through term or keyboard interrupt - while not daemonStatus.should_stop(): - daemon(daemonStatus) + while not should_stop(): + daemon(bazarr_runner) time.sleep(1)