diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py index 67f00a1..6161941 100644 --- a/aprsd/aprsd.py +++ b/aprsd/aprsd.py @@ -22,8 +22,6 @@ # python included libs import datetime import logging -from logging import NullHandler -from logging.handlers import RotatingFileHandler import os import signal import sys @@ -36,7 +34,7 @@ import click_completion import aprsd from aprsd import cli_helper from aprsd import config as aprsd_config -from aprsd import messaging, packets, stats, threads, utils +from aprsd import log, messaging, packets, stats, threads, utils # setup the global logger @@ -56,52 +54,17 @@ def custom_startswith(string, incomplete): click_completion.core.startswith = custom_startswith click_completion.init() -cli_initialized = 0 -@click.group(cls=cli_helper.GroupWithCommandOptions, context_settings=CONTEXT_SETTINGS) -@click.option( - "--loglevel", - default="INFO", - show_default=True, - type=click.Choice( - ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], - case_sensitive=False, - ), - show_choices=True, - help="The log level to use for aprsd.log", -) -@click.option( - "-c", - "--config", - "config_file", - show_default=True, - default=aprsd_config.DEFAULT_CONFIG_FILE, - help="The aprsd config file to use for options.", -) -@click.option( - "--quiet", - is_flag=True, - default=False, - help="Don't log to stdout", -) +@click.group(context_settings=CONTEXT_SETTINGS) @click.version_option() @click.pass_context -def cli(ctx, loglevel, config_file, quiet): - global cli_initialized - # Have to do the global crap because the cli_helper GroupWithCommandOptions - # ends up calling this twice. - ctx.ensure_object(dict) - ctx.obj["loglevel"] = loglevel - ctx.obj["config_file"] = config_file - ctx.obj["quiet"] = quiet - ctx.obj["config"] = aprsd_config.parse_config(config_file) - if not cli_initialized: - setup_logging(ctx.obj["config"], loglevel, quiet) - cli_initialized = 1 +def cli(ctx): + pass def main(): + # First import all the possible commands for the CLI from .cmds import ( # noqa completion, dev, healthcheck, listen, send_message, server, ) @@ -130,57 +93,17 @@ def signal_handler(sig, frame): signal.signal(signal.SIGTERM, sys.exit(0)) -# Setup the logging faciility -# to disable logging to stdout, but still log to file -# use the --quiet option on the cmdln -def setup_logging(config, loglevel, quiet): - log_level = aprsd_config.LOG_LEVELS[loglevel] - LOG.setLevel(log_level) - log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) - date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) - log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) - log_file = config["aprsd"].get("logfile", None) - if log_file: - fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4) - else: - fh = NullHandler() - - fh.setFormatter(log_formatter) - LOG.addHandler(fh) - - imap_logger = None - if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False): - - imap_logger = logging.getLogger("imapclient.imaplib") - imap_logger.setLevel(log_level) - imap_logger.addHandler(fh) - - if config.get("aprsd.web.enabled", default=False): - qh = logging.handlers.QueueHandler(threads.logging_queue) - q_log_formatter = logging.Formatter( - fmt=aprsd_config.QUEUE_LOG_FORMAT, - datefmt=aprsd_config.QUEUE_DATE_FORMAT, - ) - qh.setFormatter(q_log_formatter) - LOG.addHandler(qh) - - if not quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - if imap_logger: - imap_logger.addHandler(sh) - - @cli.command() +@cli_helper.add_options(cli_helper.common_options) @click.pass_context +@cli_helper.process_standard_options def check_version(ctx): """Check this version against the latest in pypi.org.""" config_file = ctx.obj["config_file"] loglevel = ctx.obj["loglevel"] config = aprsd_config.parse_config(config_file) - setup_logging(config, loglevel, False) + log.setup_logging(config, loglevel, False) level, msg = utils._check_version() if level: LOG.warning(msg) diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py index 3a911c6..c20e732 100644 --- a/aprsd/cli_helper.py +++ b/aprsd/cli_helper.py @@ -1,5 +1,73 @@ +from functools import update_wrapper +import typing as t + import click +from aprsd import config as aprsd_config +from aprsd import log + + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + +common_options = [ + click.option( + "--loglevel", + default="INFO", + show_default=True, + type=click.Choice( + ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], + case_sensitive=False, + ), + show_choices=True, + help="The log level to use for aprsd.log", + ), + click.option( + "-c", + "--config", + "config_file", + show_default=True, + default=aprsd_config.DEFAULT_CONFIG_FILE, + help="The aprsd config file to use for options.", + ), + click.option( + "--quiet", + is_flag=True, + default=False, + help="Don't log to stdout", + ), +] + + +def add_options(options): + def _add_options(func): + for option in reversed(options): + func = option(func) + return func + return _add_options + + +def process_standard_options(f: F) -> F: + def new_func(*args, **kwargs): + print(f"ARGS {args}") + print(f"KWARGS {kwargs}") + ctx = args[0] + ctx.ensure_object(dict) + ctx.obj["loglevel"] = kwargs["loglevel"] + ctx.obj["config_file"] = kwargs["config_file"] + ctx.obj["quiet"] = kwargs["quiet"] + ctx.obj["config"] = aprsd_config.parse_config(kwargs["config_file"]) + log.setup_logging( + ctx.obj["config"], ctx.obj["loglevel"], + ctx.obj["quiet"], + ) + + del kwargs["loglevel"] + del kwargs["config_file"] + del kwargs["quiet"] + return f(*args, **kwargs) + + return update_wrapper(t.cast(F, new_func), f) + class AliasedGroup(click.Group): def command(self, *args, **kwargs): @@ -33,42 +101,3 @@ class AliasedGroup(click.Group): self.add_command(cmd, name=alias) return cmd return decorator - - -class GroupWithCommandOptions(click.Group): - """ Allow application of options to group with multi command """ - - def add_command(self, cmd, name=None): - click.Group.add_command(self, cmd, name=name) - - # add the group parameters to the command - for param in self.params: - cmd.params.append(param) - - # hook the commands invoke with our own - cmd.invoke = self.build_command_invoke(cmd.invoke) - self.invoke_without_command = True - - def build_command_invoke(self, original_invoke): - - def command_invoke(ctx): - """ insert invocation of group function """ - - # separate the group parameters - ctx.obj = dict(_params={}) - for param in self.params: - name = param.name - if name in ctx.params: - ctx.obj["_params"][name] = ctx.params[name] - del ctx.params[name] - - # call the group function with its parameters - params = ctx.params - ctx.params = ctx.obj["_params"] - self.invoke(ctx) - ctx.params = params - - # now call the original invoke(the command) - original_invoke(ctx) - - return command_invoke diff --git a/aprsd/cmds/completion.py b/aprsd/cmds/completion.py index 6b2adf7..274443b 100644 --- a/aprsd/cmds/completion.py +++ b/aprsd/cmds/completion.py @@ -14,7 +14,7 @@ def completion(ctx): # show dumps out the completion code for a particular shell -@completion.command(help="Show completion code for shell") +@completion.command(help="Show completion code for shell", name="show") @click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion") @click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells)) def show(shell, case_insensitive): @@ -24,7 +24,7 @@ def show(shell, case_insensitive): # install will install the completion code for a particular shell -@completion.command(help="Install completion code for a shell") +@completion.command(help="Install completion code for a shell", name="install") @click.option("--append/--overwrite", help="Append the completion code to the file", default=None) @click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion") @click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells)) diff --git a/aprsd/cmds/dev.py b/aprsd/cmds/dev.py index 160aed3..8c372d6 100644 --- a/aprsd/cmds/dev.py +++ b/aprsd/cmds/dev.py @@ -8,7 +8,7 @@ import logging import click # local imports here -from aprsd import client, plugin +from aprsd import cli_helper, client, plugin from ..aprsd import cli @@ -17,7 +17,14 @@ LOG = logging.getLogger("APRSD") CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) -@cli.command() +@cli.group(help="Development type subcommands", context_settings=CONTEXT_SETTINGS) +@click.pass_context +def dev(ctx): + pass + + +@dev.command() +@cli_helper.add_options(cli_helper.common_options) @click.option( "--aprs-login", envvar="APRS_LOGIN", @@ -51,6 +58,7 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) ) @click.argument("message", nargs=-1, required=True) @click.pass_context +@cli_helper.process_standard_options def test_plugin( ctx, aprs_login, @@ -59,12 +67,7 @@ def test_plugin( number, message, ): - """Test an APRSD plugin - - This allows you to develop a plugin and send a 'command' string - directly to the plugin during development/testing. Use this before - releasing as a plugin for aprsd. - """ + """Test an individual APRSD plugin given a python path.""" config = ctx.obj["config"] fromcall = aprs_login diff --git a/aprsd/cmds/healthcheck.py b/aprsd/cmds/healthcheck.py index e18728c..333dd05 100644 --- a/aprsd/cmds/healthcheck.py +++ b/aprsd/cmds/healthcheck.py @@ -13,7 +13,7 @@ import click import requests import aprsd -from aprsd import utils +from aprsd import cli_helper, utils # local imports here from ..aprsd import cli @@ -25,6 +25,7 @@ LOG = logging.getLogger("APRSD") @cli.command() +@cli_helper.add_options(cli_helper.common_options) @click.option( "--url", "health_url", @@ -39,6 +40,7 @@ LOG = logging.getLogger("APRSD") help="How long to wait for healtcheck url to come back", ) @click.pass_context +@cli_helper.process_standard_options def healthcheck(ctx, health_url, timeout): """Check the health of the running aprsd server.""" ctx.obj["config"] diff --git a/aprsd/cmds/listen.py b/aprsd/cmds/listen.py index f44e710..49d424c 100644 --- a/aprsd/cmds/listen.py +++ b/aprsd/cmds/listen.py @@ -13,7 +13,9 @@ import click # local imports here import aprsd -from aprsd import client, messaging, packets, stats, threads, trace, utils +from aprsd import ( + cli_helper, client, messaging, packets, stats, threads, trace, utils, +) from ..aprsd import cli @@ -36,6 +38,7 @@ def signal_handler(sig, frame): @cli.command() +@cli_helper.add_options(cli_helper.common_options) @click.option( "--aprs-login", envvar="APRS_LOGIN", @@ -54,6 +57,7 @@ def signal_handler(sig, frame): required=True, ) @click.pass_context +@cli_helper.process_standard_options def listen( ctx, aprs_login, diff --git a/aprsd/cmds/send_message.py b/aprsd/cmds/send_message.py index 9de82c2..d905b24 100644 --- a/aprsd/cmds/send_message.py +++ b/aprsd/cmds/send_message.py @@ -7,7 +7,7 @@ from aprslib.exceptions import LoginError import click import aprsd -from aprsd import client, messaging, packets +from aprsd import cli_helper, client, messaging, packets from ..aprsd import cli @@ -16,6 +16,7 @@ LOG = logging.getLogger("APRSD") @cli.command() +@cli_helper.add_options(cli_helper.common_options) @click.option( "--aprs-login", envvar="APRS_LOGIN", @@ -48,6 +49,7 @@ LOG = logging.getLogger("APRSD") @click.argument("tocallsign", required=True) @click.argument("command", nargs=-1, required=True) @click.pass_context +@cli_helper.process_standard_options def send_message( ctx, aprs_login, diff --git a/aprsd/cmds/server.py b/aprsd/cmds/server.py index e818c6b..48014f9 100644 --- a/aprsd/cmds/server.py +++ b/aprsd/cmds/server.py @@ -6,7 +6,8 @@ import click import aprsd from aprsd import ( - client, flask, messaging, packets, plugin, stats, threads, trace, utils, + cli_helper, client, flask, messaging, packets, plugin, stats, threads, + trace, utils, ) from aprsd import aprsd as aprsd_main @@ -18,6 +19,7 @@ LOG = logging.getLogger("APRSD") # main() ### @cli.command() +@cli_helper.add_options(cli_helper.common_options) @click.option( "-f", "--flush", @@ -28,6 +30,7 @@ LOG = logging.getLogger("APRSD") help="Flush out all old aged messages on disk.", ) @click.pass_context +@cli_helper.process_standard_options def server(ctx, flush): """Start the aprsd server gateway process.""" ctx.obj["config_file"] diff --git a/aprsd/log.py b/aprsd/log.py new file mode 100644 index 0000000..71ecf2c --- /dev/null +++ b/aprsd/log.py @@ -0,0 +1,53 @@ +import logging +from logging import NullHandler +from logging.handlers import RotatingFileHandler +import queue +import sys + +from aprsd import config as aprsd_config + + +LOG = logging.getLogger("APRSD") +logging_queue = queue.Queue() + + +# Setup the logging faciility +# to disable logging to stdout, but still log to file +# use the --quiet option on the cmdln +def setup_logging(config, loglevel, quiet): + log_level = aprsd_config.LOG_LEVELS[loglevel] + LOG.setLevel(log_level) + log_format = config["aprsd"].get("logformat", aprsd_config.DEFAULT_LOG_FORMAT) + date_format = config["aprsd"].get("dateformat", aprsd_config.DEFAULT_DATE_FORMAT) + log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) + log_file = config["aprsd"].get("logfile", None) + if log_file: + fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4) + else: + fh = NullHandler() + + fh.setFormatter(log_formatter) + LOG.addHandler(fh) + + imap_logger = None + if config.get("aprsd.email.enabled", default=False) and config.get("aprsd.email.imap.debug", default=False): + + imap_logger = logging.getLogger("imapclient.imaplib") + imap_logger.setLevel(log_level) + imap_logger.addHandler(fh) + + if config.get("aprsd.web.enabled", default=False): + qh = logging.handlers.QueueHandler(logging_queue) + q_log_formatter = logging.Formatter( + fmt=aprsd_config.QUEUE_LOG_FORMAT, + datefmt=aprsd_config.QUEUE_DATE_FORMAT, + ) + qh.setFormatter(q_log_formatter) + LOG.addHandler(qh) + + if not quiet: + sh = logging.StreamHandler(sys.stdout) + sh.setFormatter(log_formatter) + LOG.addHandler(sh) + if imap_logger: + imap_logger.addHandler(sh) diff --git a/aprsd/threads.py b/aprsd/threads.py index 3e6efb5..3d5350b 100644 --- a/aprsd/threads.py +++ b/aprsd/threads.py @@ -17,7 +17,6 @@ RX_THREAD = "RX" EMAIL_THREAD = "Email" rx_msg_queue = queue.Queue(maxsize=20) -logging_queue = queue.Queue() msg_queues = { "rx": rx_msg_queue, }