import logging import traceback import types from lib.helpers import timing from PyQt5.QtCore import (QMutex, QObject, QSemaphore, Qt, QThread, QTimer, pyqtSignal) class Component(QObject): """emitted with data from the _get method""" out = pyqtSignal(object) _reconfigure = pyqtSignal() _pause = pyqtSignal() _resume = pyqtSignal() _set_sources = pyqtSignal(object) _set_requestors = pyqtSignal(object) _set_period = pyqtSignal(object) reconfigurators = set() reconfigurators_lock = QMutex() @staticmethod def reconfig_on_error(f): def wrapper(*arg, **kwargs): """arg[0] is expected to be self fot the wrapped f method and wrappedmethods are expected to run in the component thread""" self = arg[0] Component.reconfigurators_lock.lock() if not self._started or self in Component.reconfigurators: # do not retry on first configuration # avoid reconfiguration recursion loops when calling decorated methods during reconfiguration # self.log.exception(f"self: {self}, reconfigurators: {Component.reconfigurators}, reconfiguration loop") Component.reconfigurators_lock.unlock() try: return f(*arg, **kwargs) except Exception: self.log.exception(traceback.format_exc()) return None Component.reconfigurators.add(self) Component.reconfigurators_lock.unlock() t_limit = 3 t = 0 ok = False while t < t_limit and not ok: if not self._ready: try: self._do_reconfigure() except Exception: self.log.exception(traceback.format_exc()) try: ret = f(*arg, **kwargs) ok = True except Exception: self.log.exception(traceback.format_exc()) # mark component asnot ready on fail self._ready = False if not ok: t += 1 QThread.msleep(1000) if ok: Component.reconfigurators_lock.lock() Component.reconfigurators.remove(self) Component.reconfigurators_lock.unlock() return ret raise RuntimeError(f"retried to run {f} and reconfigure {self} with no success for {t_limit} times. giving up.") return wrapper def __init__( self, config=None, name=None, period=None, lazy=True, paused=False, threaded=True, ): """ parameters: config: value for self.config, should be instance of lib.helpers.config_reader.ConfigReader name: value for self.name, should be used to retrive component configuration section (self.config[self.name]) period: period in seconds for periodic calls to _get, set to None to disable lazy: whether or not skip periodic _get calls if falling behind paused: whether or not periodic calls toget are paused threaded: set this to tell the component if it should be thread synchronized or not(shoul be true if calls to methods are done from threads different from the component's one) """ super().__init__() self.config = config self.log=logging.getLogger(f"{self.__class__.__name__}") self.name = name if name is not None else str(id(self)) self._threaded = threaded self._period = period self._single_shot = lazy self._paused = paused self._started = False self._running = False self.sources = None self.requestors = None if self._threaded: self._lock = QSemaphore(1) self._lock.acquire(max(self._lock.available(), 1)) self._timer = None self.log = logging.getLogger(f"{self.__class__.__name__} ({self.name})") if not self._threaded: self.start() @property def ready(self): """returns True if the component is ready""" if self._threaded: self._lock.acquire(max(self._lock.available(), 1)) ready = self._ready if self._threaded: self._lock.release() return ready def config_changed(self): """ this method should be overridden when inheriting from the Component class and should contain all the initialization code that needs to access self.config so that the component will reinitialize if configuration changes this method will be called on start and when self.config (ConfigReader) emits the updated signal """ pass def start(self): """ this method is automatically called if threaded is set to False at object creation otherwise if the component is in a thread this method should be used like this: component = ComponentSubclass(threaded=True) thread = QThread() thread.setTerminationEnabled(True) component.moveToThread(thread) thread.started.connect(component.start) thread.start() component.wait_completion() # this is optional and will wait untill the component has finished started """ self._reconfigure.connect(self._do_reconfigure) self._pause.connect(self._do_pause) self._resume.connect(self._do_resume) self._set_sources.connect(self._do_set_sources) self._set_requestors.connect(self._do_set_requestors) self._set_period.connect(self._do_set_period) if self.config is not None: if self.config.updated is not None: self.config.updated.connect(self.reconfigure) self._do_reconfigure() self._init_periodic() self._started = True if not self._paused: self._do_resume() elif self._threaded: self._lock.release() self.log.info("started") @property def started(self): """returns True if the component has been started""" if self._threaded: self._lock.acquire(max(self._lock.available(), 1)) started = self._started if self._threaded: self._lock.release() return started @property def running(self): """returns True if the periodic calls to _get are not paused""" if self._threaded: self._lock.acquire(max(self._lock.available(), 1)) running = self._running if self._threaded: self._lock.release() return running def wait_completion(self, timeout=5): """ waits untill the requested action has been completed by the component this will return immediately if threaded=False was passed at component initialization """ if self._threaded: timeout = round(timeout * 1000) if self._lock.tryAcquire(max(self._lock.available(), 1), timeout): self._lock.release() else: self._lock.release() raise RuntimeError(f"{self.name} was not ready before timeout of {timeout}ms") def reconfigure(self): """will pause periodic calls to _get and sources triggers""" if self._threaded: self._lock.acquire(max(self._lock.available(), 1)) if self._threaded: self._reconfigure.emit() self.wait_completion() else: self._do_reconfigure() def pause(self): """will pause periodic calls to _get and sources or requestors triggers""" if self._threaded: self._lock.acquire(max(self._lock.available(), 1)) if self._running is False: if self._threaded: self._lock.release() return if self._threaded: self._pause.emit() self.wait_completion() else: self._do_pause() def resume(self): """will resume periodic calls to _get and sources or requestors triggers""" if self._threaded: self._lock.acquire(max(self._lock.available(), 1)) if self._running is True: if self._threaded: self._lock.release() return if self._threaded: self._resume.emit() self.wait_completion() else: self._do_resume() def set_sources(self, sources=None): # """ connect the given sources to trigger a call to _get, the sources parameter should be: a dict of signals ({"": , ...}) signals might contain one optional argument that will be passed as data to _get or None to disconnect all sources """ if sources is None: sources = {} if self._threaded: self._lock.acquire(max(self._lock.available(), 1)) self._set_sources.emit(sources) self.wait_completion() else: self._do_set_sources(sources) def add_sources(self, sources=None, overwrite_conflicting_sources=False): # sources should be {"source_name": signal_to_connect, ...} """ add the given sources to the current ones, the sources parameter should be: a dict of signals ({"": , ...}) signals might contain one optional argument that will be passed as data to _get or None if no sources are to be added this method calls set_sources, this is semplest but not the most efficient approach """ if self.sources is None: self_sources = {} else: self_sources = self.sources if sources is None: sources = {} if not overwrite_conflicting_sources: conflicting_sources = { n: [self_sources[n], sources[n]] for n in self_sources.keys() & sources.keys() if self_sources[n] is not sources[n] } if len(conflicting_sources): raise AssertionError("\n\t" + "\n\t".join([f"source named {n!r}: {s[0]!r} will not be replaced with {s[1]!r}" for n, s in conflicting_sources.items()])) self.set_sources({**self_sources, **sources}) def remove_sources(self, sources=None): """ remove the given sources to the current ones by name, the sources parameter should be: an iterable of source names (["", ...]) or None if no sources are to be removed this method calls set_sources, this is semplest but not the most efficient approach """ if sources is None or self.sources is None: return sources = set(sources) self.set_sources({n: s for n, s in self.sources.items() if n not in sources}) def set_requestors(self, requestors=None): # """ connect the given requestors to trigger a call to _set, the requestors parameter should be: a dict of signals ({"": , ...}) signals might contain one optional argument that will be passed as data to set or None to disconnect all requestors """ if requestors is None: requestors = {} if self._threaded: self._lock.acquire(max(self._lock.available(), 1)) self._set_requestors.emit(requestors) self.wait_completion() else: self._do_set_requestors(requestors) def add_requestors(self, requestors=None, overwrite_conflicting_requestors=False): # requestors should be {"source_name": signal_to_connect, ...} """ add the given requestors to the current ones, the requestors parameter should be: a dict of signals ({"": , ...}) signals might contain one optional argument that will be passed as data to set or None if no requestors are to be added this method calls set_requestors, this is semplest but not the most efficient approach """ if self.requestors is None: self_requestors = {} else: self_requestors = self.requestors if requestors is None: requestors = {} if not overwrite_conflicting_requestors: conflicting_requestors = { n: [self_requestors[n], requestors[n]] for n in self_requestors.keys() & requestors.keys() if self_requestors[n] is not requestors[n] } if len(conflicting_requestors): raise AssertionError("\n\t" + "\n\t".join([f"requestor named {n!r}: {s[0]!r} will not be replaced with {s[1]!r}" for n, s in conflicting_requestors.items()])) self.set_requestors({**self_requestors, **requestors}) def remove_requestors(self, requestors=None): """ remove the given requestors to the current ones by name, the requestors parameter should be: an iterable of requestor names (["", ...]) or None if no requestors are to be removed this method calls set_requestors, this is semplest but not the most efficient approach """ if requestors is None or self.requestors is None: return requestors = set(requestors) self.set_requestors({n: s for n, s in self.requestors.items() if n not in requestors}) def _init_periodic(self): if self._period is not None: if self._timer is None: self._timer = QTimer() self._timer.setTimerType(Qt.PreciseTimer) self._timer.setSingleShot(self._single_shot) self._timer.setInterval(round(self._period * 1000)) self.log.debug(f"init periodic: period: {self._period}, single shot: {self._single_shot}") else: self.log.debug("no init periodic") def set_period(self, period=None, lazy=True): """will set the period for periodic calls to _get and whether or not those are lazy (see init parameters)""" if self._threaded: self._lock.acquire(max(self._lock.available(), 1)) self._set_period.emit({"period": period, "lazy": lazy}) self.wait_completion() else: self._do_set_period({"period": period, "lazy": lazy}) def _start_periodic(self): if self._timer is not None: self._timer.timeout.connect(self._get) self._timer.start() self.log.debug("started periodic") else: self.log.debug("no started periodic") def _stop_periodic(self): if self._timer is not None: self._timer.stop() try: self._timer.timeout.disconnect() except TypeError: pass self.log.debug("stopped periodic") else: self.log.debug("no stopped periodic") def _connect_sources(self): if self.sources is not None: for source in self.sources.values(): source.connect(self._get) self.log.debug(f"connected sources: {list(self.sources)}") else: self.log.debug("no connected sources") def _disconnect_sources(self): if self.sources is not None: for source in self.sources.values(): try: source.disconnect() except TypeError: pass self.log.debug(f"disconnected sources: {list(self.sources)}") else: self.log.debug("no disconnected sources") def _connect_requestors(self): if self.requestors is not None: for requestor in self.requestors.values(): requestor.connect(self._set) self.log.debug(f"connected requestors: {list(self.requestors)}") else: self.log.debug("no connected requestors") def _disconnect_requestors(self): if self.requestors is not None: for requestor in self.requestors.values(): try: requestor.disconnect() except TypeError: pass self.log.debug(f"disconnected requestors: {list(self.requestors)}") else: self.log.debug("no disconnected requestors") def _do_reconfigure(self): """this method must run in the component thread or must be invoked trough reconfigure""" self.log.info("reconfigure") self._ready = False try: self.config_changed() self._ready = True except Exception: self.log.exception(traceback.format_exc()) self._ready = False self.log.debug(f"config: {self.config}, ready: {self._ready}") if self._threaded: self._lock.release() def _do_resume(self): self._start_periodic() self._connect_sources() self._connect_requestors() self._running = True self.log.info("resumed") if self._threaded: self._lock.release() def _do_pause(self): self._stop_periodic() self._disconnect_sources() self._disconnect_requestors() self._running = False self.log.info("paused") if self._threaded: self._lock.release() def _do_set_sources(self, sources): if self._running: self._disconnect_sources() if sources is not None and not len(sources): sources = None self.sources = sources if self._running: self._connect_sources() self.log.info("set sources") if self._threaded: self._lock.release() def _do_set_requestors(self, requestors): if self._running: self._disconnect_requestors() if requestors is not None and not len(requestors): requestors = None self.requestors = requestors if self._running: self._connect_requestors() self.log.info("set requestors") if self._threaded: self._lock.release() def _do_set_period(self, spec): self._period = spec.get("period", None) self._single_shot = spec.get("lazy", True) self._init_periodic() self.log.info("set period") if self._threaded: self._lock.release() def _get(self, data=None, emit=True): """ this method should be overridden when inheriting from the Component class the overriding method should retrieve all the data and then call super()._get(data) this will emit the data in the proper format if the emit parameter is not set to False """ if emit: if data is None: data = [None] t = timing() got = [{ "time": t if type(d) is not dict or "time" not in d else d["time"], self.name: d, } for d in data] self.out.emit(got) self.log.debug(f"_get: {got}") else: self.log.debug("_get") if self._timer is not None and self._single_shot: self._timer.start() def _set(self, val): """ this method should be overridden when inheriting from the Component class the overriding method should set the requested val and then call super()._set(set_value) this will log the value that has been set """ self.log.debug(f"_set: {val}")