borgend/dreamtime.py

Sun, 04 Feb 2018 00:22:20 +0000

author
Tuomo Valkonen <tuomov@iki.fi>
date
Sun, 04 Feb 2018 00:22:20 +0000
changeset 100
b141bed9e718
parent 97
96d5adbe0205
child 101
3068b0de12ee
permissions
-rw-r--r--

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!

This patch is a first stage of this fix, implementing stopped dreamtime.
It also has improved comparisons of different types of time, based on
snapshotting.

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

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

    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(self):
        if self._dreamtime is None:
            if _dreamtime_monitor:
                self._dreamtime=_dreamtime_monitor.dreamtime(snapshot=self)
            else:
                self._dreamtime=self.monotonic()
        return self._dreamtime

# 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):
        return datetime.datetime.fromtimestamp(self.realtime())

    def seconds_to(self):
        return self._value-self._now(Snapshot())

    def isoformat(self):
        return self.datetime().isoformat()

    def realtime(self):
        return self._realtime(Snapshot())

    def monotonic(self):
        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 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())

    @staticmethod
    def _now(snapshot):
        return snapshot.dreamtime()


if platform.system()=='Darwin':

    import Foundation
    import AppKit

    #
    # 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")
            now=time.monotonic()
            with self.__lock:
                self.__sleeptime=now

        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()
            except:
                logger.exception("Bug in wakeup handler")

            for callback in callbacks.values():
                try:
                    callback()
                except Exception:
                    logger.exception("Error in wake notification callback")

        # Return dreamtime
        def dreamtime(self, snapshot=Snapshot()):
            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
                else:
                    now_monotonic=snapshot.monotonic()
                now_dreamtime=max(0, now_monotonic-self.__epoch-self.__slept)
            return now_dreamtime

        # 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