From 0d5b7166b3b787558b724d457054654965e8ae33 Mon Sep 17 00:00:00 2001 From: Hemna Date: Tue, 2 Nov 2021 11:47:40 -0400 Subject: [PATCH 01/12] Updated Changelog --- ChangeLog | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 827fd51..5e30306 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,6 +1,12 @@ CHANGES ======= +v2.4.2 +------ + +* Be more careful picking data to/from disk +* Updated Changelog + v2.4.1 ------ @@ -38,11 +44,11 @@ v2.4.0 * Refactored client classes * Refactor utils usage * 2.3.1 Changelog -* Fixed issue of aprs-is missing keepalive v2.3.1 ------ +* Fixed issue of aprs-is missing keepalive * Fixed packet processing issue with aprsd send-message v2.3.0 From 49f3ea83392b98e5d1e362c37282819fb6f8dce7 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 10:39:47 -0400 Subject: [PATCH 02/12] Fixed a problem with send-message command This patch fixes a problem with the packets object not being initialized correctly for the send-message command from the command line. Also adds the --wait-response option for send-message, which by default is now False --- aprsd/main.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/aprsd/main.py b/aprsd/main.py index b07f8fd..78ff12c 100644 --- a/aprsd/main.py +++ b/aprsd/main.py @@ -276,6 +276,14 @@ def sample_config(): default=False, help="Don't wait for an ack, just sent it to APRS-IS and bail.", ) +@click.option( + "--wait-response", + "-w", + is_flag=True, + show_default=True, + default=False, + help="Wait for a response to the message?", +) @click.option("--raw", default=None, help="Send a raw message. Implies --no-ack") @click.argument("tocallsign", required=False) @click.argument("command", nargs=-1, required=False) @@ -286,6 +294,7 @@ def send_message( aprs_login, aprs_password, no_ack, + wait_response, raw, tocallsign, command, @@ -316,6 +325,9 @@ def send_message( else: LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") + packets.WatchList(config=config) + packets.SeenList(config=config) + got_ack = False got_response = False @@ -348,8 +360,12 @@ def send_message( ) ack.send_direct() - if got_ack and got_response: - sys.exit(0) + if got_ack: + if wait_response: + if got_response: + sys.exit(0) + else: + sys.exit(0) try: client.ClientFactory.setup(config) From 30df452e007b023e28dbeb1b7b19d702251bd045 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 10:51:22 -0400 Subject: [PATCH 03/12] Updated Changelog for 4.2.3 --- ChangeLog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ChangeLog b/ChangeLog index 5e30306..7206e72 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,9 +1,15 @@ CHANGES ======= +v4.2.3 +------ + +* Fixed a problem with send-message command + v2.4.2 ------ +* Updated Changelog * Be more careful picking data to/from disk * Updated Changelog From 7d0006b0a6c4b05c14214609fcfe718ae7f0c146 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 13:36:33 -0400 Subject: [PATCH 04/12] Refactor the cli This patch refactors the cli to incorporate the dev, send-message, listen commands into the main aprsd app. This also moves the command line completion installer/show into it's own subgroup. --- aprsd/aprsd.py | 197 ++++++++++++++ aprsd/cli_helper.py | 35 +++ aprsd/clients/__init__.py | 0 aprsd/cmds/__init__.py | 0 aprsd/cmds/completion.py | 35 +++ aprsd/cmds/dev.py | 104 +++++++ aprsd/cmds/listen.py | 152 +++++++++++ aprsd/cmds/send_message.py | 162 +++++++++++ aprsd/cmds/server.py | 119 ++++++++ aprsd/dev.py | 251 ----------------- aprsd/listen.py | 394 --------------------------- aprsd/main.py | 536 ------------------------------------- setup.cfg | 4 +- 13 files changed, 805 insertions(+), 1184 deletions(-) create mode 100644 aprsd/aprsd.py create mode 100644 aprsd/cli_helper.py create mode 100644 aprsd/clients/__init__.py create mode 100644 aprsd/cmds/__init__.py create mode 100644 aprsd/cmds/completion.py create mode 100644 aprsd/cmds/dev.py create mode 100644 aprsd/cmds/listen.py create mode 100644 aprsd/cmds/send_message.py create mode 100644 aprsd/cmds/server.py delete mode 100644 aprsd/dev.py delete mode 100644 aprsd/listen.py delete mode 100644 aprsd/main.py diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py new file mode 100644 index 0000000..2a33154 --- /dev/null +++ b/aprsd/aprsd.py @@ -0,0 +1,197 @@ +# +# Listen on amateur radio aprs-is network for messages and respond to them. +# You must have an amateur radio callsign to use this software. You must +# create an ~/.aprsd/config.yml file with all of the required settings. To +# generate an example config.yml, just run aprsd, then copy the sample config +# to ~/.aprsd/config.yml and edit the settings. +# +# APRS messages: +# l(ocation) = descriptive location of calling station +# w(eather) = temp, (hi/low) forecast, later forecast +# t(ime) = respond with the current time +# f(ortune) = respond with a short fortune +# -email_addr email text = send an email +# -2 = display the last 2 emails received +# p(ing) = respond with Pong!/time +# anything else = respond with usage +# +# (C)2018 Craig Lamparter +# License GPLv2 +# + +# python included libs +import datetime +import logging +from logging import NullHandler +from logging.handlers import RotatingFileHandler +import os +import signal +import sys +import time + +import click +import click_completion + +# local imports here +import aprsd +from aprsd import config as aprsd_config +from aprsd import messaging, packets, stats, threads, utils +from aprsd.cli_helper import AliasedGroup + + +# setup the global logger +# logging.basicConfig(level=logging.DEBUG) # level=10 +LOG = logging.getLogger("APRSD") +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) +flask_enabled = False + + +def custom_startswith(string, incomplete): + """A custom completion match that supports case insensitive matching.""" + if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): + string = string.lower() + incomplete = incomplete.lower() + return string.startswith(incomplete) + + +click_completion.core.startswith = custom_startswith +click_completion.init() + + +@click.group(cls=AliasedGroup, 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.version_option() +@click.pass_context +def cli(ctx, loglevel, config_file, quiet): + 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) + setup_logging(ctx.obj["config"], loglevel, quiet) + +def main(): + from .cmds import completion, dev, listen, send_message, server # noqa + cli() + + +def signal_handler(sig, frame): + global flask_enabled + + click.echo("signal_handler: called") + threads.APRSDThreadList().stop_all() + if "subprocess" not in str(frame): + LOG.info( + "Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format( + datetime.datetime.now(), + ), + ) + time.sleep(1.5) + messaging.MsgTrack().save() + packets.WatchList().save() + packets.SeenList().save() + LOG.info(stats.APRSDStats()) + # signal.signal(signal.SIGTERM, sys.exit(0)) + # sys.exit(0) + if flask_enabled: + 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() +@click.pass_context +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) + level, msg = utils._check_version() + if level: + LOG.warning(msg) + else: + LOG.info(msg) + + +@cli.command() +@click.pass_context +def sample_config(ctx): + """This dumps the config to stdout.""" + click.echo(aprsd_config.dump_default_cfg()) + + +@cli.command() +@click.pass_context +def version(ctx): + """Show the APRSD version.""" + click.echo(f"APRSD Version : {aprsd.__version__}") + + +if __name__ == "__main__": + main() diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py new file mode 100644 index 0000000..ef83aae --- /dev/null +++ b/aprsd/cli_helper.py @@ -0,0 +1,35 @@ +import click + + +class AliasedGroup(click.Group): + def command(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + Copied from `click` and extended for `aliases`. + """ + def decorator(f): + aliases = kwargs.pop("aliases", []) + cmd = click.decorators.command(*args, **kwargs)(f) + self.add_command(cmd) + for alias in aliases: + self.add_command(cmd, name=alias) + return cmd + return decorator + + def group(self, *args, **kwargs): + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` but + immediately registers the created command with this instance by + calling into :meth:`add_command`. + Copied from `click` and extended for `aliases`. + """ + def decorator(f): + aliases = kwargs.pop("aliases", []) + cmd = click.decorators.group(*args, **kwargs)(f) + self.add_command(cmd) + for alias in aliases: + self.add_command(cmd, name=alias) + return cmd + return decorator diff --git a/aprsd/clients/__init__.py b/aprsd/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aprsd/cmds/__init__.py b/aprsd/cmds/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aprsd/cmds/completion.py b/aprsd/cmds/completion.py new file mode 100644 index 0000000..55621a3 --- /dev/null +++ b/aprsd/cmds/completion.py @@ -0,0 +1,35 @@ +import click +import click_completion + +from ..aprsd import cli + + +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + +@cli.group(help="Click Completion subcommands", context_settings=CONTEXT_SETTINGS) +@click.pass_context +def completion(ctx): + pass + + +# show dumps out the completion code for a particular shell +@completion.command(help="Show completion code for shell") +@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): + """Show the click-completion-command completion code""" + extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {} + click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) + + +# install will install the completion code for a particular shell +@completion.command(help="Install completion code for a shell") +@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)) +@click.argument("path", required=False) +def install(append, case_insensitive, shell, path): + """Install the click-completion-command completion""" + extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {} + shell, path = click_completion.core.install(shell=shell, path=path, append=append, extra_env=extra_env) + click.echo(f"{shell} completion installed in {path}") diff --git a/aprsd/cmds/dev.py b/aprsd/cmds/dev.py new file mode 100644 index 0000000..5a910e5 --- /dev/null +++ b/aprsd/cmds/dev.py @@ -0,0 +1,104 @@ +# +# Dev.py is used to help develop plugins +# +# +# python included libs +import logging + +import click + +# local imports here +from aprsd import client, plugin + +from ..aprsd import cli + + +LOG = logging.getLogger("APRSD") +CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + + +@cli.group(help="Development tools", context_settings=CONTEXT_SETTINGS) +@click.pass_context +def dev(ctx): + pass + + +@dev.command() +@click.option( + "-p", + "--plugin", + "plugin_path", + show_default=True, + default=None, + help="The plugin to run. Ex: aprsd.plugins.ping.PingPlugin", +) +@click.option( + "-a", + "--all", + "load_all", + show_default=True, + is_flag=True, + default=False, + help="Load all the plugins in config?", +) +@click.option( + "-n", + "--num", + "number", + show_default=True, + default=1, + help="Number of times to call the plugin", +) +@click.argument("fromcall") +@click.argument("message", nargs=-1, required=True) +@click.pass_context +def test_plugin( + ctx, + plugin_path, + load_all, + number, + fromcall, + message, +): + """APRSD Plugin test app.""" + config = ctx.obj["config"] + + if not plugin_path: + click.echo(ctx.get_help()) + click.echo("") + ctx.fail("Failed to provide -p option to test a plugin") + ctx.exit() + + if type(message) is tuple: + message = " ".join(message) + client.Client(config) + + pm = plugin.PluginManager(config) + if load_all: + pm.setup_plugins() + else: + pm._init() + obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) + # Register the plugin they wanted tested. + LOG.info( + "Testing plugin {} Version {}".format( + obj.__class__, obj.version, + ), + ) + pm._pluggy_pm.register(obj) + login = config["aprs"]["login"] + + packet = { + "from": fromcall, "addresse": login, + "message_text": message, + "format": "message", + "msgNo": 1, + } + LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'") + + for x in range(number): + reply = pm.run(packet) + # Plugin might have threads, so lets stop them so we can exit. + # obj.stop_threads() + LOG.info(f"Result{x} = '{reply}'") + pm.stop() diff --git a/aprsd/cmds/listen.py b/aprsd/cmds/listen.py new file mode 100644 index 0000000..69f258d --- /dev/null +++ b/aprsd/cmds/listen.py @@ -0,0 +1,152 @@ +# +# License GPLv2 +# + +# python included libs +import datetime +import logging +import sys +import time + +import aprslib +import click + +# local imports here +import aprsd +from aprsd import client, messaging, packets, stats, threads, trace, utils + +from ..aprsd import cli + + +# setup the global logger +# logging.basicConfig(level=logging.DEBUG) # level=10 +LOG = logging.getLogger("APRSD") + + +def signal_handler(sig, frame): + threads.APRSDThreadList().stop_all() + if "subprocess" not in str(frame): + LOG.info( + "Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format( + datetime.datetime.now(), + ), + ) + time.sleep(5) + LOG.info(stats.APRSDStats()) + + +@cli.command() +@click.option( + "--aprs-login", + envvar="APRS_LOGIN", + show_envvar=True, + help="What callsign to send the message from.", +) +@click.option( + "--aprs-password", + envvar="APRS_PASSWORD", + show_envvar=True, + help="the APRS-IS password for APRS_LOGIN", +) +@click.argument( + "filter", + nargs=-1, + required=True, +) +@click.pass_context +def listen( + ctx, + aprs_login, + aprs_password, + filter, +): + """Listen to packets on the APRS-IS Network based on FILTER. + + FILTER is the APRS Filter to use. see http://www.aprs-is.net/javAPRSFilter.aspx + + """ + config = ctx.obj["config"] + + if not aprs_login: + click.echo(ctx.get_help()) + click.echo("") + ctx.fail("Must set --aprs_login or APRS_LOGIN") + ctx.exit() + + if not aprs_password: + click.echo(ctx.get_help()) + click.echo("") + ctx.fail("Must set --aprs-password or APRS_PASSWORD") + ctx.exit() + + config["aprs"]["login"] = aprs_login + config["aprs"]["password"] = aprs_password + + LOG.info(f"APRSD Listen Started version: {aprsd.__version__}") + + flat_config = utils.flatten_dict(config) + LOG.info("Using CONFIG values:") + for x in flat_config: + if "password" in x or "aprsd.web.users.admin" in x: + LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") + else: + LOG.info(f"{x} = {flat_config[x]}") + + stats.APRSDStats(config) + + # Try and load saved MsgTrack list + LOG.debug("Loading saved MsgTrack object.") + messaging.MsgTrack(config=config).load() + packets.WatchList(config=config).load() + packets.SeenList(config=config).load() + + @trace.trace + def rx_packet(packet): + resp = packet.get("response", None) + if resp == "ack": + ack_num = packet.get("msgNo") + LOG.info(f"We saw an ACK {ack_num} Ignoring") + messaging.log_packet(packet) + else: + message = packet.get("message_text", None) + fromcall = packet["from"] + msg_number = packet.get("msgNo", "0") + messaging.log_message( + "Received Message", + packet["raw"], + message, + fromcall=fromcall, + ack=msg_number, + ) + + # Initialize the client factory and create + # The correct client object ready for use + client.ClientFactory.setup(config) + # Make sure we have 1 client transport enabled + if not client.factory.is_client_enabled(): + LOG.error("No Clients are enabled in config.") + sys.exit(-1) + + # Creates the client object + LOG.info("Creating client connection") + client.factory.create().client + aprs_client = client.factory.create().client + + LOG.debug(f"Filter by '{filter}'") + aprs_client.set_filter(filter) + + while True: + try: + # This will register a packet consumer with aprslib + # When new packets come in the consumer will process + # the packet + aprs_client.consumer(rx_packet, raw=False) + except aprslib.exceptions.ConnectionDrop: + LOG.error("Connection dropped, reconnecting") + time.sleep(5) + # Force the deletion of the client object connected to aprs + # This will cause a reconnect, next time client.get_client() + # is called + aprs_client.reset() + except aprslib.exceptions.UnknownFormat: + LOG.error("Got a Bad packet") diff --git a/aprsd/cmds/send_message.py b/aprsd/cmds/send_message.py new file mode 100644 index 0000000..89f770c --- /dev/null +++ b/aprsd/cmds/send_message.py @@ -0,0 +1,162 @@ +import logging +import sys +import time + +import aprslib +from aprslib.exceptions import LoginError +import click + +import aprsd +from aprsd import client, messaging, packets + +from ..aprsd import cli + + +LOG = logging.getLogger("APRSD") + + +@cli.command() +@click.option( + "--aprs-login", + envvar="APRS_LOGIN", + show_envvar=True, + help="What callsign to send the message from.", +) +@click.option( + "--aprs-password", + envvar="APRS_PASSWORD", + show_envvar=True, + help="the APRS-IS password for APRS_LOGIN", +) +@click.option( + "--no-ack", + "-n", + is_flag=True, + show_default=True, + default=False, + help="Don't wait for an ack, just sent it to APRS-IS and bail.", +) +@click.option( + "--wait-response", + "-w", + is_flag=True, + show_default=True, + default=False, + help="Wait for a response to the message?", +) +@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack") +@click.argument("tocallsign", required=False) +@click.argument("command", nargs=-1, required=False) +@click.pass_context +def send_message( + ctx, + aprs_login, + aprs_password, + no_ack, + wait_response, + raw, + tocallsign, + command, +): + """Send a message to a callsign via APRS_IS.""" + global got_ack, got_response + config = ctx.obj["config"] + quiet = ctx.obj["quiet"] + + if not aprs_login: + click.echo("Must set --aprs_login or APRS_LOGIN") + return + + if not aprs_password: + click.echo("Must set --aprs-password or APRS_PASSWORD") + return + + config["aprs"]["login"] = aprs_login + config["aprs"]["password"] = aprs_password + + LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}") + if type(command) is tuple: + command = " ".join(command) + if not quiet: + if raw: + LOG.info(f"L'{aprs_login}' R'{raw}'") + else: + LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") + + got_ack = False + got_response = False + + def rx_packet(packet): + global got_ack, got_response + # LOG.debug("Got packet back {}".format(packet)) + resp = packet.get("response", None) + if resp == "ack": + ack_num = packet.get("msgNo") + LOG.info(f"We got ack for our sent message {ack_num}") + messaging.log_packet(packet) + got_ack = True + else: + message = packet.get("message_text", None) + fromcall = packet["from"] + msg_number = packet.get("msgNo", "0") + messaging.log_message( + "Received Message", + packet["raw"], + message, + fromcall=fromcall, + ack=msg_number, + ) + got_response = True + # Send the ack back? + ack = messaging.AckMessage( + config["aprs"]["login"], + fromcall, + msg_id=msg_number, + ) + ack.send_direct() + + if got_ack: + if wait_response: + if got_response: + sys.exit(0) + else: + sys.exit(0) + + try: + client.ClientFactory.setup(config) + client.factory.create().client + except LoginError: + sys.exit(-1) + + packets.PacketList(config=config) + packets.WatchList(config=config) + + # Send a message + # then we setup a consumer to rx messages + # We should get an ack back as well as a new message + # we should bail after we get the ack and send an ack back for the + # message + if raw: + msg = messaging.RawMessage(raw) + msg.send_direct() + sys.exit(0) + else: + msg = messaging.TextMessage(aprs_login, tocallsign, command) + msg.send_direct() + + if no_ack: + sys.exit(0) + + try: + # This will register a packet consumer with aprslib + # When new packets come in the consumer will process + # the packet + aprs_client = client.factory.create().client + aprs_client.consumer(rx_packet, raw=False) + except aprslib.exceptions.ConnectionDrop: + LOG.error("Connection dropped, reconnecting") + time.sleep(5) + # Force the deletion of the client object connected to aprs + # This will cause a reconnect, next time client.get_client() + # is called + aprs_client.reset() diff --git a/aprsd/cmds/server.py b/aprsd/cmds/server.py new file mode 100644 index 0000000..bb08f7d --- /dev/null +++ b/aprsd/cmds/server.py @@ -0,0 +1,119 @@ +import logging +import signal +import sys + +import click + +import aprsd +from aprsd import ( + client, flask, messaging, packets, plugin, stats, threads, trace, utils, +) +from aprsd import aprsd as aprsd_main + +from ..aprsd import cli + + +LOG = logging.getLogger("APRSD") + + +# main() ### +@cli.command() +@click.option( + "-f", + "--flush", + "flush", + is_flag=True, + show_default=True, + default=False, + help="Flush out all old aged messages on disk.", +) +@click.pass_context +def server(ctx, flush): + """Start the aprsd server process.""" + ctx.obj["config_file"] + loglevel = ctx.obj["loglevel"] + quiet = ctx.obj["quiet"] + config = ctx.obj["config"] + + signal.signal(signal.SIGINT, aprsd_main.signal_handler) + signal.signal(signal.SIGTERM, aprsd_main.signal_handler) + + if not quiet: + click.echo("Load config") + + level, msg = utils._check_version() + if level: + LOG.warning(msg) + else: + LOG.info(msg) + LOG.info(f"APRSD Started version: {aprsd.__version__}") + + flat_config = utils.flatten_dict(config) + LOG.info("Using CONFIG values:") + for x in flat_config: + if "password" in x or "aprsd.web.users.admin" in x: + LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") + else: + LOG.info(f"{x} = {flat_config[x]}") + + if config["aprsd"].get("trace", False): + trace.setup_tracing(["method", "api"]) + stats.APRSDStats(config) + + # Initialize the client factory and create + # The correct client object ready for use + client.ClientFactory.setup(config) + # Make sure we have 1 client transport enabled + if not client.factory.is_client_enabled(): + LOG.error("No Clients are enabled in config.") + sys.exit(-1) + + # Creates the client object + LOG.info("Creating client connection") + client.factory.create().client + + # Now load the msgTrack from disk if any + packets.PacketList(config=config) + if flush: + LOG.debug("Deleting saved MsgTrack.") + messaging.MsgTrack(config=config).flush() + packets.WatchList(config=config) + packets.SeenList(config=config) + else: + # Try and load saved MsgTrack list + LOG.debug("Loading saved MsgTrack object.") + messaging.MsgTrack(config=config).load() + packets.WatchList(config=config).load() + packets.SeenList(config=config).load() + + # Create the initial PM singleton and Register plugins + LOG.info("Loading Plugin Manager and registering plugins") + plugin_manager = plugin.PluginManager(config) + plugin_manager.setup_plugins() + + rx_thread = threads.APRSDRXThread( + msg_queues=threads.msg_queues, + config=config, + ) + rx_thread.start() + + messaging.MsgTrack().restart() + + keepalive = threads.KeepAliveThread(config=config) + keepalive.start() + + web_enabled = config.get("aprsd.web.enabled", default=False) + + if web_enabled: + aprsd_main.flask_enabled = True + (socketio, app) = flask.init_flask(config, loglevel, quiet) + socketio.run( + app, + host=config["aprsd"]["web"]["host"], + port=config["aprsd"]["web"]["port"], + ) + + # If there are items in the msgTracker, then save them + LOG.info("APRSD Exiting.") + return 0 +import logging diff --git a/aprsd/dev.py b/aprsd/dev.py deleted file mode 100644 index eb952e2..0000000 --- a/aprsd/dev.py +++ /dev/null @@ -1,251 +0,0 @@ -# -# Dev.py is used to help develop plugins -# -# -# python included libs -import logging -from logging import NullHandler -from logging.handlers import RotatingFileHandler -import os -import sys - -import click -import click_completion - -# local imports here -import aprsd -from aprsd import client -from aprsd import config as aprsd_config -from aprsd import plugin - - -# setup the global logger -# logging.basicConfig(level=logging.DEBUG) # level=10 -LOG = logging.getLogger("APRSD") - -LOG_LEVELS = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, -} - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -def custom_startswith(string, incomplete): - """A custom completion match that supports case insensitive matching.""" - if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): - string = string.lower() - incomplete = incomplete.lower() - return string.startswith(incomplete) - - -click_completion.core.startswith = custom_startswith -click_completion.init() - - -cmd_help = """Shell completion for click-completion-command -Available shell types: -\b - %s -Default type: auto -""" % "\n ".join( - f"{k:<12} {click_completion.core.shells[k]}" - for k in sorted(click_completion.core.shells.keys()) -) - - -@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS) -@click.version_option() -def main(): - pass - - -@main.command() -@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): - """Show the click-completion-command completion code""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) - - -@main.command() -@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), -) -@click.argument("path", required=False) -def install(append, case_insensitive, shell, path): - """Install the click-completion-command completion""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - shell, path = click_completion.core.install( - shell=shell, - path=path, - append=append, - extra_env=extra_env, - ) - click.echo(f"{shell} completion installed in {path}") - - -# 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) - - if not quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - - -@main.command() -@click.option( - "--loglevel", - default="DEBUG", - 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( - "-p", - "--plugin", - "plugin_path", - show_default=True, - default="aprsd.plugins.wx.WxPlugin", - help="The plugin to run", -) -@click.option( - "-a", - "--all", - "load_all", - show_default=True, - is_flag=True, - default=False, - help="Load all the plugins in config?", -) -@click.option( - "-n", - "--num", - "number", - show_default=True, - default=1, - help="Number of times to call the plugin", -) -@click.argument("fromcall") -@click.argument("message", nargs=-1, required=True) -def test_plugin( - loglevel, - config_file, - plugin_path, - load_all, - number, - fromcall, - message, -): - """APRSD Plugin test app.""" - - config = aprsd_config.parse_config(config_file) - setup_logging(config, loglevel, False) - - LOG.info(f"Test APRSD Plgin version: {aprsd.__version__}") - if type(message) is tuple: - message = " ".join(message) - client.Client(config) - - pm = plugin.PluginManager(config) - if load_all: - pm.setup_plugins() - else: - pm._init() - obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) - # Register the plugin they wanted tested. - LOG.info( - "Testing plugin {} Version {}".format( - obj.__class__, obj.version, - ), - ) - pm._pluggy_pm.register(obj) - login = config["aprs"]["login"] - - packet = { - "from": fromcall, "addresse": login, - "message_text": message, - "format": "message", - "msgNo": 1, - } - LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'") - - for x in range(number): - reply = pm.run(packet) - # Plugin might have threads, so lets stop them so we can exit. - # obj.stop_threads() - LOG.info(f"Result{x} = '{reply}'") - pm.stop() - - -if __name__ == "__main__": - main() diff --git a/aprsd/listen.py b/aprsd/listen.py deleted file mode 100644 index 40d90b7..0000000 --- a/aprsd/listen.py +++ /dev/null @@ -1,394 +0,0 @@ -# -# Listen on amateur radio aprs-is network for messages and respond to them. -# You must have an amateur radio callsign to use this software. You must -# create an ~/.aprsd/config.yml file with all of the required settings. To -# generate an example config.yml, just run aprsd, then copy the sample config -# to ~/.aprsd/config.yml and edit the settings. -# -# APRS messages: -# l(ocation) = descriptive location of calling station -# w(eather) = temp, (hi/low) forecast, later forecast -# t(ime) = respond with the current time -# f(ortune) = respond with a short fortune -# -email_addr email text = send an email -# -2 = display the last 2 emails received -# p(ing) = respond with Pong!/time -# anything else = respond with usage -# -# (C)2018 Craig Lamparter -# License GPLv2 -# - -# python included libs -import datetime -import logging -from logging import NullHandler -from logging.handlers import RotatingFileHandler -import os -import signal -import sys -import time - -import aprslib -from aprslib.exceptions import LoginError -import click -import click_completion - -# local imports here -import aprsd -from aprsd import client -from aprsd import config as aprsd_config -from aprsd import messaging, stats, threads, trace, utils - - -# setup the global logger -# logging.basicConfig(level=logging.DEBUG) # level=10 -LOG = logging.getLogger("APRSD") - - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - -flask_enabled = False - -# server_event = threading.Event() - -# localization, please edit: -# HOST = "noam.aprs2.net" # north america tier2 servers round robin -# USER = "KM6XXX-9" # callsign of this aprs client with SSID -# PASS = "99999" # google how to generate this -# BASECALLSIGN = "KM6XXX" # callsign of radio in the field to send email -# shortcuts = { -# "aa" : "5551239999@vtext.com", -# "cl" : "craiglamparter@somedomain.org", -# "wb" : "5553909472@vtext.com" -# } - - -def custom_startswith(string, incomplete): - """A custom completion match that supports case insensitive matching.""" - if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): - string = string.lower() - incomplete = incomplete.lower() - return string.startswith(incomplete) - - -click_completion.core.startswith = custom_startswith -click_completion.init() - - -cmd_help = """Shell completion for click-completion-command -Available shell types: -\b - %s -Default type: auto -""" % "\n ".join( - f"{k:<12} {click_completion.core.shells[k]}" - for k in sorted(click_completion.core.shells.keys()) -) - - -@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS) -@click.version_option() -def main(): - pass - - -@main.command() -@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): - """Show the click-completion-command completion code""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) - - -@main.command() -@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), -) -@click.argument("path", required=False) -def install(append, case_insensitive, shell, path): - """Install the click-completion-command completion""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - shell, path = click_completion.core.install( - shell=shell, - path=path, - append=append, - extra_env=extra_env, - ) - click.echo(f"{shell} completion installed in {path}") - - -def signal_handler(sig, frame): - global flask_enabled - - threads.APRSDThreadList().stop_all() - if "subprocess" not in str(frame): - LOG.info( - "Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format( - datetime.datetime.now(), - ), - ) - time.sleep(5) - tracker = messaging.MsgTrack() - tracker.save() - LOG.info(stats.APRSDStats()) - # signal.signal(signal.SIGTERM, sys.exit(0)) - # sys.exit(0) - if flask_enabled: - 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["aprsd"]["email"].get("enabled", False) and config["aprsd"]["email"][ - "imap" - ].get("debug", False): - - imap_logger = logging.getLogger("imapclient.imaplib") - imap_logger.setLevel(log_level) - imap_logger.addHandler(fh) - - if not quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - if imap_logger: - imap_logger.addHandler(sh) - - -@main.command() -@click.option( - "--loglevel", - default="DEBUG", - 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("--quiet", is_flag=True, default=False, help="Don't log to stdout") -@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( - "--aprs-login", - envvar="APRS_LOGIN", - show_envvar=True, - help="What callsign to send the message from.", -) -@click.option( - "--aprs-password", - envvar="APRS_PASSWORD", - show_envvar=True, - help="the APRS-IS password for APRS_LOGIN", -) -@click.option( - "--no-ack", - "-n", - is_flag=True, - show_default=True, - default=False, - help="Don't wait for an ack, just sent it to APRS-IS and bail.", -) -@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack") -@click.argument("tocallsign", required=False) -@click.argument("command", nargs=-1, required=False) -def listen( - loglevel, - quiet, - config_file, - aprs_login, - aprs_password, - no_ack, - raw, - tocallsign, - command, -): - """Send a message to a callsign via APRS_IS.""" - global got_ack, got_response - - config = aprsd_config.parse_config(config_file) - if not aprs_login: - click.echo("Must set --aprs_login or APRS_LOGIN") - return - - if not aprs_password: - click.echo("Must set --aprs-password or APRS_PASSWORD") - return - - config["aprs"]["login"] = aprs_login - config["aprs"]["password"] = aprs_password - messaging.CONFIG = config - - setup_logging(config, loglevel, quiet) - LOG.info(f"APRSD TEST Started version: {aprsd.__version__}") - if type(command) is tuple: - command = " ".join(command) - if not quiet: - if raw: - LOG.info(f"L'{aprs_login}' R'{raw}'") - else: - LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") - - flat_config = utils.flatten_dict(config) - LOG.info("Using CONFIG values:") - for x in flat_config: - if "password" in x or "aprsd.web.users.admin" in x: - LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") - else: - LOG.info(f"{x} = {flat_config[x]}") - - got_ack = False - got_response = False - - # TODO(walt) - manually edit this list - # prior to running aprsd-listen listen - watch_list = ["k*"] - - # build last seen list - last_seen = {} - for callsign in watch_list: - call = callsign.replace("*", "") - last_seen[call] = datetime.datetime.now() - - LOG.debug("Last seen list") - LOG.debug(last_seen) - - @trace.trace - def rx_packet(packet): - global got_ack, got_response - LOG.debug("Got packet back {}".format(packet["raw"])) - - if packet["from"] in last_seen: - now = datetime.datetime.now() - age = str(now - last_seen[packet["from"]]) - - delta = utils.parse_delta_str(age) - d = datetime.timedelta(**delta) - - max_timeout = { - "seconds": config["aprsd"]["watch_list"]["alert_time_seconds"], - } - max_delta = datetime.timedelta(**max_timeout) - if d > max_delta: - LOG.debug( - "NOTIFY!!! {} last seen {} max age={}".format( - packet["from"], - age, - max_delta, - ), - ) - else: - LOG.debug(f"Not old enough to notify {d} < {max_delta}") - LOG.debug("Update last seen from {}".format(packet["from"])) - last_seen[packet["from"]] = now - else: - LOG.debug( - "ignoring packet because {} not in watch list".format(packet["from"]), - ) - - resp = packet.get("response", None) - if resp == "ack": - ack_num = packet.get("msgNo") - LOG.info(f"We saw an ACK {ack_num} Ignoring") - # messaging.log_packet(packet) - got_ack = True - else: - message = packet.get("message_text", None) - fromcall = packet["from"] - msg_number = packet.get("msgNo", "0") - messaging.log_message( - "Received Message", - packet["raw"], - message, - fromcall=fromcall, - ack=msg_number, - ) - - try: - cl = client.Client(config) - cl.setup_connection() - except LoginError: - sys.exit(-1) - - aprs_client = client.get_client() - - # filter_str = 'b/{}'.format('/'.join(watch_list)) - # LOG.debug("Filter by '{}'".format(filter_str)) - # aprs_client.set_filter(filter_str) - filter_str = "p/{}".format("/".join(watch_list)) - LOG.debug(f"Filter by '{filter_str}'") - aprs_client.set_filter(filter_str) - - while True: - try: - # This will register a packet consumer with aprslib - # When new packets come in the consumer will process - # the packet - aprs_client.consumer(rx_packet, raw=False) - except aprslib.exceptions.ConnectionDrop: - LOG.error("Connection dropped, reconnecting") - time.sleep(5) - # Force the deletion of the client object connected to aprs - # This will cause a reconnect, next time client.get_client() - # is called - cl.reset() - except aprslib.exceptions.UnknownFormat: - LOG.error("Got a shitty packet") - - -if __name__ == "__main__": - main() diff --git a/aprsd/main.py b/aprsd/main.py deleted file mode 100644 index 78ff12c..0000000 --- a/aprsd/main.py +++ /dev/null @@ -1,536 +0,0 @@ -# -# Listen on amateur radio aprs-is network for messages and respond to them. -# You must have an amateur radio callsign to use this software. You must -# create an ~/.aprsd/config.yml file with all of the required settings. To -# generate an example config.yml, just run aprsd, then copy the sample config -# to ~/.aprsd/config.yml and edit the settings. -# -# APRS messages: -# l(ocation) = descriptive location of calling station -# w(eather) = temp, (hi/low) forecast, later forecast -# t(ime) = respond with the current time -# f(ortune) = respond with a short fortune -# -email_addr email text = send an email -# -2 = display the last 2 emails received -# p(ing) = respond with Pong!/time -# anything else = respond with usage -# -# (C)2018 Craig Lamparter -# License GPLv2 -# - -# python included libs -import datetime -import logging -from logging import NullHandler -from logging.handlers import RotatingFileHandler -import os -import signal -import sys -import time - -import aprslib -from aprslib.exceptions import LoginError -import click -import click_completion - -# local imports here -import aprsd -from aprsd import ( - flask, messaging, packets, plugin, stats, threads, trace, utils, -) -from aprsd import client -from aprsd import config as aprsd_config - - -# setup the global logger -# logging.basicConfig(level=logging.DEBUG) # level=10 -LOG = logging.getLogger("APRSD") - - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) -flask_enabled = False - - -def custom_startswith(string, incomplete): - """A custom completion match that supports case insensitive matching.""" - if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): - string = string.lower() - incomplete = incomplete.lower() - return string.startswith(incomplete) - - -click_completion.core.startswith = custom_startswith -click_completion.init() - - -cmd_help = """Shell completion for click-completion-command -Available shell types: -\b - %s -Default type: auto -""" % "\n ".join( - f"{k:<12} {click_completion.core.shells[k]}" - for k in sorted(click_completion.core.shells.keys()) -) - - -@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS) -@click.version_option() -def main(): - pass - - -@main.command() -@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): - """Show the click-completion-command completion code""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) - - -@main.command() -@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), -) -@click.argument("path", required=False) -def install(append, case_insensitive, shell, path): - """Install the click-completion-command completion""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - shell, path = click_completion.core.install( - shell=shell, - path=path, - append=append, - extra_env=extra_env, - ) - click.echo(f"{shell} completion installed in {path}") - - -def signal_handler(sig, frame): - global flask_enabled - - threads.APRSDThreadList().stop_all() - if "subprocess" not in str(frame): - LOG.info( - "Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format( - datetime.datetime.now(), - ), - ) - time.sleep(1.5) - messaging.MsgTrack().save() - packets.WatchList().save() - packets.SeenList().save() - LOG.info(stats.APRSDStats()) - # signal.signal(signal.SIGTERM, sys.exit(0)) - # sys.exit(0) - if flask_enabled: - 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) - - -@main.command() -@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.", -) -def check_version(loglevel, config_file): - config = aprsd_config.parse_config(config_file) - - setup_logging(config, loglevel, False) - level, msg = utils._check_version() - if level: - LOG.warning(msg) - else: - LOG.info(msg) - - -@main.command() -def sample_config(): - """This dumps the config to stdout.""" - click.echo(aprsd_config.dump_default_cfg()) - - -@main.command() -@click.option( - "--loglevel", - default="DEBUG", - 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("--quiet", is_flag=True, default=False, help="Don't log to stdout") -@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( - "--aprs-login", - envvar="APRS_LOGIN", - show_envvar=True, - help="What callsign to send the message from.", -) -@click.option( - "--aprs-password", - envvar="APRS_PASSWORD", - show_envvar=True, - help="the APRS-IS password for APRS_LOGIN", -) -@click.option( - "--no-ack", - "-n", - is_flag=True, - show_default=True, - default=False, - help="Don't wait for an ack, just sent it to APRS-IS and bail.", -) -@click.option( - "--wait-response", - "-w", - is_flag=True, - show_default=True, - default=False, - help="Wait for a response to the message?", -) -@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack") -@click.argument("tocallsign", required=False) -@click.argument("command", nargs=-1, required=False) -def send_message( - loglevel, - quiet, - config_file, - aprs_login, - aprs_password, - no_ack, - wait_response, - raw, - tocallsign, - command, -): - """Send a message to a callsign via APRS_IS.""" - global got_ack, got_response - - config = aprsd_config.parse_config(config_file) - if not aprs_login: - click.echo("Must set --aprs_login or APRS_LOGIN") - return - - if not aprs_password: - click.echo("Must set --aprs-password or APRS_PASSWORD") - return - - config["aprs"]["login"] = aprs_login - config["aprs"]["password"] = aprs_password - messaging.CONFIG = config - - setup_logging(config, loglevel, quiet) - LOG.info(f"APRSD Started version: {aprsd.__version__}") - if type(command) is tuple: - command = " ".join(command) - if not quiet: - if raw: - LOG.info(f"L'{aprs_login}' R'{raw}'") - else: - LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") - - packets.WatchList(config=config) - packets.SeenList(config=config) - - got_ack = False - got_response = False - - def rx_packet(packet): - global got_ack, got_response - # LOG.debug("Got packet back {}".format(packet)) - resp = packet.get("response", None) - if resp == "ack": - ack_num = packet.get("msgNo") - LOG.info(f"We got ack for our sent message {ack_num}") - messaging.log_packet(packet) - got_ack = True - else: - message = packet.get("message_text", None) - fromcall = packet["from"] - msg_number = packet.get("msgNo", "0") - messaging.log_message( - "Received Message", - packet["raw"], - message, - fromcall=fromcall, - ack=msg_number, - ) - got_response = True - # Send the ack back? - ack = messaging.AckMessage( - config["aprs"]["login"], - fromcall, - msg_id=msg_number, - ) - ack.send_direct() - - if got_ack: - if wait_response: - if got_response: - sys.exit(0) - else: - sys.exit(0) - - try: - client.ClientFactory.setup(config) - client.factory.create().client - except LoginError: - sys.exit(-1) - - packets.PacketList(config=config) - packets.WatchList(config=config) - - # Send a message - # then we setup a consumer to rx messages - # We should get an ack back as well as a new message - # we should bail after we get the ack and send an ack back for the - # message - if raw: - msg = messaging.RawMessage(raw) - msg.send_direct() - sys.exit(0) - else: - msg = messaging.TextMessage(aprs_login, tocallsign, command) - msg.send_direct() - - if no_ack: - sys.exit(0) - - try: - # This will register a packet consumer with aprslib - # When new packets come in the consumer will process - # the packet - aprs_client = client.factory.create().client - aprs_client.consumer(rx_packet, raw=False) - except aprslib.exceptions.ConnectionDrop: - LOG.error("Connection dropped, reconnecting") - time.sleep(5) - # Force the deletion of the client object connected to aprs - # This will cause a reconnect, next time client.get_client() - # is called - aprs_client.reset() - - -# main() ### -@main.command() -@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("--quiet", is_flag=True, default=False, help="Don't log to stdout") -@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( - "-f", - "--flush", - "flush", - is_flag=True, - show_default=True, - default=False, - help="Flush out all old aged messages on disk.", -) -def server( - loglevel, - quiet, - config_file, - flush, -): - """Start the aprsd server process.""" - global flask_enabled - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - - if not quiet: - click.echo("Load config") - - config = aprsd_config.parse_config(config_file) - - setup_logging(config, loglevel, quiet) - level, msg = utils._check_version() - if level: - LOG.warning(msg) - else: - LOG.info(msg) - LOG.info(f"APRSD Started version: {aprsd.__version__}") - - flat_config = utils.flatten_dict(config) - LOG.info("Using CONFIG values:") - for x in flat_config: - if "password" in x or "aprsd.web.users.admin" in x: - LOG.info(f"{x} = XXXXXXXXXXXXXXXXXXX") - else: - LOG.info(f"{x} = {flat_config[x]}") - - if config["aprsd"].get("trace", False): - trace.setup_tracing(["method", "api"]) - stats.APRSDStats(config) - - # Initialize the client factory and create - # The correct client object ready for use - client.ClientFactory.setup(config) - # Make sure we have 1 client transport enabled - if not client.factory.is_client_enabled(): - LOG.error("No Clients are enabled in config.") - sys.exit(-1) - - # Creates the client object - LOG.info("Creating client connection") - client.factory.create().client - - # Now load the msgTrack from disk if any - packets.PacketList(config=config) - if flush: - LOG.debug("Deleting saved MsgTrack.") - messaging.MsgTrack(config=config).flush() - packets.WatchList(config=config) - packets.SeenList(config=config) - else: - # Try and load saved MsgTrack list - LOG.debug("Loading saved MsgTrack object.") - messaging.MsgTrack(config=config).load() - packets.WatchList(config=config).load() - packets.SeenList(config=config).load() - - # Create the initial PM singleton and Register plugins - LOG.info("Loading Plugin Manager and registering plugins") - plugin_manager = plugin.PluginManager(config) - plugin_manager.setup_plugins() - - rx_thread = threads.APRSDRXThread( - msg_queues=threads.msg_queues, - config=config, - ) - rx_thread.start() - - messaging.MsgTrack().restart() - - keepalive = threads.KeepAliveThread(config=config) - keepalive.start() - - web_enabled = config.get("aprsd.web.enabled", default=False) - - if web_enabled: - flask_enabled = True - (socketio, app) = flask.init_flask(config, loglevel, quiet) - socketio.run( - app, - host=config["aprsd"]["web"]["host"], - port=config["aprsd"]["web"]["port"], - ) - - # If there are items in the msgTracker, then save them - LOG.info("APRSD Exiting.") - return 0 - - -if __name__ == "__main__": - main() diff --git a/setup.cfg b/setup.cfg index bad2b0c..89e33bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,9 +33,7 @@ packages = [entry_points] console_scripts = - aprsd = aprsd.main:main - aprsd-listen = aprsd.listen:main - aprsd-dev = aprsd.dev:main + aprsd = aprsd.aprsd:main aprsd-healthcheck = aprsd.healthcheck:main fake_aprs = aprsd.fake_aprs:main From 2ead6a97da74cd9030f4a730ca2a40ba1d51ef05 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 13:42:27 -0400 Subject: [PATCH 05/12] Pep8 failures --- aprsd/aprsd.py | 1 + aprsd/cmds/completion.py | 1 + aprsd/cmds/server.py | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py index 2a33154..7f1aefa 100644 --- a/aprsd/aprsd.py +++ b/aprsd/aprsd.py @@ -94,6 +94,7 @@ def cli(ctx, loglevel, config_file, quiet): ctx.obj["config"] = aprsd_config.parse_config(config_file) setup_logging(ctx.obj["config"], loglevel, quiet) + def main(): from .cmds import completion, dev, listen, send_message, server # noqa cli() diff --git a/aprsd/cmds/completion.py b/aprsd/cmds/completion.py index 55621a3..6b2adf7 100644 --- a/aprsd/cmds/completion.py +++ b/aprsd/cmds/completion.py @@ -6,6 +6,7 @@ from ..aprsd import cli CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + @cli.group(help="Click Completion subcommands", context_settings=CONTEXT_SETTINGS) @click.pass_context def completion(ctx): diff --git a/aprsd/cmds/server.py b/aprsd/cmds/server.py index bb08f7d..4b4d692 100644 --- a/aprsd/cmds/server.py +++ b/aprsd/cmds/server.py @@ -116,4 +116,3 @@ def server(ctx, flush): # If there are items in the msgTracker, then save them LOG.info("APRSD Exiting.") return 0 -import logging From 3463c6eb96e44b7424c392c983c2942dd23d1f41 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 14:05:24 -0400 Subject: [PATCH 06/12] Removed the need for FROMCALL in dev test-plugin We already use the env var for APRS_LOGIN, so that is now used for the test-plugin command. Also cleaned up some help text --- aprsd/cmds/dev.py | 16 ++++++++++++++-- aprsd/cmds/listen.py | 7 ++++++- aprsd/cmds/send_message.py | 6 ++++-- aprsd/cmds/server.py | 2 +- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/aprsd/cmds/dev.py b/aprsd/cmds/dev.py index 5a910e5..a1d4e77 100644 --- a/aprsd/cmds/dev.py +++ b/aprsd/cmds/dev.py @@ -24,6 +24,12 @@ def dev(ctx): @dev.command() +@click.option( + "--aprs-login", + envvar="APRS_LOGIN", + show_envvar=True, + help="What callsign to send the message from.", +) @click.option( "-p", "--plugin", @@ -49,19 +55,19 @@ def dev(ctx): default=1, help="Number of times to call the plugin", ) -@click.argument("fromcall") @click.argument("message", nargs=-1, required=True) @click.pass_context def test_plugin( ctx, + aprs_login, plugin_path, load_all, number, - fromcall, message, ): """APRSD Plugin test app.""" config = ctx.obj["config"] + fromcall = aprs_login if not plugin_path: click.echo(ctx.get_help()) @@ -79,6 +85,12 @@ def test_plugin( else: pm._init() obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) + if not obj: + click.echo(ctx.get_help()) + click.echo("") + ctx.fail(f"Failed to create object from plugin path '{plugin_path}'") + ctx.exit() + # Register the plugin they wanted tested. LOG.info( "Testing plugin {} Version {}".format( diff --git a/aprsd/cmds/listen.py b/aprsd/cmds/listen.py index 69f258d..f44e710 100644 --- a/aprsd/cmds/listen.py +++ b/aprsd/cmds/listen.py @@ -62,7 +62,12 @@ def listen( ): """Listen to packets on the APRS-IS Network based on FILTER. - FILTER is the APRS Filter to use. see http://www.aprs-is.net/javAPRSFilter.aspx + FILTER is the APRS Filter to use.\n + see http://www.aprs-is.net/javAPRSFilter.aspx\n + r/lat/lon/dist - Range Filter Pass posits and objects within dist km from lat/lon.\n + p/aa/bb/cc... - Prefix Filter Pass traffic with fromCall that start with aa or bb or cc.\n + b/call1/call2... - Budlist Filter Pass all traffic from exact call: call1, call2, ... (* wild card allowed) \n + o/obj1/obj2... - Object Filter Pass all objects with the exact name of obj1, obj2, ... (* wild card allowed)\n """ config = ctx.obj["config"] diff --git a/aprsd/cmds/send_message.py b/aprsd/cmds/send_message.py index 89f770c..d09869f 100644 --- a/aprsd/cmds/send_message.py +++ b/aprsd/cmds/send_message.py @@ -83,6 +83,10 @@ def send_message( else: LOG.info(f"L'{aprs_login}' To'{tocallsign}' C'{command}'") + packets.PacketList(config=config) + packets.WatchList(config=config) + packets.SeenList(config=config) + got_ack = False got_response = False @@ -128,8 +132,6 @@ def send_message( except LoginError: sys.exit(-1) - packets.PacketList(config=config) - packets.WatchList(config=config) # Send a message # then we setup a consumer to rx messages diff --git a/aprsd/cmds/server.py b/aprsd/cmds/server.py index 4b4d692..e818c6b 100644 --- a/aprsd/cmds/server.py +++ b/aprsd/cmds/server.py @@ -29,7 +29,7 @@ LOG = logging.getLogger("APRSD") ) @click.pass_context def server(ctx, flush): - """Start the aprsd server process.""" + """Start the aprsd server gateway process.""" ctx.obj["config_file"] loglevel = ctx.obj["loglevel"] quiet = ctx.obj["quiet"] From 82def598f09abe02ad1fe63738040d886e568bf3 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 14:20:47 -0400 Subject: [PATCH 07/12] Added healthcheck to the cmds this patch moves the healthcheck to it's own command. aprsd healthcheck --- aprsd/aprsd.py | 4 +- aprsd/cmds/healthcheck.py | 76 +++++++++++++ aprsd/fake_aprs.py | 84 -------------- aprsd/healthcheck.py | 233 -------------------------------------- setup.cfg | 2 - 5 files changed, 79 insertions(+), 320 deletions(-) create mode 100644 aprsd/cmds/healthcheck.py delete mode 100644 aprsd/fake_aprs.py delete mode 100644 aprsd/healthcheck.py diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py index 7f1aefa..e1ef9c3 100644 --- a/aprsd/aprsd.py +++ b/aprsd/aprsd.py @@ -96,7 +96,9 @@ def cli(ctx, loglevel, config_file, quiet): def main(): - from .cmds import completion, dev, listen, send_message, server # noqa + from .cmds import ( # noqa + completion, dev, healthcheck, listen, send_message, server, + ) cli() diff --git a/aprsd/cmds/healthcheck.py b/aprsd/cmds/healthcheck.py new file mode 100644 index 0000000..7fd7576 --- /dev/null +++ b/aprsd/cmds/healthcheck.py @@ -0,0 +1,76 @@ +# +# Used to fetch the stats url and determine if +# aprsd server is 'healthy' +# +# +# python included libs +import datetime +import json +import logging +import sys + +import click +import requests + +import aprsd +from aprsd import utils + +# local imports here +from ..aprsd import cli + + +# setup the global logger +# logging.basicConfig(level=logging.DEBUG) # level=10 +LOG = logging.getLogger("APRSD") + +@cli.command() +@click.option( + "--url", + "health_url", + show_default=True, + default="http://localhost:8001/stats", + help="The aprsd url to call for checking health/stats", +) +@click.option( + "--timeout", + show_default=True, + default=3, + help="How long to wait for healtcheck url to come back", +) +@click.pass_context +def healthcheck(ctx, health_url, timeout): + """Check the health of the running aprsd server.""" + ctx.obj["config"] + LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}") + + try: + url = health_url + response = requests.get(url, timeout=timeout) + response.raise_for_status() + except Exception as ex: + LOG.error(f"Failed to fetch healthcheck url '{url}' : '{ex}'") + sys.exit(-1) + else: + stats = json.loads(response.text) + LOG.debug(stats) + + email_thread_last_update = stats["stats"]["email"]["thread_last_update"] + + delta = utils.parse_delta_str(email_thread_last_update) + d = datetime.timedelta(**delta) + max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} + max_delta = datetime.timedelta(**max_timeout) + if d > max_delta: + LOG.error(f"Email thread is very old! {d}") + sys.exit(-1) + + aprsis_last_update = stats["stats"]["aprs-is"]["last_update"] + delta = utils.parse_delta_str(aprsis_last_update) + d = datetime.timedelta(**delta) + max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} + max_delta = datetime.timedelta(**max_timeout) + if d > max_delta: + LOG.error(f"APRS-IS last update is very old! {d}") + sys.exit(-1) + + sys.exit(0) diff --git a/aprsd/fake_aprs.py b/aprsd/fake_aprs.py deleted file mode 100644 index 8eafe6b..0000000 --- a/aprsd/fake_aprs.py +++ /dev/null @@ -1,84 +0,0 @@ -import argparse -import logging -from logging.handlers import RotatingFileHandler -import socketserver -import sys -import time - -from aprsd import utils - - -# command line args -parser = argparse.ArgumentParser() -parser.add_argument( - "--loglevel", - default="DEBUG", - choices=["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"], - help="The log level to use for aprsd.log", -) -parser.add_argument("--quiet", action="store_true", help="Don't log to stdout") - -parser.add_argument("--port", default=9099, type=int, help="The port to listen on .") -parser.add_argument("--ip", default="127.0.0.1", help="The IP to listen on ") - -CONFIG = None -LOG = logging.getLogger("ARPSSERVER") - - -# Setup the logging faciility -# to disable logging to stdout, but still log to file -# use the --quiet option on the cmdln -def setup_logging(args): - global LOG - levels = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, - } - log_level = levels[args.loglevel] - - LOG.setLevel(log_level) - log_format = "%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s]" " %(message)s" - date_format = "%m/%d/%Y %I:%M:%S %p" - log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) - fh = RotatingFileHandler("aprs-server.log", maxBytes=(10248576 * 5), backupCount=4) - fh.setFormatter(log_formatter) - LOG.addHandler(fh) - - if not args.quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - - -class MyAPRSTCPHandler(socketserver.BaseRequestHandler): - def handle(self): - # self.request is the TCP socket connected to the client - self.data = self.request.recv(1024).strip() - LOG.debug(f"{self.client_address[0]} wrote:") - LOG.debug(self.data) - # just send back the same data, but upper-cased - self.request.sendall(self.data.upper()) - - -def main(): - global CONFIG - args = parser.parse_args() - setup_logging(args) - LOG.info("Test APRS server starting.") - time.sleep(1) - - CONFIG = utils.parse_config(args) - - ip = CONFIG["aprs"]["host"] - port = CONFIG["aprs"]["port"] - LOG.info(f"Start server listening on {args.ip}:{args.port}") - - with socketserver.TCPServer((ip, port), MyAPRSTCPHandler) as server: - server.serve_forever() - - -if __name__ == "__main__": - main() diff --git a/aprsd/healthcheck.py b/aprsd/healthcheck.py deleted file mode 100644 index 462661c..0000000 --- a/aprsd/healthcheck.py +++ /dev/null @@ -1,233 +0,0 @@ -# -# Used to fetch the stats url and determine if -# aprsd server is 'healthy' -# -# -# python included libs -import datetime -import json -import logging -from logging import NullHandler -from logging.handlers import RotatingFileHandler -import os -import re -import sys - -import click -import click_completion -import requests - -# local imports here -import aprsd -from aprsd import config as aprsd_config - - -# setup the global logger -# logging.basicConfig(level=logging.DEBUG) # level=10 -LOG = logging.getLogger("APRSD") - -LOG_LEVELS = { - "CRITICAL": logging.CRITICAL, - "ERROR": logging.ERROR, - "WARNING": logging.WARNING, - "INFO": logging.INFO, - "DEBUG": logging.DEBUG, -} - -CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -def custom_startswith(string, incomplete): - """A custom completion match that supports case insensitive matching.""" - if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"): - string = string.lower() - incomplete = incomplete.lower() - return string.startswith(incomplete) - - -click_completion.core.startswith = custom_startswith -click_completion.init() - - -cmd_help = """Shell completion for click-completion-command -Available shell types: -\b - %s -Default type: auto -""" % "\n ".join( - f"{k:<12} {click_completion.core.shells[k]}" - for k in sorted(click_completion.core.shells.keys()) -) - - -@click.group(help=cmd_help, context_settings=CONTEXT_SETTINGS) -@click.version_option() -def main(): - pass - - -@main.command() -@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): - """Show the click-completion-command completion code""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - click.echo(click_completion.core.get_code(shell, extra_env=extra_env)) - - -@main.command() -@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), -) -@click.argument("path", required=False) -def install(append, case_insensitive, shell, path): - """Install the click-completion-command completion""" - extra_env = ( - {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} - if case_insensitive - else {} - ) - shell, path = click_completion.core.install( - shell=shell, - path=path, - append=append, - extra_env=extra_env, - ) - click.echo(f"{shell} completion installed in {path}") - - -# 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 = LOG_LEVELS[loglevel] - LOG.setLevel(log_level) - log_format = "[%(asctime)s] [%(threadName)-12s] [%(levelname)-5.5s]" " %(message)s" - date_format = "%m/%d/%Y %I:%M:%S %p" - log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format) - log_file = config["aprs"].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) - - if not quiet: - sh = logging.StreamHandler(sys.stdout) - sh.setFormatter(log_formatter) - LOG.addHandler(sh) - - -def parse_delta_str(s): - if "day" in s: - m = re.match( - r"(?P[-\d]+) day[s]*, (?P\d+):(?P\d+):(?P\d[\.\d+]*)", - s, - ) - else: - m = re.match(r"(?P\d+):(?P\d+):(?P\d[\.\d+]*)", s) - return {key: float(val) for key, val in m.groupdict().items()} - - -@main.command() -@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( - "--url", - "health_url", - show_default=True, - default="http://localhost:8001/stats", - help="The aprsd url to call for checking health/stats", -) -@click.option( - "--timeout", - show_default=True, - default=3, - help="How long to wait for healtcheck url to come back", -) -def check(loglevel, config_file, health_url, timeout): - """APRSD Plugin test app.""" - - config = aprsd_config.parse_config(config_file) - - setup_logging(config, loglevel, False) - LOG.debug(f"APRSD HealthCheck version: {aprsd.__version__}") - - try: - url = health_url - response = requests.get(url, timeout=timeout) - response.raise_for_status() - except Exception as ex: - LOG.error(f"Failed to fetch healthcheck url '{url}' : '{ex}'") - sys.exit(-1) - else: - stats = json.loads(response.text) - LOG.debug(stats) - - email_thread_last_update = stats["stats"]["email"]["thread_last_update"] - - delta = parse_delta_str(email_thread_last_update) - d = datetime.timedelta(**delta) - max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} - max_delta = datetime.timedelta(**max_timeout) - if d > max_delta: - LOG.error(f"Email thread is very old! {d}") - sys.exit(-1) - - aprsis_last_update = stats["stats"]["aprs-is"]["last_update"] - delta = parse_delta_str(aprsis_last_update) - d = datetime.timedelta(**delta) - max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0} - max_delta = datetime.timedelta(**max_timeout) - if d > max_delta: - LOG.error(f"APRS-IS last update is very old! {d}") - sys.exit(-1) - - sys.exit(0) - - -if __name__ == "__main__": - main() diff --git a/setup.cfg b/setup.cfg index 89e33bd..a254fed 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,6 @@ packages = [entry_points] console_scripts = aprsd = aprsd.aprsd:main - aprsd-healthcheck = aprsd.healthcheck:main - fake_aprs = aprsd.fake_aprs:main [build_sphinx] source-dir = docs From 8287c09ce5ce871db09d0e70b6b6f5d0e186fd00 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 14:38:23 -0400 Subject: [PATCH 08/12] pep8 --- aprsd/cmds/healthcheck.py | 1 + aprsd/cmds/send_message.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/aprsd/cmds/healthcheck.py b/aprsd/cmds/healthcheck.py index 7fd7576..e18728c 100644 --- a/aprsd/cmds/healthcheck.py +++ b/aprsd/cmds/healthcheck.py @@ -23,6 +23,7 @@ from ..aprsd import cli # logging.basicConfig(level=logging.DEBUG) # level=10 LOG = logging.getLogger("APRSD") + @cli.command() @click.option( "--url", diff --git a/aprsd/cmds/send_message.py b/aprsd/cmds/send_message.py index d09869f..569b4bb 100644 --- a/aprsd/cmds/send_message.py +++ b/aprsd/cmds/send_message.py @@ -132,7 +132,6 @@ def send_message( except LoginError: sys.exit(-1) - # Send a message # then we setup a consumer to rx messages # We should get an ack back as well as a new message From 9187b9781a537ccdc9a3e200a050693c5e6b9587 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 16:26:24 -0400 Subject: [PATCH 09/12] Ensure common params are honored --- aprsd/aprsd.py | 12 +++++++++--- aprsd/cli_helper.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py index e1ef9c3..67f00a1 100644 --- a/aprsd/aprsd.py +++ b/aprsd/aprsd.py @@ -34,9 +34,9 @@ import click_completion # local imports here import aprsd +from aprsd import cli_helper from aprsd import config as aprsd_config from aprsd import messaging, packets, stats, threads, utils -from aprsd.cli_helper import AliasedGroup # setup the global logger @@ -56,9 +56,10 @@ def custom_startswith(string, incomplete): click_completion.core.startswith = custom_startswith click_completion.init() +cli_initialized = 0 -@click.group(cls=AliasedGroup, context_settings=CONTEXT_SETTINGS) +@click.group(cls=cli_helper.GroupWithCommandOptions, context_settings=CONTEXT_SETTINGS) @click.option( "--loglevel", default="INFO", @@ -87,12 +88,17 @@ click_completion.init() @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) - setup_logging(ctx.obj["config"], loglevel, quiet) + if not cli_initialized: + setup_logging(ctx.obj["config"], loglevel, quiet) + cli_initialized = 1 def main(): diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py index ef83aae..3a911c6 100644 --- a/aprsd/cli_helper.py +++ b/aprsd/cli_helper.py @@ -33,3 +33,42 @@ 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 From 617973f56145a96eadce808f2002088106a013d4 Mon Sep 17 00:00:00 2001 From: Hemna Date: Fri, 5 Nov 2021 16:40:07 -0400 Subject: [PATCH 10/12] Fixed test-plugin --- aprsd/cmds/dev.py | 15 +++++++-------- aprsd/cmds/send_message.py | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/aprsd/cmds/dev.py b/aprsd/cmds/dev.py index a1d4e77..160aed3 100644 --- a/aprsd/cmds/dev.py +++ b/aprsd/cmds/dev.py @@ -17,13 +17,7 @@ LOG = logging.getLogger("APRSD") CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) -@cli.group(help="Development tools", context_settings=CONTEXT_SETTINGS) -@click.pass_context -def dev(ctx): - pass - - -@dev.command() +@cli.command() @click.option( "--aprs-login", envvar="APRS_LOGIN", @@ -65,7 +59,12 @@ def test_plugin( number, message, ): - """APRSD Plugin test app.""" + """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. + """ config = ctx.obj["config"] fromcall = aprs_login diff --git a/aprsd/cmds/send_message.py b/aprsd/cmds/send_message.py index 569b4bb..9de82c2 100644 --- a/aprsd/cmds/send_message.py +++ b/aprsd/cmds/send_message.py @@ -45,8 +45,8 @@ LOG = logging.getLogger("APRSD") help="Wait for a response to the message?", ) @click.option("--raw", default=None, help="Send a raw message. Implies --no-ack") -@click.argument("tocallsign", required=False) -@click.argument("command", nargs=-1, required=False) +@click.argument("tocallsign", required=True) +@click.argument("command", nargs=-1, required=True) @click.pass_context def send_message( ctx, From 89727e2b8e602662f22b6581ac06b6220e4e4631 Mon Sep 17 00:00:00 2001 From: Hemna Date: Mon, 8 Nov 2021 11:52:41 -0500 Subject: [PATCH 11/12] Reworked all the common arguments This patch reworks all the common arguments for the commands and subcommands --loglevel --config_file --quiet These are all now processed in 1 place. --- aprsd/aprsd.py | 93 +++----------------------------- aprsd/cli_helper.py | 107 +++++++++++++++++++++++-------------- aprsd/cmds/completion.py | 4 +- aprsd/cmds/dev.py | 19 ++++--- aprsd/cmds/healthcheck.py | 4 +- aprsd/cmds/listen.py | 6 ++- aprsd/cmds/send_message.py | 4 +- aprsd/cmds/server.py | 5 +- aprsd/log.py | 53 ++++++++++++++++++ aprsd/threads.py | 1 - 10 files changed, 157 insertions(+), 139 deletions(-) create mode 100644 aprsd/log.py 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, } From cdcb98e438df407394f95eb11a849cc5a2b066b7 Mon Sep 17 00:00:00 2001 From: Hemna Date: Mon, 8 Nov 2021 12:18:23 -0500 Subject: [PATCH 12/12] Cleaned up some verbose output & colorized output Some commands now have some color if the shell detects it supports it. --- aprsd/aprsd.py | 15 ++++++--------- aprsd/cli_helper.py | 36 ------------------------------------ 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/aprsd/aprsd.py b/aprsd/aprsd.py index 6161941..1556a01 100644 --- a/aprsd/aprsd.py +++ b/aprsd/aprsd.py @@ -34,7 +34,7 @@ import click_completion import aprsd from aprsd import cli_helper from aprsd import config as aprsd_config -from aprsd import log, messaging, packets, stats, threads, utils +from aprsd import messaging, packets, stats, threads, utils # setup the global logger @@ -65,6 +65,7 @@ def cli(ctx): def main(): # First import all the possible commands for the CLI + # The commands themselves live in the cmds directory from .cmds import ( # noqa completion, dev, healthcheck, listen, send_message, server, ) @@ -99,16 +100,11 @@ def signal_handler(sig, frame): @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) - - log.setup_logging(config, loglevel, False) level, msg = utils._check_version() if level: - LOG.warning(msg) + click.secho(msg, fg="yellow") else: - LOG.info(msg) + click.secho(msg, fg="green") @cli.command() @@ -122,7 +118,8 @@ def sample_config(ctx): @click.pass_context def version(ctx): """Show the APRSD version.""" - click.echo(f"APRSD Version : {aprsd.__version__}") + click.echo(click.style("APRSD Version : ", fg="white"), nl=False) + click.secho(f"{aprsd.__version__}", fg="yellow", bold=True) if __name__ == "__main__": diff --git a/aprsd/cli_helper.py b/aprsd/cli_helper.py index c20e732..ab8bf7e 100644 --- a/aprsd/cli_helper.py +++ b/aprsd/cli_helper.py @@ -48,8 +48,6 @@ def add_options(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"] @@ -67,37 +65,3 @@ def process_standard_options(f: F) -> F: return f(*args, **kwargs) return update_wrapper(t.cast(F, new_func), f) - - -class AliasedGroup(click.Group): - def command(self, *args, **kwargs): - """A shortcut decorator for declaring and attaching a command to - the group. This takes the same arguments as :func:`command` but - immediately registers the created command with this instance by - calling into :meth:`add_command`. - Copied from `click` and extended for `aliases`. - """ - def decorator(f): - aliases = kwargs.pop("aliases", []) - cmd = click.decorators.command(*args, **kwargs)(f) - self.add_command(cmd) - for alias in aliases: - self.add_command(cmd, name=alias) - return cmd - return decorator - - def group(self, *args, **kwargs): - """A shortcut decorator for declaring and attaching a group to - the group. This takes the same arguments as :func:`group` but - immediately registers the created command with this instance by - calling into :meth:`add_command`. - Copied from `click` and extended for `aliases`. - """ - def decorator(f): - aliases = kwargs.pop("aliases", []) - cmd = click.decorators.group(*args, **kwargs)(f) - self.add_command(cmd) - for alias in aliases: - self.add_command(cmd, name=alias) - return cmd - return decorator