mirror of
https://github.com/craigerl/aprsd.git
synced 2025-08-10 01:02:27 -04:00
Merge pull request #47 from craigerl/stabilize_1_6_0
Branch to stabilize for the 1.6.0 release.
This commit is contained in:
commit
3a6316fa8a
42
Dockerfile
42
Dockerfile
@ -1,42 +0,0 @@
|
||||
FROM alpine:latest as aprsd
|
||||
|
||||
ENV VERSION=1.0.0
|
||||
ENV APRS_USER=aprs
|
||||
ENV HOME=/home/aprs
|
||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||
|
||||
ENV INSTALL=$HOME/install
|
||||
RUN apk add --update git wget py3-pip py3-virtualenv bash fortune
|
||||
|
||||
# Setup Timezone
|
||||
ENV TZ=US/Eastern
|
||||
#RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
#RUN apt-get install -y tzdata
|
||||
#RUN dpkg-reconfigure --frontend noninteractive tzdata
|
||||
|
||||
|
||||
RUN addgroup --gid 1000 $APRS_USER
|
||||
RUN adduser -h $HOME -D -u 1001 -G $APRS_USER $APRS_USER
|
||||
|
||||
ENV LC_ALL=C.UTF-8
|
||||
ENV LANG=C.UTF-8
|
||||
|
||||
USER $APRS_USER
|
||||
RUN pip3 install wheel
|
||||
RUN python3 -m venv $VIRTUAL_ENV
|
||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
RUN echo "export PATH=\$PATH:\$HOME/.local/bin" >> $HOME/.bashrc
|
||||
VOLUME ["/config", "/plugins"]
|
||||
|
||||
WORKDIR $HOME
|
||||
RUN pip install aprsd
|
||||
USER root
|
||||
RUN aprsd sample-config > /config/aprsd.yml
|
||||
RUN chown -R $APRS_USER:$APRS_USER /config
|
||||
|
||||
# override this to run another configuration
|
||||
ENV CONF default
|
||||
USER $APRS_USER
|
||||
|
||||
ADD build/bin/run.sh $HOME/
|
||||
ENTRYPOINT ["/home/aprs/run.sh"]
|
144
aprsd/email.py
144
aprsd/email.py
@ -7,7 +7,7 @@ import re
|
||||
import smtplib
|
||||
import time
|
||||
|
||||
from aprsd import messaging, stats, threads
|
||||
from aprsd import messaging, stats, threads, trace
|
||||
import imapclient
|
||||
from validate_email import validate_email
|
||||
|
||||
@ -17,6 +17,7 @@ LOG = logging.getLogger("APRSD")
|
||||
CONFIG = None
|
||||
|
||||
|
||||
@trace.trace
|
||||
def _imap_connect():
|
||||
imap_port = CONFIG["aprsd"]["email"]["imap"].get("port", 143)
|
||||
use_ssl = CONFIG["aprsd"]["email"]["imap"].get("use_ssl", False)
|
||||
@ -31,9 +32,10 @@ def _imap_connect():
|
||||
port=imap_port,
|
||||
use_uid=True,
|
||||
ssl=use_ssl,
|
||||
timeout=30,
|
||||
)
|
||||
except Exception:
|
||||
LOG.error("Failed to connect IMAP server")
|
||||
except Exception as e:
|
||||
LOG.error("Failed to connect IMAP server", e)
|
||||
return
|
||||
|
||||
try:
|
||||
@ -47,9 +49,15 @@ def _imap_connect():
|
||||
return
|
||||
|
||||
server.select_folder("INBOX")
|
||||
|
||||
server.fetch = trace.trace(server.fetch)
|
||||
server.search = trace.trace(server.search)
|
||||
server.remove_flags = trace.trace(server.remove_flags)
|
||||
server.add_flags = trace.trace(server.add_flags)
|
||||
return server
|
||||
|
||||
|
||||
@trace.trace
|
||||
def _smtp_connect():
|
||||
host = CONFIG["aprsd"]["email"]["smtp"]["host"]
|
||||
smtp_port = CONFIG["aprsd"]["email"]["smtp"]["port"]
|
||||
@ -64,15 +72,28 @@ def _smtp_connect():
|
||||
|
||||
try:
|
||||
if use_ssl:
|
||||
server = smtplib.SMTP_SSL(host=host, port=smtp_port)
|
||||
server = smtplib.SMTP_SSL(
|
||||
host=host,
|
||||
port=smtp_port,
|
||||
timeout=30,
|
||||
)
|
||||
else:
|
||||
server = smtplib.SMTP(host=host, port=smtp_port)
|
||||
server = smtplib.SMTP(
|
||||
host=host,
|
||||
port=smtp_port,
|
||||
timeout=30,
|
||||
)
|
||||
except Exception:
|
||||
LOG.error("Couldn't connect to SMTP Server")
|
||||
return
|
||||
|
||||
LOG.debug("Connected to smtp host {}".format(msg))
|
||||
|
||||
debug = CONFIG["aprsd"]["email"]["smtp"].get("debug", False)
|
||||
if debug:
|
||||
server.set_debuglevel(5)
|
||||
server.sendmail = trace.trace(server.sendmail)
|
||||
|
||||
try:
|
||||
server.login(
|
||||
CONFIG["aprsd"]["email"]["smtp"]["login"],
|
||||
@ -87,7 +108,7 @@ def _smtp_connect():
|
||||
|
||||
|
||||
def validate_shortcuts(config):
|
||||
shortcuts = config.get("shortcuts", None)
|
||||
shortcuts = config["aprsd"]["email"].get("shortcuts", None)
|
||||
if not shortcuts:
|
||||
return
|
||||
|
||||
@ -120,7 +141,7 @@ def validate_shortcuts(config):
|
||||
for key in delete_keys:
|
||||
del config["aprsd"]["email"]["shortcuts"][key]
|
||||
|
||||
LOG.info("Available shortcuts: {}".format(config["shortcuts"]))
|
||||
LOG.info("Available shortcuts: {}".format(config["aprsd"]["email"]["shortcuts"]))
|
||||
|
||||
|
||||
def get_email_from_shortcut(addr):
|
||||
@ -152,6 +173,7 @@ def validate_email_config(config, disable_validation=False):
|
||||
return False
|
||||
|
||||
|
||||
@trace.trace
|
||||
def parse_email(msgid, data, server):
|
||||
envelope = data[b"ENVELOPE"]
|
||||
# email address match
|
||||
@ -162,7 +184,12 @@ def parse_email(msgid, data, server):
|
||||
else:
|
||||
from_addr = "noaddr"
|
||||
LOG.debug("Got a message from '{}'".format(from_addr))
|
||||
m = server.fetch([msgid], ["RFC822"])
|
||||
try:
|
||||
m = server.fetch([msgid], ["RFC822"])
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't fetch email from server in parse_email", e)
|
||||
return
|
||||
|
||||
msg = email.message_from_string(m[msgid][b"RFC822"].decode(errors="ignore"))
|
||||
if msg.is_multipart():
|
||||
text = ""
|
||||
@ -238,6 +265,7 @@ def parse_email(msgid, data, server):
|
||||
# end parse_email
|
||||
|
||||
|
||||
@trace.trace
|
||||
def send_email(to_addr, content):
|
||||
global check_email_delay
|
||||
|
||||
@ -282,6 +310,7 @@ def send_email(to_addr, content):
|
||||
# end send_email
|
||||
|
||||
|
||||
@trace.trace
|
||||
def resend_email(count, fromcall):
|
||||
global check_email_delay
|
||||
date = datetime.datetime.now()
|
||||
@ -290,7 +319,7 @@ def resend_email(count, fromcall):
|
||||
year = date.year
|
||||
today = "{}-{}-{}".format(day, month, year)
|
||||
|
||||
shortcuts = CONFIG["shortcuts"]
|
||||
shortcuts = CONFIG["aprsd"]["email"]["shortcuts"]
|
||||
# swap key/value
|
||||
shortcuts_inverted = {v: k for k, v in shortcuts.items()}
|
||||
|
||||
@ -300,7 +329,12 @@ def resend_email(count, fromcall):
|
||||
LOG.exception("Failed to Connect to IMAP. Cannot resend email ", e)
|
||||
return
|
||||
|
||||
messages = server.search(["SINCE", today])
|
||||
try:
|
||||
messages = server.search(["SINCE", today])
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't search for emails in resend_email ", e)
|
||||
return
|
||||
|
||||
# LOG.debug("%d messages received today" % len(messages))
|
||||
|
||||
msgexists = False
|
||||
@ -308,11 +342,21 @@ def resend_email(count, fromcall):
|
||||
messages.sort(reverse=True)
|
||||
del messages[int(count) :] # only the latest "count" messages
|
||||
for message in messages:
|
||||
for msgid, data in list(server.fetch(message, ["ENVELOPE"]).items()):
|
||||
try:
|
||||
parts = server.fetch(message, ["ENVELOPE"]).items()
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't fetch email parts in resend_email", e)
|
||||
continue
|
||||
|
||||
for msgid, data in list(parts):
|
||||
# one at a time, otherwise order is random
|
||||
(body, from_addr) = parse_email(msgid, data, server)
|
||||
# unset seen flag, will stay bold in email client
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
try:
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to remove SEEN flag in resend_email", e)
|
||||
|
||||
if from_addr in shortcuts_inverted:
|
||||
# reverse lookup of a shortcut
|
||||
from_addr = shortcuts_inverted[from_addr]
|
||||
@ -320,7 +364,7 @@ def resend_email(count, fromcall):
|
||||
reply = "-" + from_addr + " * " + body.decode(errors="ignore")
|
||||
# messaging.send_message(fromcall, reply)
|
||||
msg = messaging.TextMessage(
|
||||
CONFIG["aprsd"]["email"]["aprs"]["login"],
|
||||
CONFIG["aprs"]["login"],
|
||||
fromcall,
|
||||
reply,
|
||||
)
|
||||
@ -358,6 +402,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
self.msg_queues = msg_queues
|
||||
self.config = config
|
||||
|
||||
@trace.trace
|
||||
def run(self):
|
||||
global check_email_delay
|
||||
|
||||
@ -395,17 +440,30 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
try:
|
||||
server = _imap_connect()
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to get IMAP server Can't check email.", e)
|
||||
LOG.exception("IMAP failed to connect.", e)
|
||||
|
||||
if not server:
|
||||
continue
|
||||
|
||||
messages = server.search(["SINCE", today])
|
||||
try:
|
||||
messages = server.search(["SINCE", today])
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to search for messages since today.", e)
|
||||
continue
|
||||
LOG.debug("{} messages received today".format(len(messages)))
|
||||
|
||||
for msgid, data in server.fetch(messages, ["ENVELOPE"]).items():
|
||||
try:
|
||||
_msgs = server.fetch(messages, ["ENVELOPE"])
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to fetch/flag messages: ", e)
|
||||
continue
|
||||
|
||||
for msgid, data in _msgs.items():
|
||||
envelope = data[b"ENVELOPE"]
|
||||
# LOG.debug('ID:%d "%s" (%s)' % (msgid, envelope.subject.decode(), envelope.date))
|
||||
LOG.debug(
|
||||
'ID:%d "%s" (%s)'
|
||||
% (msgid, envelope.subject.decode(), envelope.date),
|
||||
)
|
||||
f = re.search(
|
||||
r"'([[A-a][0-9]_-]+@[[A-a][0-9]_-\.]+)",
|
||||
str(envelope.from_[0]),
|
||||
@ -418,16 +476,31 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
# LOG.debug("Message flags/tags: " + str(server.get_flags(msgid)[msgid]))
|
||||
# if "APRS" not in server.get_flags(msgid)[msgid]:
|
||||
# in python3, imap tags are unicode. in py2 they're strings. so .decode them to handle both
|
||||
taglist = [
|
||||
x.decode(errors="ignore")
|
||||
for x in server.get_flags(msgid)[msgid]
|
||||
]
|
||||
try:
|
||||
taglist = [
|
||||
x.decode(errors="ignore")
|
||||
for x in server.get_flags(msgid)[msgid]
|
||||
]
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to get flags.", e)
|
||||
break
|
||||
|
||||
if "APRS" not in taglist:
|
||||
# if msg not flagged as sent via aprs
|
||||
server.fetch([msgid], ["RFC822"])
|
||||
try:
|
||||
server.fetch([msgid], ["RFC822"])
|
||||
except Exception as e:
|
||||
LOG.exception("Failed single server fetch for RFC822", e)
|
||||
break
|
||||
|
||||
(body, from_addr) = parse_email(msgid, data, server)
|
||||
# unset seen flag, will stay bold in email client
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
try:
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
except Exception as e:
|
||||
LOG.exception("Failed to remove flags SEEN", e)
|
||||
# Not much we can do here, so lets try and
|
||||
# send the aprs message anyway
|
||||
|
||||
if from_addr in shortcuts_inverted:
|
||||
# reverse lookup of a shortcut
|
||||
@ -441,14 +514,28 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
)
|
||||
self.msg_queues["tx"].put(msg)
|
||||
# flag message as sent via aprs
|
||||
server.add_flags(msgid, ["APRS"])
|
||||
# unset seen flag, will stay bold in email client
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
try:
|
||||
server.add_flags(msgid, ["APRS"])
|
||||
# unset seen flag, will stay bold in email client
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't add APRS flag to email", e)
|
||||
|
||||
try:
|
||||
server.remove_flags(msgid, [imapclient.SEEN])
|
||||
except Exception as e:
|
||||
LOG.exception("Couldn't remove seen flag from email", e)
|
||||
|
||||
# check email more often since we just received an email
|
||||
check_email_delay = 60
|
||||
|
||||
# reset clock
|
||||
LOG.debug("Done looping over Server.fetch, logging out.")
|
||||
past = datetime.datetime.now()
|
||||
server.logout()
|
||||
try:
|
||||
server.logout()
|
||||
except Exception as e:
|
||||
LOG.exception("IMAP failed to logout: ", e)
|
||||
continue
|
||||
else:
|
||||
# We haven't hit the email delay yet.
|
||||
# LOG.debug("Delta({}) < {}".format(now - past, check_email_delay))
|
||||
@ -457,6 +544,3 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||
# Remove ourselves from the global threads list
|
||||
threads.APRSDThreadList().remove(self)
|
||||
LOG.info("Exiting")
|
||||
|
||||
|
||||
# end check_email()
|
||||
|
@ -1,21 +1,65 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
import aprsd
|
||||
from aprsd import messaging, stats
|
||||
import flask
|
||||
import flask_classful
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
auth = HTTPBasicAuth()
|
||||
users = None
|
||||
|
||||
|
||||
# HTTPBasicAuth doesn't work on a class method.
|
||||
# This has to be out here. Rely on the APRSDFlask
|
||||
# class to initialize the users from the config
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
global users
|
||||
|
||||
if username in users and check_password_hash(users.get(username), password):
|
||||
return username
|
||||
|
||||
|
||||
class APRSDFlask(flask_classful.FlaskView):
|
||||
config = None
|
||||
|
||||
def set_config(self, config):
|
||||
global users
|
||||
self.config = config
|
||||
self.users = {}
|
||||
for user in self.config["aprsd"]["web"]["users"]:
|
||||
self.users[user] = generate_password_hash(
|
||||
self.config["aprsd"]["web"]["users"][user],
|
||||
)
|
||||
|
||||
users = self.users
|
||||
|
||||
def index(self):
|
||||
return "Hello"
|
||||
# return flask.render_template("index.html", message=msg)
|
||||
|
||||
@auth.login_required
|
||||
def messages(self):
|
||||
track = messaging.MsgTrack()
|
||||
msgs = []
|
||||
for id in track:
|
||||
LOG.info(track[id].dict())
|
||||
msgs.append(track[id].dict())
|
||||
|
||||
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
||||
|
||||
@auth.login_required
|
||||
def save(self):
|
||||
"""Save the existing queue to disk."""
|
||||
track = messaging.MsgTrack()
|
||||
track.save()
|
||||
return json.dumps({"messages": "saved"})
|
||||
|
||||
def stats(self):
|
||||
stats_obj = stats.APRSDStats()
|
||||
track = messaging.MsgTrack()
|
||||
@ -30,9 +74,16 @@ class APRSDFlask(flask_classful.FlaskView):
|
||||
|
||||
|
||||
def init_flask(config):
|
||||
flask_app = flask.Flask("aprsd")
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="",
|
||||
static_folder="web/static",
|
||||
template_folder="web/templates",
|
||||
)
|
||||
server = APRSDFlask()
|
||||
server.set_config(config)
|
||||
# flask_app.route('/', methods=['GET'])(server.index)
|
||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||
flask_app.route("/save", methods=["GET"])(server.save)
|
||||
return flask_app
|
||||
|
@ -20,6 +20,7 @@
|
||||
#
|
||||
|
||||
# python included libs
|
||||
import datetime
|
||||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
@ -27,12 +28,11 @@ import os
|
||||
import queue
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import client, email, flask, messaging, plugin, stats, threads, utils
|
||||
from aprsd import client, email, flask, messaging, plugin, stats, threads, trace, utils
|
||||
import aprslib
|
||||
from aprslib.exceptions import LoginError
|
||||
import click
|
||||
@ -52,7 +52,9 @@ LOG_LEVELS = {
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
server_event = threading.Event()
|
||||
flask_enabled = False
|
||||
|
||||
# server_event = threading.Event()
|
||||
|
||||
# localization, please edit:
|
||||
# HOST = "noam.aprs2.net" # north america tier2 servers round robin
|
||||
@ -150,20 +152,23 @@ def install(append, case_insensitive, shell, path):
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global server_vent
|
||||
global flask_enabled
|
||||
|
||||
LOG.info(
|
||||
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds to exit all threads",
|
||||
)
|
||||
threads.APRSDThreadList().stop_all()
|
||||
server_event.set()
|
||||
LOG.info("EXITING STATS")
|
||||
LOG.info(stats.APRSDStats())
|
||||
# time.sleep(1)
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
# end signal_handler
|
||||
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
|
||||
@ -184,10 +189,21 @@ def setup_logging(config, loglevel, quiet):
|
||||
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()
|
||||
@ -394,9 +410,7 @@ def server(
|
||||
flush,
|
||||
):
|
||||
"""Start the aprsd server process."""
|
||||
global event
|
||||
|
||||
event = threading.Event()
|
||||
global flask_enabled
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
if not quiet:
|
||||
@ -410,6 +424,8 @@ def server(
|
||||
email.CONFIG = config
|
||||
|
||||
setup_logging(config, loglevel, quiet)
|
||||
if config["aprsd"].get("trace", False):
|
||||
trace.setup_tracing(["method", "api"])
|
||||
LOG.info("APRSD Started version: {}".format(aprsd.__version__))
|
||||
stats.APRSDStats(config)
|
||||
|
||||
@ -468,6 +484,7 @@ def server(
|
||||
web_enabled = False
|
||||
|
||||
if web_enabled:
|
||||
flask_enabled = True
|
||||
app = flask.init_flask(config)
|
||||
app.run(
|
||||
host=config["aprsd"]["web"]["host"],
|
||||
@ -475,10 +492,8 @@ def server(
|
||||
)
|
||||
|
||||
# If there are items in the msgTracker, then save them
|
||||
tracker = messaging.MsgTrack()
|
||||
tracker.save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
LOG.info("APRSD Exiting.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
@ -53,6 +53,38 @@ class MsgTrack:
|
||||
cls._instance.lock = threading.Lock()
|
||||
return cls._instance
|
||||
|
||||
def __getitem__(self, name):
|
||||
with self.lock:
|
||||
return self.track[name]
|
||||
|
||||
def __iter__(self):
|
||||
with self.lock:
|
||||
return iter(self.track)
|
||||
|
||||
def keys(self):
|
||||
with self.lock:
|
||||
return self.track.keys()
|
||||
|
||||
def items(self):
|
||||
with self.lock:
|
||||
return self.track.items()
|
||||
|
||||
def values(self):
|
||||
with self.lock:
|
||||
return self.track.values()
|
||||
|
||||
def __len__(self):
|
||||
with self.lock:
|
||||
return len(self.track)
|
||||
|
||||
def __str__(self):
|
||||
with self.lock:
|
||||
result = "{"
|
||||
for key in self.track.keys():
|
||||
result += "{}: {}, ".format(key, str(self.track[key]))
|
||||
result += "}"
|
||||
return result
|
||||
|
||||
def add(self, msg):
|
||||
with self.lock:
|
||||
key = int(msg.id)
|
||||
@ -71,24 +103,18 @@ class MsgTrack:
|
||||
if key in self.track.keys():
|
||||
del self.track[key]
|
||||
|
||||
def __len__(self):
|
||||
with self.lock:
|
||||
return len(self.track)
|
||||
|
||||
def __str__(self):
|
||||
with self.lock:
|
||||
result = "{"
|
||||
for key in self.track.keys():
|
||||
result += "{}: {}, ".format(key, str(self.track[key]))
|
||||
result += "}"
|
||||
return result
|
||||
|
||||
def save(self):
|
||||
"""Save this shit to disk?"""
|
||||
"""Save any queued to disk?"""
|
||||
LOG.debug("Save tracker to disk? {}".format(len(self)))
|
||||
if len(self) > 0:
|
||||
LOG.info("Saving {} tracking messages to disk".format(len(self)))
|
||||
pickle.dump(self.dump(), open(utils.DEFAULT_SAVE_FILE, "wb+"))
|
||||
else:
|
||||
LOG.debug(
|
||||
"Nothing to save, flushing old save file '{}'".format(
|
||||
utils.DEFAULT_SAVE_FILE,
|
||||
),
|
||||
)
|
||||
self.flush()
|
||||
|
||||
def dump(self):
|
||||
@ -229,8 +255,17 @@ class RawMessage(Message):
|
||||
super().__init__(None, None, msg_id=None)
|
||||
self.message = message
|
||||
|
||||
def __repr__(self):
|
||||
return self.message
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
return {
|
||||
"type": "raw",
|
||||
"message": self.message.rstrip("\n"),
|
||||
"raw": self.message.rstrip("\n"),
|
||||
"retry_count": self.retry_count,
|
||||
"last_send_attempt": self.last_send_attempt,
|
||||
"last_send_time": str(self.last_send_time),
|
||||
"last_send_age": str(now - self.last_send_time),
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return self.message
|
||||
@ -246,12 +281,12 @@ class RawMessage(Message):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
self.message,
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
stats.APRSDStats().msgs_sent_inc()
|
||||
|
||||
|
||||
@ -267,7 +302,22 @@ class TextMessage(Message):
|
||||
# an ack? Some messages we don't want to do this ever.
|
||||
self.allow_delay = allow_delay
|
||||
|
||||
def __repr__(self):
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": "text-message",
|
||||
"fromcall": self.fromcall,
|
||||
"tocall": self.tocall,
|
||||
"message": self.message.rstrip("\n"),
|
||||
"raw": str(self).rstrip("\n"),
|
||||
"retry_count": self.retry_count,
|
||||
"last_send_attempt": self.last_send_attempt,
|
||||
"last_send_time": str(self.last_send_time),
|
||||
"last_send_age": str(now - self.last_send_time),
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
"""Build raw string to send over the air."""
|
||||
return "{}>APZ100::{}:{}{{{}\n".format(
|
||||
self.fromcall,
|
||||
@ -276,19 +326,6 @@ class TextMessage(Message):
|
||||
str(self.id),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
delta = "Never"
|
||||
if self.last_send_time:
|
||||
now = datetime.datetime.now()
|
||||
delta = now - self.last_send_time
|
||||
return "{}>{} Msg({})({}): '{}'".format(
|
||||
self.fromcall,
|
||||
self.tocall,
|
||||
self.id,
|
||||
delta,
|
||||
self.message,
|
||||
)
|
||||
|
||||
def _filter_for_send(self):
|
||||
"""Filter and format message string for FCC."""
|
||||
# max? ftm400 displays 64, raw msg shows 74
|
||||
@ -311,12 +348,12 @@ class TextMessage(Message):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending Message Direct",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
self.message,
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
|
||||
|
||||
@ -370,13 +407,13 @@ class SendMessageThread(threads.APRSDThread):
|
||||
# tracking the time.
|
||||
log_message(
|
||||
"Sending Message",
|
||||
repr(msg).rstrip("\n"),
|
||||
str(msg).rstrip("\n"),
|
||||
msg.message,
|
||||
tocall=self.msg.tocall,
|
||||
retry_number=msg.last_send_attempt,
|
||||
msg_num=msg.id,
|
||||
)
|
||||
cl.sendall(repr(msg))
|
||||
cl.sendall(str(msg))
|
||||
stats.APRSDStats().msgs_tx_inc()
|
||||
msg.last_send_time = datetime.datetime.now()
|
||||
msg.last_send_attempt += 1
|
||||
@ -392,29 +429,40 @@ class AckMessage(Message):
|
||||
def __init__(self, fromcall, tocall, msg_id):
|
||||
super().__init__(fromcall, tocall, msg_id=msg_id)
|
||||
|
||||
def __repr__(self):
|
||||
def dict(self):
|
||||
now = datetime.datetime.now()
|
||||
return {
|
||||
"id": self.id,
|
||||
"type": "ack",
|
||||
"fromcall": self.fromcall,
|
||||
"tocall": self.tocall,
|
||||
"raw": str(self).rstrip("\n"),
|
||||
"retry_count": self.retry_count,
|
||||
"last_send_attempt": self.last_send_attempt,
|
||||
"last_send_time": str(self.last_send_time),
|
||||
"last_send_age": str(now - self.last_send_time),
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return "{}>APZ100::{}:ack{}\n".format(
|
||||
self.fromcall,
|
||||
self.tocall.ljust(9),
|
||||
self.id,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "From({}) TO({}) Ack ({})".format(self.fromcall, self.tocall, self.id)
|
||||
|
||||
def send_thread(self):
|
||||
"""Separate thread to send acks with retries."""
|
||||
cl = client.get_client()
|
||||
for i in range(self.retry_count, 0, -1):
|
||||
log_message(
|
||||
"Sending ack",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
None,
|
||||
ack=self.id,
|
||||
tocall=self.tocall,
|
||||
retry_number=i,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
stats.APRSDStats().ack_tx_inc()
|
||||
# aprs duplicate detection is 30 secs?
|
||||
# (21 only sends first, 28 skips middle)
|
||||
@ -433,13 +481,13 @@ class AckMessage(Message):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending ack",
|
||||
repr(self).rstrip("\n"),
|
||||
str(self).rstrip("\n"),
|
||||
None,
|
||||
ack=self.id,
|
||||
tocall=self.tocall,
|
||||
fromcall=self.fromcall,
|
||||
)
|
||||
cl.sendall(repr(self))
|
||||
cl.sendall(str(self))
|
||||
|
||||
|
||||
class SendAckThread(threads.APRSDThread):
|
||||
@ -476,13 +524,13 @@ class SendAckThread(threads.APRSDThread):
|
||||
cl = client.get_client()
|
||||
log_message(
|
||||
"Sending ack",
|
||||
repr(self.ack).rstrip("\n"),
|
||||
str(self.ack).rstrip("\n"),
|
||||
None,
|
||||
ack=self.ack.id,
|
||||
tocall=self.ack.tocall,
|
||||
retry_number=self.ack.last_send_attempt,
|
||||
)
|
||||
cl.sendall(repr(self.ack))
|
||||
cl.sendall(str(self.ack))
|
||||
stats.APRSDStats().ack_tx_inc()
|
||||
self.ack.last_send_attempt += 1
|
||||
self.ack.last_send_time = datetime.datetime.now()
|
||||
|
@ -2,7 +2,7 @@ import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from aprsd import email, messaging, plugin
|
||||
from aprsd import email, messaging, plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@ -18,6 +18,7 @@ class EmailPlugin(plugin.APRSDPluginBase):
|
||||
# five mins {int:int}
|
||||
email_sent_dict = {}
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Email COMMAND")
|
||||
reply = None
|
||||
@ -79,6 +80,7 @@ class EmailPlugin(plugin.APRSDPluginBase):
|
||||
self.email_sent_dict.clear()
|
||||
self.email_sent_dict[ack] = now
|
||||
else:
|
||||
reply = messaging.NULL_MESSAGE
|
||||
LOG.info(
|
||||
"Email for message number "
|
||||
+ ack
|
||||
|
@ -2,7 +2,7 @@ import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from aprsd import plugin
|
||||
from aprsd import plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@ -14,6 +14,7 @@ class FortunePlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[fF]"
|
||||
command_name = "fortune"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("FortunePlugin")
|
||||
reply = None
|
||||
|
@ -2,7 +2,7 @@ import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from aprsd import plugin, plugin_utils, utils
|
||||
from aprsd import plugin, plugin_utils, trace, utils
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@ -14,6 +14,7 @@ class LocationPlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[lL]"
|
||||
command_name = "location"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Location Plugin")
|
||||
# get last location of a callsign, get descriptive name from weather service
|
||||
@ -65,6 +66,10 @@ class LocationPlugin(plugin.APRSDPluginBase):
|
||||
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(ex))
|
||||
wx_data = {"location": {"areaDescription": "Unknown Location"}}
|
||||
|
||||
if "location" not in wx_data:
|
||||
LOG.error("Couldn't fetch forecast.weather.gov '{}'".format(wx_data))
|
||||
wx_data = {"location": {"areaDescription": "Unknown Location"}}
|
||||
|
||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||
searchcall,
|
||||
wx_data["location"]["areaDescription"],
|
||||
|
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aprsd import plugin
|
||||
from aprsd import plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@ -13,6 +13,7 @@ class PingPlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[pP]"
|
||||
command_name = "ping"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("PINGPlugin")
|
||||
stm = time.localtime()
|
||||
|
@ -2,7 +2,7 @@ import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
from aprsd import messaging, plugin
|
||||
from aprsd import messaging, plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@ -14,6 +14,7 @@ class QueryPlugin(plugin.APRSDPluginBase):
|
||||
command_regex = r"^\!.*"
|
||||
command_name = "query"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Query COMMAND")
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from aprsd import fuzzyclock, plugin, plugin_utils, utils
|
||||
from aprsd import fuzzyclock, plugin, plugin_utils, trace, utils
|
||||
from opencage.geocoder import OpenCageGeocode
|
||||
import pytz
|
||||
|
||||
@ -38,6 +38,7 @@ class TimePlugin(plugin.APRSDPluginBase):
|
||||
|
||||
return reply
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("TIME COMMAND")
|
||||
# So we can mock this in unit tests
|
||||
@ -52,6 +53,7 @@ class TimeOpenCageDataPlugin(TimePlugin):
|
||||
command_regex = "^[tT]"
|
||||
command_name = "Time"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
try:
|
||||
@ -92,6 +94,7 @@ class TimeOWMPlugin(TimePlugin):
|
||||
command_regex = "^[tT]"
|
||||
command_name = "Time"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
api_key = self.config["services"]["aprs.fi"]["apiKey"]
|
||||
try:
|
||||
|
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
|
||||
import aprsd
|
||||
from aprsd import plugin
|
||||
from aprsd import plugin, trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
@ -17,6 +17,7 @@ class VersionPlugin(plugin.APRSDPluginBase):
|
||||
# five mins {int:int}
|
||||
email_sent_dict = {}
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Version COMMAND")
|
||||
return "APRSD version '{}'".format(aprsd.__version__)
|
||||
|
@ -2,7 +2,7 @@ import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from aprsd import plugin, plugin_utils, utils
|
||||
from aprsd import plugin, plugin_utils, trace, utils
|
||||
import requests
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@ -25,6 +25,7 @@ class USWeatherPlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[wW]"
|
||||
command_name = "weather"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("Weather Plugin")
|
||||
try:
|
||||
@ -84,6 +85,7 @@ class USMetarPlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[metar]"
|
||||
command_name = "Metar"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("WX Plugin '{}'".format(message))
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
@ -175,6 +177,7 @@ class OWMWeatherPlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[wW]"
|
||||
command_name = "Weather"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("OWMWeather Plugin '{}'".format(message))
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
@ -295,6 +298,7 @@ class AVWXWeatherPlugin(plugin.APRSDPluginBase):
|
||||
command_regex = "^[metar]"
|
||||
command_name = "Weather"
|
||||
|
||||
@trace.trace
|
||||
def command(self, fromcall, message, ack):
|
||||
LOG.info("OWMWeather Plugin '{}'".format(message))
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
|
@ -4,8 +4,9 @@ import logging
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import tracemalloc
|
||||
|
||||
from aprsd import client, messaging, plugin, stats
|
||||
from aprsd import client, messaging, plugin, stats, trace
|
||||
import aprslib
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
@ -69,6 +70,7 @@ class KeepAliveThread(APRSDThread):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("KeepAlive")
|
||||
tracemalloc.start()
|
||||
|
||||
def loop(self):
|
||||
if self.cntr % 6 == 0:
|
||||
@ -81,14 +83,17 @@ class KeepAliveThread(APRSDThread):
|
||||
else:
|
||||
email_thread_time = "N/A"
|
||||
|
||||
current, peak = tracemalloc.get_traced_memory()
|
||||
LOG.debug(
|
||||
"Uptime ({}) Tracker({}) "
|
||||
"Msgs: TX:{} RX:{} EmailThread: {}".format(
|
||||
"Msgs: TX:{} RX:{} EmailThread: {} RAM: Current:{} Peak:{}".format(
|
||||
stats_obj.uptime,
|
||||
len(tracker),
|
||||
stats_obj.msgs_tx,
|
||||
stats_obj.msgs_rx,
|
||||
email_thread_time,
|
||||
current,
|
||||
peak,
|
||||
),
|
||||
)
|
||||
self.cntr += 1
|
||||
@ -219,11 +224,11 @@ class APRSDRXThread(APRSDThread):
|
||||
self.msg_queues["tx"].put(ack)
|
||||
LOG.debug("Packet processing complete")
|
||||
|
||||
@trace.trace
|
||||
def process_packet(self, packet):
|
||||
"""Process a packet recieved from aprs-is server."""
|
||||
|
||||
try:
|
||||
LOG.info("Got message: {}".format(packet))
|
||||
stats.APRSDStats().msgs_rx_inc()
|
||||
|
||||
msg = packet.get("message_text", None)
|
||||
|
181
aprsd/trace.py
Normal file
181
aprsd/trace.py
Normal file
@ -0,0 +1,181 @@
|
||||
import abc
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import time
|
||||
import types
|
||||
|
||||
VALID_TRACE_FLAGS = {"method", "api"}
|
||||
TRACE_API = False
|
||||
TRACE_METHOD = False
|
||||
TRACE_ENABLED = False
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
def trace(*dec_args, **dec_kwargs):
|
||||
"""Trace calls to the decorated function.
|
||||
|
||||
This decorator should always be defined as the outermost decorator so it
|
||||
is defined last. This is important so it does not interfere
|
||||
with other decorators.
|
||||
|
||||
Using this decorator on a function will cause its execution to be logged at
|
||||
`DEBUG` level with arguments, return values, and exceptions.
|
||||
|
||||
:returns: a function decorator
|
||||
"""
|
||||
|
||||
def _decorator(f):
|
||||
|
||||
func_name = f.__name__
|
||||
|
||||
@functools.wraps(f)
|
||||
def trace_logging_wrapper(*args, **kwargs):
|
||||
filter_function = dec_kwargs.get("filter_function")
|
||||
logger = LOG
|
||||
|
||||
# NOTE(ameade): Don't bother going any further if DEBUG log level
|
||||
# is not enabled for the logger.
|
||||
if not logger.isEnabledFor(logging.DEBUG) or not TRACE_ENABLED:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
all_args = inspect.getcallargs(f, *args, **kwargs)
|
||||
|
||||
pass_filter = filter_function is None or filter_function(all_args)
|
||||
|
||||
if pass_filter:
|
||||
logger.debug(
|
||||
"==> %(func)s: call %(all_args)r",
|
||||
{
|
||||
"func": func_name,
|
||||
"all_args": str(all_args),
|
||||
},
|
||||
)
|
||||
|
||||
start_time = time.time() * 1000
|
||||
try:
|
||||
result = f(*args, **kwargs)
|
||||
except Exception as exc:
|
||||
total_time = int(round(time.time() * 1000)) - start_time
|
||||
logger.debug(
|
||||
"<== %(func)s: exception (%(time)dms) %(exc)r",
|
||||
{
|
||||
"func": func_name,
|
||||
"time": total_time,
|
||||
"exc": exc,
|
||||
},
|
||||
)
|
||||
raise
|
||||
total_time = int(round(time.time() * 1000)) - start_time
|
||||
|
||||
if isinstance(result, dict):
|
||||
mask_result = result
|
||||
elif isinstance(result, str):
|
||||
mask_result = result
|
||||
else:
|
||||
mask_result = result
|
||||
|
||||
if pass_filter:
|
||||
logger.debug(
|
||||
"<== %(func)s: return (%(time)dms) %(result)r",
|
||||
{
|
||||
"func": func_name,
|
||||
"time": total_time,
|
||||
"result": mask_result,
|
||||
},
|
||||
)
|
||||
return result
|
||||
|
||||
return trace_logging_wrapper
|
||||
|
||||
if len(dec_args) == 0:
|
||||
# filter_function is passed and args does not contain f
|
||||
return _decorator
|
||||
else:
|
||||
# filter_function is not passed
|
||||
return _decorator(dec_args[0])
|
||||
|
||||
|
||||
def trace_api(*dec_args, **dec_kwargs):
|
||||
"""Decorates a function if TRACE_API is true."""
|
||||
|
||||
def _decorator(f):
|
||||
@functools.wraps(f)
|
||||
def trace_api_logging_wrapper(*args, **kwargs):
|
||||
if TRACE_API:
|
||||
return trace(f, *dec_args, **dec_kwargs)(*args, **kwargs)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return trace_api_logging_wrapper
|
||||
|
||||
if len(dec_args) == 0:
|
||||
# filter_function is passed and args does not contain f
|
||||
return _decorator
|
||||
else:
|
||||
# filter_function is not passed
|
||||
return _decorator(dec_args[0])
|
||||
|
||||
|
||||
def trace_method(f):
|
||||
"""Decorates a function if TRACE_METHOD is true."""
|
||||
|
||||
@functools.wraps(f)
|
||||
def trace_method_logging_wrapper(*args, **kwargs):
|
||||
if TRACE_METHOD:
|
||||
return trace(f)(*args, **kwargs)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return trace_method_logging_wrapper
|
||||
|
||||
|
||||
class TraceWrapperMetaclass(type):
|
||||
"""Metaclass that wraps all methods of a class with trace_method.
|
||||
|
||||
This metaclass will cause every function inside of the class to be
|
||||
decorated with the trace_method decorator.
|
||||
|
||||
To use the metaclass you define a class like so:
|
||||
class MyClass(object, metaclass=utils.TraceWrapperMetaclass):
|
||||
"""
|
||||
|
||||
def __new__(cls, classname, bases, class_dict):
|
||||
new_class_dict = {}
|
||||
for attribute_name, attribute in class_dict.items():
|
||||
if isinstance(attribute, types.FunctionType):
|
||||
# replace it with a wrapped version
|
||||
attribute = functools.update_wrapper(
|
||||
trace_method(attribute),
|
||||
attribute,
|
||||
)
|
||||
new_class_dict[attribute_name] = attribute
|
||||
|
||||
return type.__new__(cls, classname, bases, new_class_dict)
|
||||
|
||||
|
||||
class TraceWrapperWithABCMetaclass(abc.ABCMeta, TraceWrapperMetaclass):
|
||||
"""Metaclass that wraps all methods of a class with trace."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def setup_tracing(trace_flags):
|
||||
"""Set global variables for each trace flag.
|
||||
|
||||
Sets variables TRACE_METHOD and TRACE_API, which represent
|
||||
whether to log methods or api traces.
|
||||
|
||||
:param trace_flags: a list of strings
|
||||
"""
|
||||
global TRACE_METHOD
|
||||
global TRACE_API
|
||||
global TRACE_ENABLED
|
||||
|
||||
try:
|
||||
trace_flags = [flag.strip() for flag in trace_flags]
|
||||
except TypeError: # Handle when trace_flags is None or a test mock
|
||||
trace_flags = []
|
||||
for invalid_flag in set(trace_flags) - VALID_TRACE_FLAGS:
|
||||
LOG.warning("Invalid trace flag: %s", invalid_flag)
|
||||
TRACE_METHOD = "method" in trace_flags
|
||||
TRACE_API = "api" in trace_flags
|
||||
TRACE_ENABLED = True
|
@ -22,6 +22,7 @@ DEFAULT_CONFIG_DICT = {
|
||||
},
|
||||
"aprsd": {
|
||||
"logfile": "/tmp/aprsd.log",
|
||||
"trace": False,
|
||||
"plugin_dir": "~/.config/aprsd/plugins",
|
||||
"enabled_plugins": plugin.CORE_PLUGINS,
|
||||
"units": "imperial",
|
||||
@ -29,6 +30,9 @@ DEFAULT_CONFIG_DICT = {
|
||||
"enabled": True,
|
||||
"host": "0.0.0.0",
|
||||
"port": 8001,
|
||||
"users": {
|
||||
"admin": "aprsd",
|
||||
},
|
||||
},
|
||||
"email": {
|
||||
"enabled": True,
|
||||
@ -43,6 +47,7 @@ DEFAULT_CONFIG_DICT = {
|
||||
"host": "smtp.gmail.com",
|
||||
"port": 465,
|
||||
"use_ssl": False,
|
||||
"debug": False,
|
||||
},
|
||||
"imap": {
|
||||
"login": "IMAP_USERNAME",
|
||||
@ -50,6 +55,7 @@ DEFAULT_CONFIG_DICT = {
|
||||
"host": "imap.gmail.com",
|
||||
"port": 993,
|
||||
"use_ssl": True,
|
||||
"debug": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -297,6 +303,15 @@ def parse_config(config_file):
|
||||
["aprs", "password"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprs"]["password"],
|
||||
)
|
||||
|
||||
# Ensure they change the admin password
|
||||
if config["aprsd"]["web"]["enabled"] is True:
|
||||
check_option(
|
||||
config,
|
||||
["aprsd", "web", "users", "admin"],
|
||||
default_fail=DEFAULT_CONFIG_DICT["aprsd"]["web"]["users"]["admin"],
|
||||
)
|
||||
|
||||
if config["aprsd"]["email"]["enabled"] is True:
|
||||
# Check IMAP server settings
|
||||
check_option(config, ["aprsd", "email", "imap", "host"])
|
||||
|
57
aprsd/web/static/json-viewer/jquery.json-viewer.css
Normal file
57
aprsd/web/static/json-viewer/jquery.json-viewer.css
Normal file
@ -0,0 +1,57 @@
|
||||
/* Root element */
|
||||
.json-document {
|
||||
padding: 1em 2em;
|
||||
}
|
||||
|
||||
/* Syntax highlighting for JSON objects */
|
||||
ul.json-dict, ol.json-array {
|
||||
list-style-type: none;
|
||||
margin: 0 0 0 1px;
|
||||
border-left: 1px dotted #ccc;
|
||||
padding-left: 2em;
|
||||
}
|
||||
.json-string {
|
||||
color: #0B7500;
|
||||
}
|
||||
.json-literal {
|
||||
color: #1A01CC;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Toggle button */
|
||||
a.json-toggle {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-toggle:focus {
|
||||
outline: none;
|
||||
}
|
||||
a.json-toggle:before {
|
||||
font-size: 1.1em;
|
||||
color: #c0c0c0;
|
||||
content: "\25BC"; /* down arrow */
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
text-align: center;
|
||||
line-height: 1em;
|
||||
left: -1.2em;
|
||||
}
|
||||
a.json-toggle:hover:before {
|
||||
color: #aaa;
|
||||
}
|
||||
a.json-toggle.collapsed:before {
|
||||
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
/* Collapsable placeholder links */
|
||||
a.json-placeholder {
|
||||
color: #aaa;
|
||||
padding: 0 1em;
|
||||
text-decoration: none;
|
||||
}
|
||||
a.json-placeholder:hover {
|
||||
text-decoration: underline;
|
||||
}
|
158
aprsd/web/static/json-viewer/jquery.json-viewer.js
Normal file
158
aprsd/web/static/json-viewer/jquery.json-viewer.js
Normal file
@ -0,0 +1,158 @@
|
||||
/**
|
||||
* jQuery json-viewer
|
||||
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
|
||||
* @link: https://github.com/abodelot/jquery.json-viewer
|
||||
*/
|
||||
(function($) {
|
||||
|
||||
/**
|
||||
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
|
||||
* @return boolean
|
||||
*/
|
||||
function isCollapsable(arg) {
|
||||
return arg instanceof Object && Object.keys(arg).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a string represents a valid url
|
||||
* @return boolean
|
||||
*/
|
||||
function isUrl(string) {
|
||||
var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i;
|
||||
return urlRegexp.test(string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a json object into html representation
|
||||
* @return string
|
||||
*/
|
||||
function json2html(json, options) {
|
||||
var html = '';
|
||||
if (typeof json === 'string') {
|
||||
// Escape tags and quotes
|
||||
json = json
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/"/g, '"');
|
||||
|
||||
if (options.withLinks && isUrl(json)) {
|
||||
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
|
||||
} else {
|
||||
// Escape double quotes in the rendered non-URL string.
|
||||
json = json.replace(/"/g, '\\"');
|
||||
html += '<span class="json-string">"' + json + '"</span>';
|
||||
}
|
||||
} else if (typeof json === 'number') {
|
||||
html += '<span class="json-literal">' + json + '</span>';
|
||||
} else if (typeof json === 'boolean') {
|
||||
html += '<span class="json-literal">' + json + '</span>';
|
||||
} else if (json === null) {
|
||||
html += '<span class="json-literal">null</span>';
|
||||
} else if (json instanceof Array) {
|
||||
if (json.length > 0) {
|
||||
html += '[<ol class="json-array">';
|
||||
for (var i = 0; i < json.length; ++i) {
|
||||
html += '<li>';
|
||||
// Add toggle button if item is collapsable
|
||||
if (isCollapsable(json[i])) {
|
||||
html += '<a href class="json-toggle"></a>';
|
||||
}
|
||||
html += json2html(json[i], options);
|
||||
// Add comma if item is not last
|
||||
if (i < json.length - 1) {
|
||||
html += ',';
|
||||
}
|
||||
html += '</li>';
|
||||
}
|
||||
html += '</ol>]';
|
||||
} else {
|
||||
html += '[]';
|
||||
}
|
||||
} else if (typeof json === 'object') {
|
||||
var keyCount = Object.keys(json).length;
|
||||
if (keyCount > 0) {
|
||||
html += '{<ul class="json-dict">';
|
||||
for (var key in json) {
|
||||
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
||||
html += '<li>';
|
||||
var keyRepr = options.withQuotes ?
|
||||
'<span class="json-string">"' + key + '"</span>' : key;
|
||||
// Add toggle button if item is collapsable
|
||||
if (isCollapsable(json[key])) {
|
||||
html += '<a href class="json-toggle">' + keyRepr + '</a>';
|
||||
} else {
|
||||
html += keyRepr;
|
||||
}
|
||||
html += ': ' + json2html(json[key], options);
|
||||
// Add comma if item is not last
|
||||
if (--keyCount > 0) {
|
||||
html += ',';
|
||||
}
|
||||
html += '</li>';
|
||||
}
|
||||
}
|
||||
html += '</ul>}';
|
||||
} else {
|
||||
html += '{}';
|
||||
}
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
/**
|
||||
* jQuery plugin method
|
||||
* @param json: a javascript object
|
||||
* @param options: an optional options hash
|
||||
*/
|
||||
$.fn.jsonViewer = function(json, options) {
|
||||
// Merge user options with default options
|
||||
options = Object.assign({}, {
|
||||
collapsed: false,
|
||||
rootCollapsable: true,
|
||||
withQuotes: false,
|
||||
withLinks: true
|
||||
}, options);
|
||||
|
||||
// jQuery chaining
|
||||
return this.each(function() {
|
||||
|
||||
// Transform to HTML
|
||||
var html = json2html(json, options);
|
||||
if (options.rootCollapsable && isCollapsable(json)) {
|
||||
html = '<a href class="json-toggle"></a>' + html;
|
||||
}
|
||||
|
||||
// Insert HTML in target DOM element
|
||||
$(this).html(html);
|
||||
$(this).addClass('json-document');
|
||||
|
||||
// Bind click on toggle buttons
|
||||
$(this).off('click');
|
||||
$(this).on('click', 'a.json-toggle', function() {
|
||||
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
|
||||
target.toggle();
|
||||
if (target.is(':visible')) {
|
||||
target.siblings('.json-placeholder').remove();
|
||||
} else {
|
||||
var count = target.children('li').length;
|
||||
var placeholder = count + (count > 1 ? ' items' : ' item');
|
||||
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Simulate click on toggle button when placeholder is clicked
|
||||
$(this).on('click', 'a.json-placeholder', function() {
|
||||
$(this).siblings('a.json-toggle').click();
|
||||
return false;
|
||||
});
|
||||
|
||||
if (options.collapsed == true) {
|
||||
// Trigger click to collapse all nodes
|
||||
$(this).find('a.json-toggle').click();
|
||||
}
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
15
aprsd/web/templates/messages.html
Normal file
15
aprsd/web/templates/messages.html
Normal file
@ -0,0 +1,15 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
|
||||
<script src="json-viewer/jquery.json-viewer.js"></script>
|
||||
<link href="json-viewer/jquery.json-viewer.css" type="text/css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<pre id="json-viewer"></pre>
|
||||
|
||||
<script>
|
||||
var data = {{ messages | safe }}
|
||||
$('#json-viewer').jsonViewer(data)
|
||||
</script>
|
||||
|
||||
</html>
|
@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Use this script to locally build the docker image
|
||||
docker build --no-cache -t hemna6969/aprsd:latest ..
|
@ -1,12 +1,12 @@
|
||||
FROM alpine:latest as aprsd
|
||||
|
||||
# Dockerfile for building a container during aprsd development.
|
||||
ARG BRANCH
|
||||
|
||||
ENV VERSION=1.5.1
|
||||
ENV APRS_USER=aprs
|
||||
ENV HOME=/home/aprs
|
||||
ENV APRSD=http://github.com/craigerl/aprsd.git
|
||||
ENV APRSD_BRANCH="master"
|
||||
ENV APRSD_BRANCH=$BRANCH
|
||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
||||
|
||||
ENV INSTALL=$HOME/install
|
||||
@ -46,5 +46,5 @@ RUN chown -R $APRS_USER:$APRS_USER /config
|
||||
ENV CONF default
|
||||
USER $APRS_USER
|
||||
|
||||
ADD build/bin/run.sh $HOME/
|
||||
ADD bin/run.sh $HOME/
|
||||
ENTRYPOINT ["/home/aprs/run.sh"]
|
4
docker/build.sh
Executable file
4
docker/build.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Use this script to locally build the docker image
|
||||
docker build --no-cache -t hemna6969/aprsd:latest -f ./Dockerfile .
|
@ -13,4 +13,5 @@ pre-commit
|
||||
pytz
|
||||
opencage
|
||||
flask
|
||||
flask_classful
|
||||
flask-classful
|
||||
flask-httpauth
|
||||
|
@ -37,10 +37,13 @@ filelock==3.0.12
|
||||
# virtualenv
|
||||
flask-classful==0.14.2
|
||||
# via -r requirements.in
|
||||
flask-httpauth==4.2.0
|
||||
# via -r requirements.in
|
||||
flask==1.1.2
|
||||
# via
|
||||
# -r requirements.in
|
||||
# flask-classful
|
||||
# flask-httpauth
|
||||
identify==1.5.13
|
||||
# via pre-commit
|
||||
idna==2.10
|
||||
|
Loading…
x
Reference in New Issue
Block a user