chore: Update proginit to 1.4.0

This commit is contained in:
2025-04-18 08:26:45 +02:00
parent 0380311a9d
commit 049ddfdc0f

View File

@@ -5,19 +5,20 @@
__author__ = "Sven Sager" __author__ = "Sven Sager"
__copyright__ = "Copyright (C) 2018-2023 Sven Sager" __copyright__ = "Copyright (C) 2018-2023 Sven Sager"
__license__ = "LGPL-2.0-or-later" __license__ = "LGPL-2.0-or-later"
__version__ = "1.3.2" __version__ = "1.4.0"
import logging import logging
import sys import sys
from argparse import ArgumentParser from argparse import ArgumentParser, Namespace
from configparser import ConfigParser from configparser import ConfigParser
from enum import Enum
from os import R_OK, W_OK, access, environ, getpid, remove from os import R_OK, W_OK, access, environ, getpid, remove
from os.path import abspath, dirname, exists, join from os.path import abspath, dirname, exists, join
from shutil import copy, move from shutil import copy, move
from threading import Event from threading import Event
try: 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 from .__about__ import __version__ as external_version
except Exception: except Exception:
external_version = None external_version = None
@@ -26,11 +27,12 @@ programname = "revpi-middleware" # Program name
program_version = external_version or "a.b.c" 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 = 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_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 old conf file [filename].bak conf_rw_backup = False # Keep a backup of an old conf file [filename].bak
_extend_daemon_startup_timeout = 0.0 # Default startup timeout is 90 seconds _extend_daemon_startup_timeout = 0.0 # The default startup timeout is 90 seconds
conf = ConfigParser() conf = ConfigParser()
pargs = Namespace(daemon=False, verbose=0)
logger = logging.getLogger() logger = logging.getLogger()
pidfile = "/var/run/{0}.pid".format(programname) pidfile = "/var/run/{0}.pid".format(programname)
_daemon_started_up = Event() _daemon_started_up = Event()
@@ -90,11 +92,19 @@ def cleanup():
sys.stdout.close() 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.""" """Configure logging module of program."""
class FilterDebug(logging.Filter): 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: def filter(self, record: logging.LogRecord) -> bool:
remove_record = False remove_record = False
@@ -104,29 +114,33 @@ def reconfigure_logger():
return not remove_record return not remove_record
# Clear all log handler # Clear all log handlers
for lhandler in logger.handlers.copy(): for lhandler in logger.handlers.copy():
lhandler.close() lhandler.close()
logger.removeHandler(lhandler) logger.removeHandler(lhandler)
if pargs.daemon: if pargs.daemon:
# Create daemon log file # Create the daemon log file
fh_logfile = open("/var/log/{0}.log".format(programname), "a") 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.close()
sys.stdout = fh_logfile sys.stdout = fh_logfile
sys.stderr = sys.stdout sys.stderr = sys.stdout
# Create new log handler # Create the new log handler
if pargs.verbose > 2: if pargs.verbose > 2:
log_frm = "{asctime} [{levelname:8}] {name} {message}" log_frm = "{asctime} [{levelname:8}] {name} {message}"
else: else:
log_frm = "{asctime} [{levelname:8}] {message}" log_frm = "{asctime} [{levelname:8}] {message}"
logformat = logging.Formatter(log_frm, datefmt="%Y-%m-%d %H:%M:%S", style="{") logformat = logging.Formatter(log_frm, datefmt="%Y-%m-%d %H:%M:%S", style="{")
lhandler = logging.StreamHandler(sys.stdout)
lhandler.setFormatter(logformat) if std_output is not StdLogOutput.NONE:
logger.addHandler(lhandler) 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: if "logfile" in pargs and pargs.logfile is not None:
# Write logs to a logfile # Write logs to a logfile
@@ -147,13 +161,13 @@ def reconfigure_logger():
def reload_conf(clear_load=False) -> None: 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. systemd that all functions are available again.
If keys are commented out in conf file, they will still be in the conf file. 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. To remove not existing keys, set clear_load to True.
:param clear_load: Clear conf before reload :param clear_load: Clear conf before reload
""" """
@@ -161,11 +175,11 @@ def reload_conf(clear_load=False) -> None:
# Inform systemd about reloading configuration # Inform systemd about reloading configuration
_systemd_socket.sendto(b"RELOADING=1\n", _systemd_notify) _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() _daemon_started_up.clear()
if "conffile" in pargs: if "conffile" in pargs:
# Check config file # Check the config file
if not access(pargs.conffile, R_OK): if not access(pargs.conffile, R_OK):
raise RuntimeError("can not access config file '{0}'".format(pargs.conffile)) raise RuntimeError("can not access config file '{0}'".format(pargs.conffile))
if conf_rw: if conf_rw:
@@ -212,7 +226,7 @@ def startup_complete():
this daemon are available so that the starts of further daemons can be this daemon are available so that the starts of further daemons can be
properly timed. 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, to 'Type=notify'. If the daemon supports reloading the settings,
'ExecReload=/bin/kill -HUP $MAINPID' must also be set. The daemon must '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 call this function again after the reload in order to signal systemd the
@@ -231,13 +245,13 @@ def startup_complete():
return return
if _systemd_notify: 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) _systemd_socket.sendto(b"READY=1\n", _systemd_notify)
if pargs.daemon: if pargs.daemon:
from os import kill from os import kill
# Send SIGTERM signal to main process # Send SIGTERM signal to the main process
kill(_daemon_main_pid, 15) kill(_daemon_main_pid, 15)
_daemon_started_up.set() _daemon_started_up.set()
@@ -246,35 +260,15 @@ def startup_complete():
# Generate command arguments of the program # Generate command arguments of the program
parser = ArgumentParser( parser = ArgumentParser(
prog=programname, prog=programname,
# todo: Add program description for help
description="Program description", description="Program description",
) )
parser.add_argument("--version", action="version", version=f"%(prog)s {program_version}") 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( parser.add_argument(
"-f", "-f",
"--logfile", "--logfile",
dest="logfile", dest="logfile",
help="save log entries to this file", help="save log entries to this file",
) )
# TODO: Insert more arguments
parser.add_argument( parser.add_argument(
"-v", "-v",
"--verbose", "--verbose",
@@ -292,68 +286,75 @@ if exists(open_source_licenses):
dest="oss_licenses", dest="oss_licenses",
help="print packed open-source-licenses and exit", 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 def init_app(logger_std_output: StdLogOutput = StdLogOutput.STDOUT):
if "daemon" not in pargs: global pargs
pargs.daemon = False pargs = parser.parse_args()
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))
# 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) 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 # Check if the program should run as a daemon
pwd = abspath(".") 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 # Fork to daemon
if "logfile" in pargs and pargs.logfile is not None and dirname(pargs.logfile) == "": from os import fork
pargs.logfile = join(pwd, pargs.logfile)
reconfigure_logger()
# Initialize configparser of globalconfig pid = fork()
if "conffile" in pargs and dirname(pargs.conffile) == "": if pid > 0:
pargs.conffile = join(pwd, pargs.conffile) # 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 # Catch the TERM signal, which will be sent from the forked process after startup_complete
# reload_conf() signal(SIGTERM, lambda number, frame: _daemon_started_up.set())
# Log PID for development purposes # Use the default timeout of 90 seconds from systemd also for the '--daemon' flag
logger.debug("Running with PID {}".format(getpid())) 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()