1
0
mirror of https://github.com/craigerl/aprsd.git synced 2024-11-13 20:01:48 -05:00

Added objectstore Mixin

This patch adds the new objectstore Mixin class that enables
classes that store their date in self.data as a serializeable dict,
to be able to be stored to disk at shutdown and loaded at startup.

The SeenList and WatchList are now saved/loaded to/from disk.
This commit is contained in:
Hemna 2021-10-20 14:07:22 -04:00
parent 9b2212245f
commit 4233827dea
7 changed files with 123 additions and 30 deletions

View File

@ -140,9 +140,9 @@ class Config(collections.UserDict):
""" """
Example: Example:
d = {'meta': {'status': 'OK', 'status_code': 200}} d = {'meta': {'status': 'OK', 'status_code': 200}}
deep_get(d, ['meta', 'status_code']) # => 200 _get(d, ['meta', 'status_code']) # => 200
deep_get(d, ['garbage', 'status_code']) # => None _get(d, ['garbage', 'status_code']) # => None
deep_get(d, ['meta', 'garbage'], default='-') # => '-' _get(d, ['meta', 'garbage'], default='-') # => '-'
""" """
if type(keys) is str and "." in keys: if type(keys) is str and "." in keys:
@ -166,7 +166,7 @@ class Config(collections.UserDict):
def exists(self, path): def exists(self, path):
"""See if a conf value exists.""" """See if a conf value exists."""
test = "-3.14TEST41.3-" test = "-3.14TEST41.3-"
return (self.get(path, default=test) != test) return self.get(path, default=test) != test
def check_option(self, path, default_fail=None): def check_option(self, path, default_fail=None):
"""Make sure the config option doesn't have default value.""" """Make sure the config option doesn't have default value."""

View File

@ -211,10 +211,9 @@ def test_plugin(
config = aprsd_config.parse_config(config_file) config = aprsd_config.parse_config(config_file)
setup_logging(config, loglevel, False) setup_logging(config, loglevel, False)
LOG.info(f"Test APRSD PLugin version: {aprsd.__version__}") LOG.info(f"Test APRSD Plgin version: {aprsd.__version__}")
if type(message) is tuple: if type(message) is tuple:
message = " ".join(message) message = " ".join(message)
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
client.Client(config) client.Client(config)
pm = plugin.PluginManager(config) pm = plugin.PluginManager(config)
@ -224,6 +223,11 @@ def test_plugin(
pm._init() pm._init()
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config) obj = pm._create_class(plugin_path, plugin.APRSDPluginBase, config=config)
# Register the plugin they wanted tested. # Register the plugin they wanted tested.
LOG.info(
"Testing plugin {} Version {}".format(
obj.__class__, obj.version,
),
)
pm._pluggy_pm.register(obj) pm._pluggy_pm.register(obj)
login = config["aprs"]["login"] login = config["aprs"]["login"]
@ -233,6 +237,7 @@ def test_plugin(
"format": "message", "format": "message",
"msgNo": 1, "msgNo": 1,
} }
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
for x in range(number): for x in range(number):
reply = pm.run(packet) reply = pm.run(packet)

View File

