borgend/dreamtime.py

Wed, 31 Jan 2018 00:06:54 +0000

author
Tuomo Valkonen <tuomov@iki.fi>
date
Wed, 31 Jan 2018 00:06:54 +0000
changeset 97
96d5adbe0205
parent 94
2a11fd29c192
child 100
b141bed9e718
permissions
-rw-r--r--

Pruning support

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

# Return difference (delay) of "dreamtime" to monotonic time
def dreamtime_difference():
    if _dreamtime_monitor:
        return _dreamtime_monitor.diff()
    else:
        return time.monotonic()

# Return "dreamtime"
def dreamtime():
    return max(0, time.monotonic()-dreamtime_difference())

class Time:
    def __init__(self, when):
        self._value=when

    def realtime(self):
        raise NotImplementedError

    def monotonic(self):
        raise NotImplementedError

    @staticmethod
    def _now():
        raise NotImplementedError

    @classmethod
    def now(cls):
        return cls(cls._now())

    @classmethod
    def from_realtime(cls, realtime):
        return cls(realtime-time.time()+cls._now())

    @classmethod
    def from_monotonic(cls, monotonic):
        return cls(monotonic-time.monotonic()+cls._now())

    @classmethod
    def after(cls, seconds):
        return cls(cls._now()+seconds)

    @classmethod
    def after_other(cls, other, seconds):
        if isinstance(other, cls):
            return cls(other._value+seconds)
        else:
            return cls.from_monotonic(other.monotonic()+seconds)

    def datetime(self):
        return datetime.datetime.fromtimestamp(self.realtime())

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

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

    def __lt__(self, other):
        return self.monotonic() < other.monotonic()

    def __gt__(self, other):
        return self.monotonic() > other.monotonic()

    def __le__(self, other):
        return self.monotonic() <= other.monotonic()

    def __ge__(self, other):
        return self.monotonic() >= other.monotonic()

    def __eq__(self, other):
        return self.monotonic() == other.realtime()

class RealTime(Time):
    def realtime(self):
        return self._value

    def monotonic(self):
        return self._value+(time.monotonic()-time.time())

    @staticmethod
    def _now():
        return time.time()

class MonotonicTime(Time):
    def realtime(self):
        return self._value+(time.time()-time.monotonic())

    def monotonic(self):
        return self._value

    @staticmethod
    def _now():
        return time.monotonic()

class DreamTime(Time):
    def realtime(self):
        return self._value+(time.time()-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):
        return self._value+dreamtime_difference()

    @staticmethod
    def _now():
        return 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 difference to time.monotonic()
        def diff(self):
            with self.__lock:
                diff=self.__epoch+self.__slept
            return diff

        # 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