borgend/dreamtime.py

Sun, 04 Feb 2018 01:27:38 +0000

author
Tuomo Valkonen <tuomov@iki.fi>
date
Sun, 04 Feb 2018 01:27:38 +0000
changeset 101
3068b0de12ee
parent 100
b141bed9e718
child 106
a7bdc239ef62
permissions
-rw-r--r--

Part 2 of handling macOS sleep/wake signal brokenness.
a) Added Time.horizon(), which indicates how far the Time event is from the
epoch, in monotonic time. For DreamTime, if the system is sleeping, this
returns ∞. b) The DreamTime monitor also signals sleep to callbacks, so
that the Scheduler can re-sort the events. The sorting is now done by
the horizon, so DreamTime events will be moved last and not activated when
the system is sleeping or "sleeping", but other events will be executed
normally if the system is merely "sleeping".

#
# 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

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

        def handleSleepNotification_(self, aNotification):
            logger.info("System going to sleep")
            try:
                now=time.monotonic()
                with self.__lock:
                    self.__sleeptime=now
                    callbacks=self.__callbacks.copy()
                do_callbacks(callbacks, False)
            except:
                logger.exception("Bug in sleep handler")

        def handleWakeNotification_(self, aNotification):
            logger.info("System waking up from sleep")
            try:
                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)
            except:
                logger.exception("Bug in wakeup handler")

        # 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()))

mercurial