Skip to content

Commit

Permalink
Refactored Shutdown, Restart and exit status codes
Browse files Browse the repository at this point in the history
  • Loading branch information
JaiZed authored Mar 3, 2024
1 parent c455345 commit 9ae6842
Show file tree
Hide file tree
Showing 11 changed files with 211 additions and 131 deletions.
103 changes: 62 additions & 41 deletions bazarr.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import subprocess
import sys
import time
import atexit

from bazarr.app.get_args import args
from bazarr.literals import *

def exit_program(status_code):
print(f'Bazarr exited with status code {status_code}.')
raise SystemExit(status_code)

def check_python_version():
python_version = platform.python_version_tuple()
Expand All @@ -19,15 +22,15 @@ def check_python_version():
if int(python_version[0]) < minimum_py3_tuple[0]:
print("Python " + minimum_py3_str + " or greater required. "
"Current version is " + platform.python_version() + ". Please upgrade Python.")
sys.exit(1)
exit_program(EXIT_PYTHON_UPGRADE_NEEDED)
elif int(python_version[0]) == 3 and int(python_version[1]) > 11:
print("Python version greater than 3.11.x is unsupported. Current version is " + platform.python_version() +
". Keep in mind that even if it works, you're on your own.")
elif (int(python_version[0]) == minimum_py3_tuple[0] and int(python_version[1]) < minimum_py3_tuple[1]) or \
(int(python_version[0]) != minimum_py3_tuple[0]):
print("Python " + minimum_py3_str + " or greater required. "
"Current version is " + platform.python_version() + ". Please upgrade Python.")
sys.exit(1)
exit_program(EXIT_PYTHON_UPGRADE_NEEDED)


def get_python_path():
Expand All @@ -49,55 +52,77 @@ def get_python_path():

dir_name = os.path.dirname(__file__)

def start_bazarr():
script = [get_python_path(), "-u", os.path.normcase(os.path.join(dir_name, 'bazarr', 'main.py'))] + sys.argv[1:]
ep = subprocess.Popen(script, stdout=None, stderr=None, stdin=subprocess.DEVNULL)
print(f"Bazarr starting child process with PID {ep.pid}...")
return ep


def terminate_child():
print(f"Terminating child process with PID {child_process.pid}")
child_process.terminate()

def end_child_process(ep):

def get_stop_status_code(input_file):
try:
if os.name != 'nt':
try:
ep.send_signal(signal.SIGINT)
except ProcessLookupError:
pass
else:
import win32api
import win32con
with open(input_file,'r') as file:
# read status code from file, if it exists
line = file.readline()
try:
win32api.GenerateConsoleCtrlEvent(win32con.CTRL_C_EVENT, ep.pid)
except KeyboardInterrupt:
pass
status_code = int(line)
except (ValueError, TypeError):
status_code = EXIT_NORMAL
file.close()
except:
ep.terminate()


def start_bazarr():
script = [get_python_path(), "-u", os.path.normcase(os.path.join(dir_name, 'bazarr', 'main.py'))] + sys.argv[1:]
ep = subprocess.Popen(script, stdout=None, stderr=None, stdin=subprocess.DEVNULL)
atexit.register(end_child_process, ep=ep)
signal.signal(signal.SIGTERM, lambda signal_no, frame: end_child_process(ep))
status_code = EXIT_NORMAL
return status_code


def check_status():
global child_process
if os.path.exists(stopfile):
status_code = get_stop_status_code(stopfile)
try:
print(f"Deleting stop file...")
os.remove(stopfile)
except Exception:
except Exception as e:
print('Unable to delete stop file.')
finally:
print('Bazarr exited.')
sys.exit(0)
terminate_child()
exit_program(status_code)

if os.path.exists(restartfile):
try:
print(f"Deleting restart file...")
os.remove(restartfile)
except Exception:
print('Unable to delete restart file.')
else:
print("Bazarr is restarting...")
start_bazarr()
finally:
terminate_child()
print(f"Bazarr is restarting...")
child_process = start_bazarr()


def interrupt_handler(signum, frame):
# catch and ignore keyboard interrupt Ctrl-C
# the child process Server object will catch SIGINT and perform an orderly shutdown
global interrupted
if not interrupted:
# ignore user hammering Ctrl-C; we heard you the first time!
interrupted = True
print('Handling keyboard interrupt...')
else:
print(f"Stop doing that! I heard you the first time!")