@ -296,14 +296,14 @@ class APRSDFlask(flask_classful.FlaskView):
) )
wl = packets.WatchList() wl = packets.WatchList()
if wl.is_enabled(): if wl.is_enabled():
watch_count = len(wl.callsigns) watch_count = len(wl)
watch_age = wl.max_delta() watch_age = wl.max_delta()
else: else:
watch_count = 0 watch_count = 0
watch_age = 0 watch_age = 0
sl = packets.SeenList() sl = packets.SeenList()
seen_count = len(sl.callsigns) seen_count = len(sl)
pm = plugin.PluginManager() pm = plugin.PluginManager()
plugins = pm.get_plugins() plugins = pm.get_plugins()
@ -408,14 +408,14 @@ class APRSDFlask(flask_classful.FlaskView):
# Convert the watch_list entries to age # Convert the watch_list entries to age
wl = packets.WatchList() wl = packets.WatchList()
new_list = {} new_list = {}
for call in wl.callsigns: for call in wl.get_all():
# call_date = datetime.datetime.strptime( # call_date = datetime.datetime.strptime(
# str(wl.last_seen(call)), # str(wl.last_seen(call)),
# "%Y-%m-%d %H:%M:%S.%f", # "%Y-%m-%d %H:%M:%S.%f",
# ) # )
new_list[call] = { new_list[call] = {
"last": wl.age(call), "last": wl.age(call),
"packets": wl.callsigns[call]["packets"].get(), "packets": wl.get(call)["packets"].get(),
} }
stats_dict["aprsd"]["watch_list"] = new_list stats_dict["aprsd"]["watch_list"] = new_list

View File

