Mon, 05 Feb 2018 10:25:17 +0000
Added exeption protection decorators to callbacks.
If callbacks crash, there's rarely anything in the logs otherwise.
# # Borgend by Tuomo Valkonen, 2018 # # This file implements system wake/sleep detection for scheduling adjustments. # import platform import time import threading import weakref import datetime import logging import math from .exprotect import protect_noreturn logger=logging.getLogger(__name__) _dreamtime_monitor=None # # Support classes for dealing with different times # # Time snapshotting to helps to create stable comparisons of different # subclasses of Time. class Snapshot: def __init__(self): self._monotonic=None self._realtime=None self._dreamtime=None self._sleeping=None def monotonic(self): if self._monotonic is None: self._monotonic=time.monotonic() return self._monotonic def realtime(self): if self._realtime is None: self._realtime=time.time() return self._realtime def dreamtime_sleeping(self): if self._dreamtime is None: if _dreamtime_monitor: self._dreamtime, self._sleeping=_dreamtime_monitor.dreamtime_sleeping(snapshot=self) else: self._dreamtime, self._sleeping=self.monotonic(), False return self._dreamtime, self._sleeping def dreamtime(self): time, _=self.dreamtime_sleeping() return time def sleeping(self): _, sleeping=self.dreamtime_sleeping() return sleeping # The main Time class, for time advancing in various paces class Time: def __init__(self, when): self._value=when def _monotonic(self, snapshot): raise NotImplementedError def _realtime(self, snapshot): raise NotImplementedError @staticmethod def _now(snapshot): raise NotImplementedError @classmethod def now(cls): return cls(cls._now(Snapshot())) @classmethod def from_realtime(cls, realtime, snapshot=Snapshot()): return cls(realtime-snapshot.realtime()+cls._now(snapshot)) @classmethod def from_monotonic(cls, monotonic, snapshot=Snapshot()): return cls(monotonic-snapshot.monotonic()+cls._now(snapshot)) @classmethod def after(cls, seconds, snapshot=Snapshot()): return cls(cls._now(snapshot)+seconds) @classmethod def after_other(cls, other, seconds, snapshot=Snapshot()): if isinstance(other, cls): return cls(other._value+seconds) else: return cls.from_monotonic(other._monotonic(snapshot)+seconds, snapshot) def datetime(self, snapshot=Snapshot()): return datetime.datetime.fromtimestamp(self._realtime(snapshot)) def seconds_to(self, snapshot=Snapshot()): return self._value-self._now(snapshot) def isoformat(self): return self.datetime().isoformat() def realtime(self, snapshot=Snapshot()): return self._realtime(snapshot) def monotonic(self, snapshot=Snapshot()): return self._monotonic(snapshot) # Counted from the monotonic epoch, how far is this event? Usually should # equal self.monotonic(), but Dreamtime can be stopped by system sleep, # and will return ∞ (math.inf). def horizon(self, snapshot=Snapshot()): return self._monotonic(snapshot) def __compare(self, other, fn): if isinstance(other, self.__class__): return fn(self._value, other._value) else: snapshot=Snapshot() return fn(self._monotonic(snapshot), other._monotonic(snapshot)) def __lt__(self, other): return self.__compare(other, lambda x, y: x < y) def __gt__(self, other): return self.__compare(other, lambda x, y: x > y) def __le__(self, other): return self.__compare(other, lambda x, y: x <= y) def __ge__(self, other): return self.__compare(other, lambda x, y: x >= y) def __eq__(self, other): return self.__compare(other, lambda x, y: x == y) class RealTime(Time): def _realtime(self, snapshot): return self._value def _monotonic(self, snapshot): return self._value+(snapshot.monotonic()-snapshot.realtime()) @staticmethod def _now(snapshot): return snapshot.realtime() class MonotonicTime(Time): def _realtime(self, snapshot): return self._value+(snapshot.realtime()-snapshot.monotonic()) def _monotonic(self, snapshot): return self._value @staticmethod def _now(snapshot): return snapshot.monotonic() # class Infinity(Time): # def __init__(self): # super().__init__(math.inf) # # def _realtime(self, snapshot): # return math.inf # # def _monotonic(self, snapshot): # return math.inf # # @staticmethod # def _now(snapshot): # return 0 class DreamTime(Time): def _realtime(self, snapshot): return self._value+(snapshot.realtime()-snapshot.dreamtime()) # Important: monotonic is "static" within a wakeup period # and does not need to call time.monotonic(), as it gets compared # to a specific time.monotonic() realisation def _monotonic(self, snapshot): return self._value+(snapshot.monotonic()-snapshot.dreamtime()) def horizon(self, snapshot=Snapshot()): if snapshot.sleeping(): return math.inf else: return self._monotonic(snapshot) @staticmethod def _now(snapshot): return snapshot.dreamtime() if platform.system()=='Darwin': import Foundation import AppKit def do_callbacks(callbacks, woke): for callback in callbacks.values(): try: callback(woke) except Exception: logger.exception("Error in sleep/wake notification callback") # # Wake up / sleep handling # class SleepHandler(Foundation.NSObject): """ Handle wake/sleep notifications """ def init(self): self.__sleeptime=None self.__slept=0 self.__epoch=time.monotonic() self.__lock=threading.Lock() self.__callbacks=weakref.WeakKeyDictionary() return self @protect_noreturn def handleSleepNotification_(self, aNotification): logger.info("System going to sleep") now=time.monotonic() with self.__lock: self.__sleeptime=now callbacks=self.__callbacks.copy() do_callbacks(callbacks, False) @protect_noreturn def handleWakeNotification_(self, aNotification): logger.info("System waking up from sleep") now=time.monotonic() with self.__lock: if self.__sleeptime: slept=max(0, now-self.__sleeptime) logger.info("Slept %f seconds" % slept) self.__slept=self.__slept+slept self.__sleeptime=None callbacks=self.__callbacks.copy() do_callbacks(callbacks, True) # Return dreamtime def dreamtime_sleeping(self, snapshot=Snapshot()): sleeping=False with self.__lock: # macOS "sleep" signals / status are complete bollocks: at least # when plugged in, the system is actually sometimes running all # night with the lid closed and sleepNotification delivered. # Therefore, we need our timers to work in a sane manner when # we should be sleeping! if self.__sleeptime is not None: now_monotonic=self.__sleeptime sleeping=True else: now_monotonic=snapshot.monotonic() now_dreamtime=max(0, now_monotonic-self.__epoch-self.__slept) return now_dreamtime, sleeping # Weirdo (Py)ObjC naming to stop it form choking up def addForObj_aCallback_(self, obj, callback): with self.__lock: self.__callbacks[obj]=callback # obj is to use a a key in a weak key dictionary def add_callback(obj, callback): global _dreamtime_monitor monitor=_dreamtime_monitor if not monitor: raise Exception("Dreamtime monitor not started") else: monitor.addForObj_aCallback_(obj, callback) def start_monitoring(): global _dreamtime_monitor logger.debug("Starting to monitor system sleep") workspace = AppKit.NSWorkspace.sharedWorkspace() notification_center = workspace.notificationCenter() _dreamtime_monitor = SleepHandler.new() notification_center.addObserver_selector_name_object_( _dreamtime_monitor, "handleSleepNotification:", AppKit.NSWorkspaceWillSleepNotification, None) notification_center.addObserver_selector_name_object_( _dreamtime_monitor, "handleWakeNotification:", AppKit.NSWorkspaceDidWakeNotification, None) else: # Not on macOS def add_callback(obj, callback): pass def start_monitoring(): logger.warning(("No system sleep monitor implemented for '%s'" % platform.system()))