if __name__ == '__main__':
restartfile = os.path.join(args.config_dir, 'bazarr.restart')
stopfile = os.path.join(args.config_dir, 'bazarr.stop')
interrupted = False
signal.signal(signal.SIGINT, interrupt_handler)
restartfile = os.path.join(args.config_dir, FILE_RESTART)
stopfile = os.path.join(args.config_dir, FILE_STOP)
os.environ[ENV_STOPFILE] = stopfile
os.environ[ENV_RESTARTFILE] = restartfile

# Cleanup leftover files
try:
Expand All @@ -111,18 +136,14 @@ def check_status():
pass

# Initial start of main bazarr process
print("Bazarr starting...")
start_bazarr()
child_process = start_bazarr()

# Keep the script running forever until stop is requested through term or keyboard interrupt
# Keep the script running forever until stop is requested through term, special files or keyboard interrupt
while True:
check_status()
try:
if sys.platform.startswith('win'):
time.sleep(5)
else:
os.wait()
time.sleep(1)
time.sleep(5)
except (KeyboardInterrupt, SystemExit, ChildProcessError):
print('Bazarr exited.')
sys.exit(0)
# this code should never be reached, if signal handling is working properly
print(f'Bazarr exited main script file via keyboard interrupt.')
exit_program(EXIT_INTERRUPT)
5 changes: 3 additions & 2 deletions bazarr/api/system/logs.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
# coding=utf-8

import io
import os
import re

from flask_restx import Resource, Namespace, fields, marshal

from app.config import settings
from app.logger import empty_log
from app.get_args import args

from utilities.central import get_log_file_path
from ..utils import authenticate

api_ns_system_logs = Namespace('System Logs', description='List log file entries or empty log file')
Expand Down Expand Up @@ -54,7 +55,7 @@ def get(self):
include = include.casefold()
exclude = exclude.casefold()

with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file:
with io.open(get_log_file_path(), encoding='UTF-8') as file:
raw_lines = file.read()
lines = raw_lines.split('|\n')
for line in lines:
Expand Down
7 changes: 5 additions & 2 deletions bazarr/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import re

from urllib.parse import quote_plus
from literals import EXIT_VALIDATION_ERROR
from utilities.central import stop_bazarr
from subliminal.cache import region
from dynaconf import Dynaconf, Validator as OriginalValidator
from dynaconf.loaders.yaml_loader import write
Expand Down Expand Up @@ -410,8 +412,9 @@ def convert_ini_to_yaml(config_file):
settings[current_validator_details.names[0]] = current_validator_details.default
else:
logging.critical(f"Value for {current_validator_details.names[0]} doesn't pass validation and there's no "
f"default value. This issue must be reported. Bazarr won't works until it's been fixed.")
os._exit(0)
f"default value. This issue must be reported to and fixed by the development team. "
f"Bazarr won't work until it's been fixed.")
stop_bazarr(EXIT_VALIDATION_ERROR)


def write_config():
Expand Down
30 changes: 17 additions & 13 deletions bazarr/app/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import warnings

from logging.handlers import TimedRotatingFileHandler
from utilities.central import get_log_file_path
from pytz_deprecation_shim import PytzUsageWarning

from .get_args import args
Expand Down Expand Up @@ -61,16 +62,19 @@ def filter(self, record):
if settings.general.debug is True:
# no filtering in debug mode
return True

