diff --git a/owrx/__main__.py b/owrx/__main__.py index 948781f..14264ed 100644 --- a/owrx/__main__.py +++ b/owrx/__main__.py @@ -1,6 +1,6 @@ from http.server import HTTPServer from owrx.http import RequestHandler -from owrx.config import PropertyManager, Config +from owrx.config import Config from owrx.feature import FeatureDetector from owrx.sdr import SdrService from socketserver import ThreadingMixIn @@ -36,7 +36,7 @@ Support and info: https://groups.io/g/openwebrx logger.info("OpenWebRX version {0} starting up...".format(openwebrx_version)) - pm = PropertyManager.getSharedInstance().loadConfig() + pm = Config.get() configErrors = Config.validateConfig() if configErrors: diff --git a/owrx/client.py b/owrx/client.py index 4fda3d4..6241a01 100644 --- a/owrx/client.py +++ b/owrx/client.py @@ -1,4 +1,4 @@ -from owrx.config import PropertyManager +from owrx.config import Config from owrx.metrics import Metrics, DirectMetric import threading @@ -33,7 +33,7 @@ class ClientRegistry(object): c.write_clients(n) def addClient(self, client): - pm = PropertyManager.getSharedInstance() + pm = Config.get() if len(self.clients) >= pm["max_clients"]: raise TooManyClientsException() self.clients.append(client) diff --git a/owrx/config.py b/owrx/config.py index b027f48..2aff716 100644 --- a/owrx/config.py +++ b/owrx/config.py @@ -1,155 +1,15 @@ +from owrx.property import PropertyManager import importlib.util -import logging import os +import logging logger = logging.getLogger(__name__) -class Subscription(object): - def __init__(self, subscriptee, subscriber): - self.subscriptee = subscriptee - self.subscriber = subscriber - - def call(self, *args, **kwargs): - self.subscriber(*args, **kwargs) - - def cancel(self): - self.subscriptee.unwire(self) - - -class Property(object): - def __init__(self, value=None): - self.value = value - self.subscribers = [] - - def getValue(self): - return self.value - - def setValue(self, value): - if self.value == value: - return self - self.value = value - for c in self.subscribers: - try: - c.call(self.value) - except Exception as e: - logger.exception(e) - return self - - def wire(self, callback): - sub = Subscription(self, callback) - self.subscribers.append(sub) - if not self.value is None: - sub.call(self.value) - return sub - - def unwire(self, sub): - try: - self.subscribers.remove(sub) - except ValueError: - # happens when already removed before - pass - return self - - class ConfigNotFoundException(Exception): pass -class PropertyManager(object): - sharedInstance = None - - @staticmethod - def getSharedInstance(): - if PropertyManager.sharedInstance is None: - PropertyManager.sharedInstance = PropertyManager() - return PropertyManager.sharedInstance - - def collect(self, *props): - return PropertyManager( - {name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props} - ) - - def __init__(self, properties=None): - self.properties = {} - self.subscribers = [] - if properties is not None: - for (name, prop) in properties.items(): - self.add(name, prop) - - def add(self, name, prop): - self.properties[name] = prop - - def fireCallbacks(value): - for c in self.subscribers: - try: - c.call(name, value) - except Exception as e: - logger.exception(e) - - prop.wire(fireCallbacks) - return self - - def __contains__(self, name): - return self.hasProperty(name) - - def __getitem__(self, name): - return self.getPropertyValue(name) - - def __setitem__(self, name, value): - if not self.hasProperty(name): - self.add(name, Property()) - self.getProperty(name).setValue(value) - - def __dict__(self): - return {k: v.getValue() for k, v in self.properties.items()} - - def hasProperty(self, name): - return name in self.properties - - def getProperty(self, name): - if not self.hasProperty(name): - self.add(name, Property()) - return self.properties[name] - - def getPropertyValue(self, name): - return self.getProperty(name).getValue() - - def wire(self, callback): - sub = Subscription(self, callback) - self.subscribers.append(sub) - return sub - - def unwire(self, sub): - try: - self.subscribers.remove(sub) - except ValueError: - # happens when already removed before - pass - return self - - def defaults(self, other_pm): - for (key, p) in self.properties.items(): - if p.getValue() is None: - p.setValue(other_pm[key]) - return self - - def loadConfig(self): - for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]: - try: - spec = importlib.util.spec_from_file_location("config_webrx", file) - cfg = importlib.util.module_from_spec(spec) - spec.loader.exec_module(cfg) - for name, value in cfg.__dict__.items(): - if name.startswith("__"): - continue - self[name] = value - return self - except FileNotFoundError: - pass - raise ConfigNotFoundException("no usable config found! please make sure you have a valid configuration file!") - - class ConfigError(object): def __init__(self, key, message): self.key = key @@ -160,9 +20,34 @@ class ConfigError(object): class Config: + sharedConfig = None + + @staticmethod + def _loadConfig(): + pm = PropertyManager() + for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]: + try: + spec = importlib.util.spec_from_file_location("config_webrx", file) + cfg = importlib.util.module_from_spec(spec) + spec.loader.exec_module(cfg) + for name, value in cfg.__dict__.items(): + if name.startswith("__"): + continue + pm[name] = value + return pm + except FileNotFoundError: + pass + raise ConfigNotFoundException("no usable config found! please make sure you have a valid configuration file!") + + @staticmethod + def get(): + if Config.sharedConfig is None: + Config.sharedConfig = Config._loadConfig() + return Config.sharedConfig + @staticmethod def validateConfig(): - pm = PropertyManager.getSharedInstance() + pm = Config.get() errors = [ Config.checkTempDirectory(pm) ] @@ -172,7 +57,7 @@ class Config: @staticmethod def checkTempDirectory(pm: PropertyManager): key = "temporary_directory" - if not key in pm or pm[key] is None: + if key not in pm or pm[key] is None: return ConfigError(key, "temporary directory is not set") if not os.path.exists(pm[key]): return ConfigError(key, "temporary directory doesn't exist") diff --git a/owrx/connection.py b/owrx/connection.py index 6884f69..1311102 100644 --- a/owrx/connection.py +++ b/owrx/connection.py @@ -1,4 +1,4 @@ -from owrx.config import PropertyManager +from owrx.config import Config from owrx.dsp import DspManager from owrx.cpu import CpuUsageThread from owrx.sdr import SdrService @@ -93,7 +93,7 @@ class OpenWebRxReceiverClient(Client): self.close() raise - pm = PropertyManager.getSharedInstance() + pm = Config.get() self.setSdr() @@ -198,7 +198,7 @@ class OpenWebRxReceiverClient(Client): configProps = ( self.sdr.getProps() .collect(*OpenWebRxReceiverClient.config_keys) - .defaults(PropertyManager.getSharedInstance()) + .defaults(Config.get()) ) def sendConfig(key, value): @@ -247,15 +247,16 @@ class OpenWebRxReceiverClient(Client): self.sdr.removeSpectrumClient(self) def setParams(self, params): + config = Config.get() # allow direct configuration only if enabled in the config - keys = PropertyManager.getSharedInstance()["configurable_keys"] + keys = config["configurable_keys"] if not keys: return # only the keys in the protected property manager can be overridden from the web protected = ( self.sdr.getProps() .collect(*keys) - .defaults(PropertyManager.getSharedInstance()) + .defaults(config) ) for key, value in params.items(): protected[key] = value @@ -333,7 +334,7 @@ class MapConnection(Client): def __init__(self, conn): super().__init__(conn) - pm = PropertyManager.getSharedInstance() + pm = Config.get() self.write_config(pm.collect("google_maps_api_key", "receiver_gps", "map_position_retention_time").__dict__()) Map.getSharedInstance().addClient(self) diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py index 96782f5..9a8ab24 100644 --- a/owrx/controllers/assets.py +++ b/owrx/controllers/assets.py @@ -1,5 +1,5 @@ from . import Controller -from owrx.config import PropertyManager +from owrx.config import Config from datetime import datetime import mimetypes import os @@ -47,7 +47,7 @@ class OwrxAssetsController(AssetsController): class AprsSymbolsController(AssetsController): def __init__(self, handler, request, options): - pm = PropertyManager.getSharedInstance() + pm = Config.get() path = pm["aprs_symbols_path"] if not path.endswith("/"): path += "/" diff --git a/owrx/controllers/status.py b/owrx/controllers/status.py index bb513ee..12be28f 100644 --- a/owrx/controllers/status.py +++ b/owrx/controllers/status.py @@ -2,14 +2,14 @@ from . import Controller from owrx.client import ClientRegistry from owrx.version import openwebrx_version from owrx.sdr import SdrService -from owrx.config import PropertyManager +from owrx.config import Config import os import json class StatusController(Controller): def indexAction(self): - pm = PropertyManager.getSharedInstance() + pm = Config.get() # TODO keys that have been left out since they are no longer simple strings: sdr_hw, bands, antenna vars = { "status": "active", @@ -42,7 +42,7 @@ class StatusController(Controller): return stats def jsonAction(self): - pm = PropertyManager.getSharedInstance() + pm = Config.get() gps = pm["receiver_gps"] status = { diff --git a/owrx/dsp.py b/owrx/dsp.py index f12d171..3f155c6 100644 --- a/owrx/dsp.py +++ b/owrx/dsp.py @@ -1,4 +1,4 @@ -from owrx.config import PropertyManager +from owrx.config import Config from owrx.meta import MetaParser from owrx.wsjt import WsjtParser from owrx.aprs import AprsParser @@ -39,7 +39,7 @@ class DspManager(csdr.output): "temporary_directory", "center_freq", ) - .defaults(PropertyManager.getSharedInstance()) + .defaults(Config.get()) ) self.dsp = csdr.dsp(self) diff --git a/owrx/feature.py b/owrx/feature.py index f873847..ffd266b 100644 --- a/owrx/feature.py +++ b/owrx/feature.py @@ -4,7 +4,7 @@ from operator import and_, or_ import re from distutils.version import LooseVersion import inspect -from owrx.config import PropertyManager +from owrx.config import Config import shlex import logging @@ -95,7 +95,7 @@ class FeatureDetector(object): return inspect.getdoc(self._get_requirement_method(requirement)) def command_is_runnable(self, command): - tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"] + tmp_dir = Config.get()["temporary_directory"] cmd = shlex.split(command) try: process = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=tmp_dir) diff --git a/owrx/fft.py b/owrx/fft.py index b1c61de..93cceef 100644 --- a/owrx/fft.py +++ b/owrx/fft.py @@ -1,4 +1,4 @@ -from owrx.config import PropertyManager +from owrx.config import Config from csdr import csdr import threading from owrx.source import SdrSource @@ -23,7 +23,7 @@ class SpectrumThread(csdr.output): "csdr_print_bufsizes", "csdr_through", "temporary_directory", - ).defaults(PropertyManager.getSharedInstance()) + ).defaults(Config.get()) self.dsp = dsp = csdr.dsp(self) dsp.nc_port = self.sdrSource.getPort() diff --git a/owrx/kiss.py b/owrx/kiss.py index e24284f..5da2907 100644 --- a/owrx/kiss.py +++ b/owrx/kiss.py @@ -2,7 +2,7 @@ import socket import time import logging import random -from owrx.config import PropertyManager +from owrx.config import Config logger = logging.getLogger(__name__) @@ -14,7 +14,7 @@ TFESC = 0xDD class DirewolfConfig(object): def getConfig(self, port, is_service): - pm = PropertyManager.getSharedInstance() + pm = Config.get() config = """ ACHANNELS 1 diff --git a/owrx/map.py b/owrx/map.py index f7a0d5d..f70d1cc 100644 --- a/owrx/map.py +++ b/owrx/map.py @@ -1,7 +1,8 @@ from datetime import datetime, timedelta -import threading, time -from owrx.config import PropertyManager +from owrx.config import Config from owrx.bands import Band +import threading +import time import sys import logging @@ -105,7 +106,7 @@ class Map(object): # TODO broadcast removal to clients def removeOldPositions(self): - pm = PropertyManager.getSharedInstance() + pm = Config.get() retention = timedelta(seconds=pm["map_position_retention_time"]) cutoff = datetime.now() - retention diff --git a/owrx/meta.py b/owrx/meta.py index d69127d..c7e9c1d 100644 --- a/owrx/meta.py +++ b/owrx/meta.py @@ -1,4 +1,4 @@ -from owrx.config import PropertyManager +from owrx.config import Config from urllib import request import json from datetime import datetime, timedelta @@ -54,7 +54,7 @@ class DmrMetaEnricher(object): del self.threads[id] def enrich(self, meta): - if not PropertyManager.getSharedInstance()["digital_voice_dmr_id_lookup"]: + if not Config.get()["digital_voice_dmr_id_lookup"]: return None if not "source" in meta: return None diff --git a/owrx/property/__init__.py b/owrx/property/__init__.py new file mode 100644 index 0000000..7ef892a --- /dev/null +++ b/owrx/property/__init__.py @@ -0,0 +1,121 @@ +import logging + +logger = logging.getLogger(__name__) + + +class Subscription(object): + def __init__(self, subscriptee, subscriber): + self.subscriptee = subscriptee + self.subscriber = subscriber + + def call(self, *args, **kwargs): + self.subscriber(*args, **kwargs) + + def cancel(self): + self.subscriptee.unwire(self) + + +class Property(object): + def __init__(self, value=None): + self.value = value + self.subscribers = [] + + def getValue(self): + return self.value + + def setValue(self, value): + if self.value == value: + return self + self.value = value + for c in self.subscribers: + try: + c.call(self.value) + except Exception as e: + logger.exception(e) + return self + + def wire(self, callback): + sub = Subscription(self, callback) + self.subscribers.append(sub) + if self.value is not None: + sub.call(self.value) + return sub + + def unwire(self, sub): + try: + self.subscribers.remove(sub) + except ValueError: + # happens when already removed before + pass + return self + + +class PropertyManager(object): + def collect(self, *props): + return PropertyManager( + {name: self.getProperty(name) if self.hasProperty(name) else Property() for name in props} + ) + + def __init__(self, properties=None): + self.properties = {} + self.subscribers = [] + if properties is not None: + for (name, prop) in properties.items(): + self.add(name, prop) + + def add(self, name, prop): + self.properties[name] = prop + + def fireCallbacks(value): + for c in self.subscribers: + try: + c.call(name, value) + except Exception as e: + logger.exception(e) + + prop.wire(fireCallbacks) + return self + + def __contains__(self, name): + return self.hasProperty(name) + + def __getitem__(self, name): + return self.getPropertyValue(name) + + def __setitem__(self, name, value): + if not self.hasProperty(name): + self.add(name, Property()) + self.getProperty(name).setValue(value) + + def __dict__(self): + return {k: v.getValue() for k, v in self.properties.items()} + + def hasProperty(self, name): + return name in self.properties + + def getProperty(self, name): + if not self.hasProperty(name): + self.add(name, Property()) + return self.properties[name] + + def getPropertyValue(self, name): + return self.getProperty(name).getValue() + + def wire(self, callback): + sub = Subscription(self, callback) + self.subscribers.append(sub) + return sub + + def unwire(self, sub): + try: + self.subscribers.remove(sub) + except ValueError: + # happens when already removed before + pass + return self + + def defaults(self, other_pm): + for (key, p) in self.properties.items(): + if p.getValue() is None: + p.setValue(other_pm[key]) + return self diff --git a/owrx/pskreporter.py b/owrx/pskreporter.py index da580f0..22a813f 100644 --- a/owrx/pskreporter.py +++ b/owrx/pskreporter.py @@ -5,7 +5,7 @@ import random import socket from functools import reduce from operator import and_ -from owrx.config import PropertyManager +from owrx.config import Config from owrx.version import openwebrx_version from owrx.locator import Locator from owrx.metrics import Metrics, CounterMetric @@ -36,7 +36,7 @@ class PskReporter(object): def getSharedInstance(): with PskReporter.creationLock: if PskReporter.sharedInstance is None: - if PropertyManager.getSharedInstance()["pskreporter_enabled"]: + if Config.get()["pskreporter_enabled"]: PskReporter.sharedInstance = PskReporter() else: PskReporter.sharedInstance = PskReporterDummy() @@ -181,7 +181,7 @@ class Uploader(object): ) def getReceiverInformation(self): - pm = PropertyManager.getSharedInstance() + pm = Config.get() callsign = pm["pskreporter_callsign"] locator = Locator.fromCoordinates(pm["receiver_gps"]) decodingSoftware = "OpenWebRX " + openwebrx_version diff --git a/owrx/sdr.py b/owrx/sdr.py index 52a2bbf..19c34c3 100644 --- a/owrx/sdr.py +++ b/owrx/sdr.py @@ -1,4 +1,5 @@ -from owrx.config import PropertyManager +from owrx.config import Config +from owrx.property import PropertyManager from owrx.feature import FeatureDetector, UnknownFeatureException import logging @@ -14,7 +15,7 @@ class SdrService(object): @staticmethod def loadProps(): if SdrService.sdrProps is None: - pm = PropertyManager.getSharedInstance() + pm = Config.get() featureDetector = FeatureDetector() def loadIntoPropertyManager(dict: dict): diff --git a/owrx/sdrhu.py b/owrx/sdrhu.py index f9b5207..939d352 100644 --- a/owrx/sdrhu.py +++ b/owrx/sdrhu.py @@ -1,6 +1,6 @@ import threading import time -from owrx.config import PropertyManager +from owrx.config import Config from urllib import request, parse import logging @@ -14,7 +14,7 @@ class SdrHuUpdater(threading.Thread): super().__init__(daemon=True) def update(self): - pm = PropertyManager.getSharedInstance().collect("server_hostname", "web_port", "sdrhu_key") + pm = Config.get().collect("server_hostname", "web_port", "sdrhu_key") data = parse.urlencode({ "url": "http://{server_hostname}:{web_port}".format(**pm.__dict__()), "apikey": pm["sdrhu_key"] diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py index 3e1a337..efda39d 100644 --- a/owrx/service/__init__.py +++ b/owrx/service/__init__.py @@ -5,9 +5,10 @@ from owrx.bands import Bandplan from csdr.csdr import dsp, output from owrx.wsjt import WsjtParser from owrx.aprs import AprsParser -from owrx.config import PropertyManager +from owrx.config import Config from owrx.source.resampler import Resampler from owrx.feature import FeatureDetector +from owrx.property import PropertyManager from abc import ABCMeta, abstractmethod from .schedule import ServiceScheduler @@ -96,7 +97,7 @@ class ServiceHandler(object): # this looks overly complicated... but i'd like modes with no requirements to be always available without # being listed in the hash above unavailable = [mode for mode, req in requirements.items() if not fd.is_available(req)] - configured = PropertyManager.getSharedInstance()["services_decoders"] + configured = Config.get()["services_decoders"] available = [mode for mode in configured if mode not in unavailable] return mode in available @@ -260,7 +261,7 @@ class Services(object): @staticmethod def start(): - if not PropertyManager.getSharedInstance()["services_enabled"]: + if not Config.get()["services_enabled"]: return for source in SdrService.getSources().values(): props = source.getProps() diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py index 509a26f..ade3976 100644 --- a/owrx/service/schedule.py +++ b/owrx/service/schedule.py @@ -1,6 +1,6 @@ from datetime import datetime, timezone, timedelta from owrx.source import SdrSource -from owrx.config import PropertyManager +from owrx.config import Config import threading import math from abc import ABC, ABCMeta, abstractmethod @@ -134,7 +134,7 @@ class DaylightSchedule(TimerangeSchedule): self.schedule = scheduleDict def getSunTimes(self, date): - pm = PropertyManager.getSharedInstance() + pm = Config.get() lat, lng = pm["receiver_gps"] degtorad = math.pi / 180 radtodeg = 180 / math.pi diff --git a/owrx/source/__init__.py b/owrx/source/__init__.py index cb050f2..abc35d2 100644 --- a/owrx/source/__init__.py +++ b/owrx/source/__init__.py @@ -1,4 +1,4 @@ -from owrx.config import PropertyManager +from owrx.config import Config import threading import subprocess import os @@ -35,7 +35,7 @@ class SdrSource(ABC): self.props = props self.profile_id = None self.activateProfile() - self.rtlProps = self.props.collect(*self.getEventNames()).defaults(PropertyManager.getSharedInstance()) + self.rtlProps = self.props.collect(*self.getEventNames()).defaults(Config.get()) self.wireEvents() self.commandMapper = None diff --git a/owrx/wsjt.py b/owrx/wsjt.py index 4818201..08bb911 100644 --- a/owrx/wsjt.py +++ b/owrx/wsjt.py @@ -7,8 +7,7 @@ from multiprocessing.connection import Pipe from owrx.map import Map, LocatorLocation import re from queue import Queue, Full -from owrx.config import PropertyManager -from owrx.bands import Bandplan +from owrx.config import Config from owrx.metrics import Metrics, CounterMetric, DirectMetric from owrx.pskreporter import PskReporter from owrx.parser import Parser @@ -45,7 +44,7 @@ class WsjtQueue(Queue): def getSharedInstance(): with WsjtQueue.creationLock: if WsjtQueue.sharedInstance is None: - pm = PropertyManager.getSharedInstance() + pm = Config.get() WsjtQueue.sharedInstance = WsjtQueue(maxsize=pm["wsjt_queue_length"], workers=pm["wsjt_queue_workers"]) return WsjtQueue.sharedInstance @@ -89,7 +88,7 @@ class WsjtQueue(Queue): class WsjtChopper(threading.Thread): def __init__(self, source): self.source = source - self.tmp_dir = PropertyManager.getSharedInstance()["temporary_directory"] + self.tmp_dir = Config.get()["temporary_directory"] (self.wavefilename, self.wavefile) = self.getWaveFile() self.switchingLock = threading.Lock() self.timer = None @@ -193,7 +192,7 @@ class WsjtChopper(threading.Thread): return None def decoding_depth(self, mode): - pm = PropertyManager.getSharedInstance() + pm = Config.get() # mode-specific setting? if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]: return pm["wsjt_decoding_depths"][mode] diff --git a/sdrhu.py b/sdrhu.py index 53c5c6d..9678a9b 100755 --- a/sdrhu.py +++ b/sdrhu.py @@ -22,14 +22,14 @@ """ from owrx.sdrhu import SdrHuUpdater -from owrx.config import PropertyManager +from owrx.config import Config import logging logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") logger = logging.getLogger(__name__) if __name__ == "__main__": - pm = PropertyManager.getSharedInstance().loadConfig() + pm = Config.get() if "sdrhu_public_listing" not in pm or not pm["sdrhu_public_listing"]: logger.error('Public listing on sdr.hu is not activated. Please check "sdrhu_public_listing" in your config.') diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/property/__init__.py b/test/property/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/property/test_property.py b/test/property/test_property.py new file mode 100644 index 0000000..1529f8c --- /dev/null +++ b/test/property/test_property.py @@ -0,0 +1,8 @@ +import unittest +from owrx.property import Property + + +class PropertyTest(unittest.TestCase): + def testSimple(self): + prop = Property("testvalue") + self.assertEqual(prop.getValue(), "testvalue")