Refactored Shutdown, Restart and exit status codes

pull/2411/head
JayZed 3 months ago committed by GitHub
parent c4553452a5
commit 9ae684240b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -6,10 +6,13 @@ import signal
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()
@ -19,7 +22,7 @@ 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.")
@ -27,7 +30,7 @@ def check_python_version():
(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():
@ -49,55 +52,77 @@ check_python_version()
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:
@ -111,18 +136,14 @@ if __name__ == '__main__':
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)

@ -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')
@ -54,7 +55,7 @@ class SystemLogs(Resource):
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:

@ -7,6 +7,8 @@ import logging
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
@ -410,8 +412,9 @@ while failed_validator:
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():

@ -8,6 +8,7 @@ import platform
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
@ -61,16 +62,19 @@ class UnwantedWaitressMessageFilter(logging.Filter):
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
@ -79,7 +83,7 @@ class UnwantedWaitressMessageFilter(logging.Filter):
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
@ -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')

@ -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
@ -37,6 +38,7 @@ class Server:
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)
@ -62,9 +64,17 @@ class Server:
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):
@ -72,31 +82,19 @@ class Server:
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()

@ -9,9 +9,11 @@ from functools import wraps
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
@ -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

@ -1,7 +1,6 @@
# coding=utf-8
import os
import io
import sys
import subprocess
import subliminal
@ -20,6 +19,9 @@ from utilities.backup import restore_from_backup
from app.database import init_db
from literals import *
from utilities.central import make_bazarr_dir, restart_bazarr, stop_bazarr
# set start time global variable as epoch
global startTime
startTime = time.time()
@ -37,20 +39,15 @@ if not os.path.exists(args.config_dir):
os.mkdir(os.path.join(args.config_dir))
except OSError:
print("BAZARR The configuration directory doesn't exist and Bazarr cannot create it (permission issue?).")
exit(2)
if not os.path.exists(os.path.join(args.config_dir, 'config')):
os.mkdir(os.path.join(args.config_dir, 'config'))
if not os.path.exists(os.path.join(args.config_dir, 'db')):
os.mkdir(os.path.join(args.config_dir, 'db'))
if not os.path.exists(os.path.join(args.config_dir, 'log')):
os.mkdir(os.path.join(args.config_dir, 'log'))
if not os.path.exists(os.path.join(args.config_dir, 'cache')):
os.mkdir(os.path.join(args.config_dir, 'cache'))
if not os.path.exists(os.path.join(args.config_dir, 'backup')):
os.mkdir(os.path.join(args.config_dir, 'backup'))
if not os.path.exists(os.path.join(args.config_dir, 'restore')):
os.mkdir(os.path.join(args.config_dir, 'restore'))
stop_bazarr(EXIT_CONFIG_CREATE_ERROR)
os.environ[ENV_BAZARR_ROOT_DIR] = os.path.join(args.config_dir)
make_bazarr_dir(DIR_BACKUP)
make_bazarr_dir(DIR_CACHE)
make_bazarr_dir(DIR_CONFIG)
make_bazarr_dir(DIR_DB)
make_bazarr_dir(DIR_LOG)
make_bazarr_dir(DIR_RESTORE)
# set subliminal_patch hearing-impaired extension to use when naming subtitles
os.environ["SZ_HI_EXTENSION"] = settings.general.hi_extension
@ -99,19 +96,11 @@ if not args.no_update:
subprocess.check_output(pip_command, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
logging.exception(f'BAZARR requirements.txt installation result: {e.stdout}')
os._exit(1)
os._exit(EXIT_REQUIREMENTS_ERROR)
else:
logging.info('BAZARR requirements installed.')
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()
os._exit(0)
restart_bazarr()
# change default base_url to ''
settings.general.base_url = settings.general.base_url.rstrip('/')

@ -0,0 +1,30 @@
# coding=utf-8
# only primitive types can be specified here
# for other derived values, use constants.py
# bazarr environment variable names
ENV_STOPFILE = 'STOPFILE'
ENV_RESTARTFILE = 'RESTARTFILE'
ENV_BAZARR_ROOT_DIR = 'BAZARR_ROOT'
# bazarr subdirectories
DIR_BACKUP = 'backup'
DIR_CACHE = 'cache'
DIR_CONFIG = 'config'
DIR_DB = 'db'
DIR_LOG = 'log'
DIR_RESTORE = 'restore'
# bazarr special files
FILE_LOG = 'bazarr.log'
FILE_RESTART = 'bazarr.restart'
FILE_STOP = 'bazarr.stop'
# bazarr exit codes
EXIT_NORMAL = 0
EXIT_INTERRUPT = -100
EXIT_VALIDATION_ERROR = -101
EXIT_CONFIG_CREATE_ERROR = -102
EXIT_PYTHON_UPGRADE_NEEDED = -103
EXIT_REQUIREMENTS_ERROR = -104

@ -44,16 +44,8 @@ from app.server import webserver, app # noqa E402
from app.announcements import get_announcements_to_file # noqa E402
if args.create_db_revision:
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:
create_db_revision(app)
logging.info('Bazarr is being shutdown...')
stop_file.write(str(''))
stop_file.close()
os._exit(0)
create_db_revision(app)
stop_bazarr(EXIT_NORMAL)
else:
migrate_db(app)

@ -1,7 +1,6 @@
# coding=utf-8
import os
import io
import sqlite3
import shutil
import logging
@ -12,6 +11,7 @@ from glob import glob
from app.get_args import args
from app.config import settings
from utilities.central import restart_bazarr
def get_backup_path():
@ -133,16 +133,7 @@ def restore_from_backup():
logging.exception(f'Unable to delete {dest_database_path}')
logging.info('Backup restored successfully. Bazarr will restart.')
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('')
restart_file.close()
os._exit(0)
restart_bazarr()
elif os.path.isfile(restore_config_path) or os.path.isfile(restore_database_path):
logging.debug('Cannot restore a partial backup. You must have both config and database.')
else:

@ -0,0 +1,49 @@
# coding=utf-8
# only methods can be specified here that do not cause other moudules to be loaded
# for other methods that use settings, etc., use utilities/helper.py
import logging
import os
from pathlib import Path
from literals import *
def get_bazarr_dir(sub_dir):
path = os.path.join(os.environ[ENV_BAZARR_ROOT_DIR], sub_dir)
return path
def make_bazarr_dir(sub_dir):
path = get_bazarr_dir(sub_dir)
if not os.path.exists(path):
os.mkdir(path)
def get_log_file_path():
path = os.path.join(get_bazarr_dir(DIR_LOG), FILE_LOG)
return path
def get_stop_file_path():
return os.environ[ENV_STOPFILE]
def get_restart_file_path():
return os.environ[ENV_RESTARTFILE]
def stop_bazarr(status_code=EXIT_NORMAL, exit_main=True):
try:
with open(get_stop_file_path(),'w', encoding='UTF-8') as file:
# write out status code for final exit
file.write(f'{status_code}\n')
file.close()
except Exception as e:
logging.error(f'BAZARR Cannot create stop file: {repr(e)}')
logging.info('Bazarr is being shutdown...')
if exit_main:
raise SystemExit(status_code)
def restart_bazarr():
try:
Path(get_restart_file_path()).touch()
except Exception as e:
logging.error(f'BAZARR Cannot create restart file: {repr(e)}')
logging.info('Bazarr is being restarted...')
raise SystemExit(EXIT_NORMAL)
Loading…
Cancel
Save