unwantedMessages = [
"Exception while serving /api/socket.io/",
['Session is disconnected', 'Session not found'],

"Exception while serving /api/socket.io/",
["'Session is disconnected'", "'Session not found'"],

"Exception while serving /api/socket.io/",
['"Session is disconnected"', '"Session not found"']

unwantedMessages = [
"Exception while serving /api/socket.io/",
['Session is disconnected', 'Session not found' ],

"Exception while serving /api/socket.io/",
["'Session is disconnected'", "'Session not found'" ],

"Exception while serving /api/socket.io/",
['"Session is disconnected"', '"Session not found"' ],

"Exception when servicing %r",
[],
]

wanted = True
Expand All @@ -79,7 +83,7 @@ def filter(self, record):
if record.msg == unwantedMessages[i]:
exceptionTuple = record.exc_info
if exceptionTuple is not None:
if str(exceptionTuple[1]) in unwantedMessages[i+1]:
if len(unwantedMessages[i+1]) == 0 or str(exceptionTuple[1]) in unwantedMessages[i+1]:
wanted = False
break

Expand Down Expand Up @@ -112,10 +116,10 @@ def configure_logging(debug=False):
# File Logging
global fh
if sys.version_info >= (3, 9):
fh = PatchedTimedRotatingFileHandler(os.path.join(args.config_dir, 'log/bazarr.log'), when="midnight",
fh = PatchedTimedRotatingFileHandler(get_log_file_path(), when="midnight",
interval=1, backupCount=7, delay=True, encoding='utf-8')
else:
fh = TimedRotatingFileHandler(os.path.join(args.config_dir, 'log/bazarr.log'), when="midnight", interval=1,
fh = TimedRotatingFileHandler(get_log_file_path(), when="midnight", interval=1,
backupCount=7, delay=True, encoding='utf-8')
f = FileHandlerFormatter('%(asctime)s|%(levelname)-8s|%(name)-32s|%(message)s|',
'%Y-%m-%d %H:%M:%S')
Expand Down
48 changes: 23 additions & 25 deletions bazarr/app/server.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
# coding=utf-8

import signal
import warnings
import logging
import os
import io
import errno
from literals import EXIT_INTERRUPT, EXIT_NORMAL
from utilities.central import restart_bazarr, stop_bazarr

from waitress.server import create_server
from time import sleep
Expand Down Expand Up @@ -37,6 +38,7 @@ def __init__(self):
self.connected = False
self.address = str(settings.general.ip)
self.port = int(args.port) if args.port else int(settings.general.port)
self.interrupted = False

while not self.connected:
sleep(0.1)
Expand All @@ -62,41 +64,37 @@ def configure_server(self):
logging.exception("BAZARR cannot start because of unhandled exception.")
self.shutdown()

def interrupt_handler(self, signum, frame):
# print('Server signal interrupt handler called with signal', signum)
if not self.interrupted:
# ignore user hammering Ctrl-C; we heard you the first time!
self.interrupted = True
self.shutdown(EXIT_INTERRUPT)

def start(self):
logging.info(f'BAZARR is started and waiting for request on http://{self.server.effective_host}:'
f'{self.server.effective_port}')
signal.signal(signal.SIGINT, self.interrupt_handler)
try:
self.server.run()
except (KeyboardInterrupt, SystemExit):
self.shutdown()
except Exception:
pass

def shutdown(self):
try:
stop_file = io.open(os.path.join(args.config_dir, "bazarr.stop"), "w", encoding='UTF-8')
except Exception as e:
logging.error(f'BAZARR Cannot create stop file: {repr(e)}')
else:
logging.info('Bazarr is being shutdown...')
stop_file.write(str(''))
stop_file.close()
close_database()
self.server.close()
os._exit(0)
def close_all(self):
print(f"Closing database...")
close_database()
print(f"Closing webserver...")
self.server.close()

def shutdown(self, status=EXIT_NORMAL):
self.close_all()
stop_bazarr(status, False)

def restart(self):
try:
restart_file = io.open(os.path.join(args.config_dir, "bazarr.restart"), "w", encoding='UTF-8')
except Exception as e:
logging.error(f'BAZARR Cannot create restart file: {repr(e)}')
else:
logging.info('Bazarr is being restarted...')
restart_file.write(str(''))
restart_file.close()
close_database()
self.server.close()
os._exit(0)
self.close_all()
restart_bazarr()


webserver = Server()
6 changes: 4 additions & 2 deletions bazarr/app/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
from urllib.parse import unquote

from constants import headers
from literals import FILE_LOG
from sonarr.info import url_api_sonarr
from radarr.info import url_api_radarr
from utilities.helper import check_credentials
from utilities.central import get_log_file_path

from .config import settings, base_url
from .database import System
Expand Down Expand Up @@ -98,9 +100,9 @@ def catch_all(path):


@check_login
@ui_bp.route('/bazarr.log')
@ui_bp.route('/' + FILE_LOG)
def download_log():
return send_file(os.path.join(args.config_dir, 'log', 'bazarr.log'), max_age=0, as_attachment=True)
return send_file(get_log_file_path(), max_age=0, as_attachment=True)


@check_login
Expand Down
Loading

0 comments on commit 9ae6842

Please sign in to comment.