diff --git a/src/revpi_middleware/proginit.py b/src/revpi_middleware/proginit.py index a1dd338..75d2fb4 100644 --- a/src/revpi_middleware/proginit.py +++ b/src/revpi_middleware/proginit.py @@ -5,19 +5,20 @@ __author__ = "Sven Sager" __copyright__ = "Copyright (C) 2018-2023 Sven Sager" __license__ = "LGPL-2.0-or-later" -__version__ = "1.3.2" +__version__ = "1.4.0" import logging import sys -from argparse import ArgumentParser +from argparse import ArgumentParser, Namespace from configparser import ConfigParser +from enum import Enum from os import R_OK, W_OK, access, environ, getpid, remove from os.path import abspath, dirname, exists, join from shutil import copy, move from threading import Event try: - # Import program version from meta data module of your program + # Import the program version from the meta-data module of your program from .__about__ import __version__ as external_version except Exception: external_version = None @@ -26,11 +27,12 @@ programname = "revpi-middleware" # Program name program_version = external_version or "a.b.c" conf_rw = False # If you want so save the configuration with .save_conf() set to True -conf_rw_save = False # Create new conf file in same directory and move to old one -conf_rw_backup = False # Keep a backup of old conf file [filename].bak -_extend_daemon_startup_timeout = 0.0 # Default startup timeout is 90 seconds +conf_rw_save = False # Create a new conf file in the same directory and move to the old one +conf_rw_backup = False # Keep a backup of an old conf file [filename].bak +_extend_daemon_startup_timeout = 0.0 # The default startup timeout is 90 seconds conf = ConfigParser() +pargs = Namespace(daemon=False, verbose=0) logger = logging.getLogger() pidfile = "/var/run/{0}.pid".format(programname) _daemon_started_up = Event() @@ -90,11 +92,19 @@ def cleanup(): sys.stdout.close() -def reconfigure_logger(): +class StdLogOutput(Enum): + """Enum for the different output streams of the logger.""" + + NONE = "" + STDOUT = "stdout" + STDERR = "stderr" + + +def reconfigure_logger(std_output: StdLogOutput = StdLogOutput.STDOUT): """Configure logging module of program.""" class FilterDebug(logging.Filter): - """Set this filter to log handler if verbose level is > 1.""" + """Set this filter to log handler if the verbose level is > 1.""" def filter(self, record: logging.LogRecord) -> bool: remove_record = False @@ -104,29 +114,33 @@ def reconfigure_logger(): return not remove_record - # Clear all log handler + # Clear all log handlers for lhandler in logger.handlers.copy(): lhandler.close() logger.removeHandler(lhandler) if pargs.daemon: - # Create daemon log file + # Create the daemon log file fh_logfile = open("/var/log/{0}.log".format(programname), "a") - # Close stdout and use logfile + # Close stdout and use the logfile sys.stdout.close() sys.stdout = fh_logfile sys.stderr = sys.stdout - # Create new log handler + # Create the new log handler if pargs.verbose > 2: log_frm = "{asctime} [{levelname:8}] {name} {message}" else: log_frm = "{asctime} [{levelname:8}] {message}" logformat = logging.Formatter(log_frm, datefmt="%Y-%m-%d %H:%M:%S", style="{") - lhandler = logging.StreamHandler(sys.stdout) - lhandler.setFormatter(logformat) - logger.addHandler(lhandler) + + if std_output is not StdLogOutput.NONE: + lhandler = logging.StreamHandler( + sys.stdout if std_output is StdLogOutput.STDOUT else sys.stderr + ) + lhandler.setFormatter(logformat) + logger.addHandler(lhandler) if "logfile" in pargs and pargs.logfile is not None: # Write logs to a logfile @@ -147,13 +161,13 @@ def reconfigure_logger(): def reload_conf(clear_load=False) -> None: """ - Reload config file. + Reload the config file. - After successful reload, call set_startup_complete() function to inform + After successful reload, call the set_startup_complete() function to inform systemd that all functions are available again. - If keys are commented out in conf file, they will still be in the conf file. - To remove not existing keys set clear_load to True. + If keys are commented out in the conf file, they will still be in the conf file. + To remove not existing keys, set clear_load to True. :param clear_load: Clear conf before reload """ @@ -161,11 +175,11 @@ def reload_conf(clear_load=False) -> None: # Inform systemd about reloading configuration _systemd_socket.sendto(b"RELOADING=1\n", _systemd_notify) - # Reset started up event for the set_startup_complete function + # Reset started-up event for the set_startup_complete function _daemon_started_up.clear() if "conffile" in pargs: - # Check config file + # Check the config file if not access(pargs.conffile, R_OK): raise RuntimeError("can not access config file '{0}'".format(pargs.conffile)) if conf_rw: @@ -212,7 +226,7 @@ def startup_complete(): this daemon are available so that the starts of further daemons can be properly timed. - The systemd unit file that is supposed to start this demon must be set + The systemd unit file supposed to start this demon must be set to 'Type=notify'. If the daemon supports reloading the settings, 'ExecReload=/bin/kill -HUP $MAINPID' must also be set. The daemon must call this function again after the reload in order to signal systemd the @@ -231,13 +245,13 @@ def startup_complete(): return if _systemd_notify: - # Inform systemd about complete startup of daemon process + # Inform systemd about the complete startup of the daemon process _systemd_socket.sendto(b"READY=1\n", _systemd_notify) if pargs.daemon: from os import kill - # Send SIGTERM signal to main process + # Send SIGTERM signal to the main process kill(_daemon_main_pid, 15) _daemon_started_up.set() @@ -246,35 +260,15 @@ def startup_complete(): # Generate command arguments of the program parser = ArgumentParser( prog=programname, - # todo: Add program description for help description="Program description", ) parser.add_argument("--version", action="version", version=f"%(prog)s {program_version}") -if can_be_forked(): - # Show the parameter only on systems that support fork call - parser.add_argument( - "-d", - "--daemon", - action="store_true", - dest="daemon", - help="run program as a daemon in background", - ) -parser.add_argument( - "-c", - "--conffile", - dest="conffile", - default="/etc/{0}/{0}.conf".format(programname), - help="application configuration file", -) parser.add_argument( "-f", "--logfile", dest="logfile", help="save log entries to this file", ) - -# TODO: Insert more arguments - parser.add_argument( "-v", "--verbose", @@ -292,68 +286,75 @@ if exists(open_source_licenses): dest="oss_licenses", help="print packed open-source-licenses and exit", ) -pargs = parser.parse_args() -# Process open-source-licenses argument, if set (only affects bundled apps) -if "oss_licenses" in pargs and pargs.oss_licenses: - with open(open_source_licenses, "r") as fh: - sys.stdout.write(fh.read()) - sys.exit(0) -# Check important objects and set to default if they do not exist -if "daemon" not in pargs: - pargs.daemon = False -if "verbose" not in pargs: - pargs.verbose = 0 - -# Check if the program should run as a daemon -if pargs.daemon: - # Check if daemon is already running - if exists(pidfile): - logger.error("Program already running as daemon. Check '{0}'".format(pidfile)) - sys.exit(1) - - # Fork to daemon - from os import fork - - pid = fork() - if pid > 0: - # Main process waits for exit till startup is complete - from os import kill - from signal import SIGKILL, SIGTERM, signal - - # Catch the TERM signal, which will be sent from the forked process after startup_complete - signal(SIGTERM, lambda number, frame: _daemon_started_up.set()) - - # Use the default timeout of 90 seconds from systemd also for the '--daemon' flag - if not _daemon_started_up.wait(90.0 + _extend_daemon_startup_timeout): - sys.stderr.write( - "Run into startup complete timout! Killing fork and exit main process\n" - ) - kill(pid, SIGKILL) - sys.exit(1) - - # Main process writes pidfile with pid of forked process - with open(pidfile, "w") as f: - f.write(str(pid)) +def init_app(logger_std_output: StdLogOutput = StdLogOutput.STDOUT): + global pargs + pargs = parser.parse_args() + # Process open-source-licenses argument, if set (only affects bundled apps) + if "oss_licenses" in pargs and pargs.oss_licenses: + with open(open_source_licenses, "r") as fh: + sys.stdout.write(fh.read()) sys.exit(0) + # Check important objects and set to default if they do not exist + if "daemon" not in pargs: + pargs.daemon = False + if "verbose" not in pargs: + pargs.verbose = 0 -# Get absolute paths -pwd = abspath(".") + # Check if the program should run as a daemon + if pargs.daemon: + # Check if the daemon is already running + if exists(pidfile): + logger.error("Program already running as daemon. Check '{0}'".format(pidfile)) + sys.exit(1) -# Configure logger -if "logfile" in pargs and pargs.logfile is not None and dirname(pargs.logfile) == "": - pargs.logfile = join(pwd, pargs.logfile) -reconfigure_logger() + # Fork to daemon + from os import fork -# Initialize configparser of globalconfig -if "conffile" in pargs and dirname(pargs.conffile) == "": - pargs.conffile = join(pwd, pargs.conffile) + pid = fork() + if pid > 0: + # The main process waits for exit till startup is complete + from os import kill + from signal import SIGKILL, SIGTERM, signal -# Load configuration - Comment out, if you do that in your own program -# reload_conf() + # Catch the TERM signal, which will be sent from the forked process after startup_complete + signal(SIGTERM, lambda number, frame: _daemon_started_up.set()) -# Log PID for development purposes -logger.debug("Running with PID {}".format(getpid())) + # Use the default timeout of 90 seconds from systemd also for the '--daemon' flag + if not _daemon_started_up.wait(90.0 + _extend_daemon_startup_timeout): + sys.stderr.write( + "Run into startup complete timout! Killing fork and exit main process\n" + ) + kill(pid, SIGKILL) + sys.exit(1) + + # Main process writes pidfile with pid of the forked process + with open(pidfile, "w") as f: + f.write(str(pid)) + + sys.exit(0) + + # Get absolute paths + pwd = abspath(".") + + # Configure logger + if "logfile" in pargs and pargs.logfile is not None and dirname(pargs.logfile) == "": + pargs.logfile = join(pwd, pargs.logfile) + reconfigure_logger(logger_std_output) + + # Initialize configparser of globalconfig + if "conffile" in pargs and dirname(pargs.conffile) == "": + pargs.conffile = join(pwd, pargs.conffile) + + # Load configuration - Comment out, if you do that in your own program + # reload_conf() + + # Log PID for development purposes + logger.debug("Running with PID {}".format(getpid())) + + +# Initialize global config +# init_app()