@ -148,6 +148,8 @@ def signal_handler(sig, frame):
time.sleep(1.5) time.sleep(1.5)
tracker = messaging.MsgTrack() tracker = messaging.MsgTrack()
tracker.save() tracker.save()
packets.WatchList().save()
packets.SeenList().save()
LOG.info(stats.APRSDStats()) LOG.info(stats.APRSDStats())
# signal.signal(signal.SIGTERM, sys.exit(0)) # signal.signal(signal.SIGTERM, sys.exit(0))
# sys.exit(0) # sys.exit(0)
@ -481,6 +483,8 @@ def server(
# Try and load saved MsgTrack list # Try and load saved MsgTrack list
LOG.debug("Loading saved MsgTrack object.") LOG.debug("Loading saved MsgTrack object.")
messaging.MsgTrack().load() messaging.MsgTrack().load()
packets.WatchList().load()
packets.SeenList().load()
packets.PacketList(config=config) packets.PacketList(config=config)
packets.WatchList(config=config) packets.WatchList(config=config)

83
aprsd/objectstore.py Normal file
View File

@ -0,0 +1,83 @@
import logging
import os
import pathlib
import pickle
from aprsd import config as aprsd_config
LOG = logging.getLogger("APRSD")
class ObjectStoreMixin:
"""Class 'MIXIN' intended to save/load object data.
The asumption of how this mixin is used:
The using class has to have a:
* data in self.data as a dictionary
* a self.lock thread lock
* Class must specify self.save_file as the location.
When APRSD quits, it calls save()
When APRSD Starts, it calls load()
aprsd server -f (flush) will wipe all saved objects.
"""
def __len__(self):
return len(self.data)
def get_all(self):
with self.lock:
return self.data
def get(self, id):
with self.lock:
return self.data[id]
def _save_filename(self):
return "{}/{}.p".format(
aprsd_config.DEFAULT_CONFIG_DIR,
self.__class__.__name__.lower(),
)
def _dump(self):
dump = {}
with self.lock:
for key in self.data.keys():
dump[key] = self.data[key]
LOG.debug(f"{self.__class__.__name__}:: DUMP")
LOG.debug(dump)
return dump
def save(self):
"""Save any queued to disk?"""
if len(self) > 0:
LOG.info(f"{self.__class__.__name__}::Saving {len(self)} entries to disk")
pickle.dump(self._dump(), open(self._save_filename(), "wb+"))
else:
LOG.debug(
"{} Nothing to save, flushing old save file '{}'".format(
self.__class__.__name__,
self._save_filename(),
),
)
self.flush()
def load(self):
if os.path.exists(self._save_filename()):
raw = pickle.load(open(self._save_filename(), "rb"))
if raw:
self.data = raw
LOG.debug(f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.")
LOG.debug(f"{self.data}")
def flush(self):
"""Nuke the old pickle file that stored the old results from last aprsd run."""
if os.path.exists(self._save_filename()):
pathlib.Path(self._save_filename()).unlink()
with self.lock:
self.data = {}

View File

@ -3,7 +3,7 @@ import logging
import threading import threading
import time import time
from aprsd import utils from aprsd import objectstore, utils
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -65,18 +65,18 @@ class PacketList:
return self.total_tx return self.total_tx
class WatchList: class WatchList(objectstore.ObjectStoreMixin):
"""Global watch list and info for callsigns.""" """Global watch list and info for callsigns."""
_instance = None _instance = None
callsigns = {} data = {}
config = None config = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock() cls._instance.lock = threading.Lock()
cls.callsigns = {} cls.data = {}
return cls._instance return cls._instance
def __init__(self, config=None): def __init__(self, config=None):
@ -91,7 +91,7 @@ class WatchList:
# a beacon from a callsign or some other mechanism to find # a beacon from a callsign or some other mechanism to find
# last time a message was seen by aprs-is. For now this # last time a message was seen by aprs-is. For now this
# is all we can do. # is all we can do.
self.callsigns[call] = { self.data[call] = {
"last": datetime.datetime.now(), "last": datetime.datetime.now(),
"packets": utils.RingBuffer( "packets": utils.RingBuffer(
ring_size, ring_size,
@ -105,17 +105,18 @@ class WatchList:
return False return False
def callsign_in_watchlist(self, callsign): def callsign_in_watchlist(self, callsign):
return callsign in self.callsigns return callsign in self.data
def update_seen(self, packet): def update_seen(self, packet):
with self.lock:
callsign = packet["from"] callsign = packet["from"]
if self.callsign_in_watchlist(callsign): if self.callsign_in_watchlist(callsign):
self.callsigns[callsign]["last"] = datetime.datetime.now() self.data[callsign]["last"] = datetime.datetime.now()
self.callsigns[callsign]["packets"].append(packet) self.data[callsign]["packets"].append(packet)
def last_seen(self, callsign): def last_seen(self, callsign):
if self.callsign_in_watchlist(callsign): if self.callsign_in_watchlist(callsign):
return self.callsigns[callsign]["last"] return self.data[callsign]["last"]
def age(self, callsign): def age(self, callsign):
now = datetime.datetime.now() now = datetime.datetime.now()
@ -150,18 +151,18 @@ class WatchList:
return False return False
class SeenList: class SeenList(objectstore.ObjectStoreMixin):
"""Global callsign seen list.""" """Global callsign seen list."""
_instance = None _instance = None
callsigns = {} data = {}
config = None config = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.lock = threading.Lock() cls._instance.lock = threading.Lock()
cls.callsigns = {} cls.data = {}
return cls._instance return cls._instance
def update_seen(self, packet): def update_seen(self, packet):
@ -170,13 +171,13 @@ class SeenList:
callsign = packet["fromcall"] callsign = packet["fromcall"]
elif "from" in packet: elif "from" in packet:
callsign = packet["from"] callsign = packet["from"]
if callsign not in self.callsigns: if callsign not in self.data:
self.callsigns[callsign] = { self.data[callsign] = {
"last": None, "last": None,
"count": 0, "count": 0,
} }
self.callsigns[callsign]["last"] = str(datetime.datetime.now()) self.data[callsign]["last"] = str(datetime.datetime.now())
self.callsigns[callsign]["count"] += 1 self.data[callsign]["count"] += 1
def get_packet_type(packet): def get_packet_type(packet):

View File

@ -211,8 +211,8 @@ class APRSDStats:
"memory_current_str": utils.human_size(self.memory), "memory_current_str": utils.human_size(self.memory),
"memory_peak": self.memory_peak, "memory_peak": self.memory_peak,
"memory_peak_str": utils.human_size(self.memory_peak), "memory_peak_str": utils.human_size(self.memory_peak),
"watch_list": wl.callsigns, "watch_list": wl.get_all(),
"seen_list": sl.callsigns, "seen_list": sl.get_all(),
}, },
"aprs-is": { "aprs-is": {
"server": self.aprsis_server, "server": self.aprsis_server,