Part 2 of handling macOS sleep/wake signal brokenness.

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 102
0d43cd568f3c

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/dreamtime.py file | annotate | diff | comparison | revisions
borgend/repository.py file | annotate | diff | comparison | revisions
borgend/scheduler.py file | annotate | diff | comparison | revisions
borgend/ui.py file | annotate | diff | comparison | revisions
--- a/borgend/dreamtime.py	Sun Feb 04 00:22:20 2018 +0000
+++ b/borgend/dreamtime.py	Sun Feb 04 01:27:38 2018 +0000
@@ -10,6 +10,7 @@
 import weakref
 import datetime
 import logging
+import math
 
 logger=logging.getLogger(__name__)
 
@@ -26,6 +27,7 @@
         self._monotonic=None
         self._realtime=None
         self._dreamtime=None
+        self._sleeping=None
 
     def monotonic(self):
         if self._monotonic is None:
@@ -37,13 +39,21 @@
             self._realtime=time.time()
         return self._realtime
 
-    def dreamtime(self):
+    def dreamtime_sleeping(self):
         if self._dreamtime is None:
             if _dreamtime_monitor:
-                self._dreamtime=_dreamtime_monitor.dreamtime(snapshot=self)
+                self._dreamtime, self._sleeping=_dreamtime_monitor.dreamtime_sleeping(snapshot=self)
             else:
-                self._dreamtime=self.monotonic()
-        return self._dreamtime
+                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:
@@ -84,20 +94,26 @@
             return cls.from_monotonic(other._monotonic(snapshot)+seconds,
                                       snapshot)
 
-    def datetime(self):
-        return datetime.datetime.fromtimestamp(self.realtime())
+    def datetime(self, snapshot=Snapshot()):
+        return datetime.datetime.fromtimestamp(self._realtime(snapshot))
 
-    def seconds_to(self):
-        return self._value-self._now(Snapshot())
+    def seconds_to(self, snapshot=Snapshot()):
+        return self._value-self._now(snapshot)
 
     def isoformat(self):
         return self.datetime().isoformat()
 
-    def realtime(self):
-        return self._realtime(Snapshot())
+    def realtime(self, snapshot=Snapshot()):
+        return self._realtime(snapshot)
+
+    def monotonic(self, snapshot=Snapshot()):
+        return self._monotonic(snapshot)
 
-    def monotonic(self):
-        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__):
@@ -143,6 +159,20 @@
     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())
@@ -153,6 +183,12 @@
     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()
@@ -163,6 +199,13 @@
     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
     #
@@ -181,9 +224,14 @@
 
         def handleSleepNotification_(self, aNotification):
             logger.info("System going to sleep")
-            now=time.monotonic()
-            with self.__lock:
-                self.__sleeptime=now
+            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")
@@ -196,17 +244,13 @@
                         self.__slept=self.__slept+slept
                         self.__sleeptime=None
                     callbacks=self.__callbacks.copy()
+                do_callbacks(callbacks, True)
             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()):
+        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
@@ -215,10 +259,11 @@
                 # 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
+            return now_dreamtime, sleeping
 
         # Weirdo (Py)ObjC naming to stop it form choking up
         def addForObj_aCallback_(self, obj, callback):
--- a/borgend/repository.py	Sun Feb 04 00:22:20 2018 +0000
+++ b/borgend/repository.py	Sun Feb 04 01:27:38 2018 +0000
@@ -18,7 +18,7 @@
         self._goodtogo=False
         super().__init__(cond, name=name)
 
-    def __lt__(self, other):
+    def is_before(self, other, snapshot=None):
         return False
 
 # This FIFO essentially a fancy semaphore: Each thread waits on its own
--- a/borgend/scheduler.py	Sun Feb 04 00:22:20 2018 +0000
+++ b/borgend/scheduler.py	Sun Feb 04 01:27:38 2018 +0000
@@ -22,14 +22,23 @@
         self.cond=cond
         self.linked=False
 
-    def __lt__(self, other):
+    @staticmethod
+    def snapshot():
+        return None
+
+    def is_before(self, other, snapshot=None):
         raise NotImplementedError
 
-    def insert_after(self, ev):
-        if not self.next or ev<self.next:
+    def __lt__(self, other):
+        return self.is_before(other, self.snapshot())
+
+    def insert_after(self, ev, snapshot=None):
+        if not snapshot:
+            snapshot=self.snapshot()
+        if not self.next or ev.is_before(self.next, snapshot):
             self.insert_immediately_after(ev)
         else:
-            self.next.insert_after(ev)
+            self.next.insert_after(ev, snapshot)
 
     def insert_immediately_after(self, ev):
         assert(ev.next is None and ev.prev is None)
@@ -59,8 +68,14 @@
         super().__init__(cond, name=name)
         self.when=when
 
-    def __lt__(self, other):
-        return self.when < other.when
+    @staticmethod
+    def snapshot():
+        return dreamtime.Snapshot()
+
+    def is_before(self, other, snapshot=None):
+        if not snapshot:
+            snapshot=self.snapshot()
+        return self.when.horizon(snapshot) < other.when.horizon(snapshot)
 
 class TerminableThread(Thread):
     def __init__(self, *args, **kwargs):
@@ -79,7 +94,7 @@
         self.daemon = True
         self._list = None
 
-    def _insert(self, ev):
+    def _insert(self, ev, snapshot=None):
         assert(not ev.linked)
         if not self._list:
             #logger.debug("Insert first")
@@ -90,7 +105,9 @@
             self._list=ev
         else:
             #logger.debug("Insert after")
-            self._list.insert_after(ev)
+            if not snapshot:
+                snapshot=ev.snapshot()
+            self._list.insert_after(ev, snapshot)
         ev.linked=True
 
     def _unlink(self, ev):
@@ -100,15 +117,17 @@
         ev.unlink()
         ev.linked=False
 
-    def _resort(self):
+    def _resort(self, snapshot=None):
         oldlist=self._list
         self._list=None
+        if oldlist and not snapshot:
+            snapshot=oldlist.snapshot()
         while oldlist:
             ev=oldlist
             oldlist=oldlist.next
             ev.unlink()
             ev.linked=False
-            self._insert(ev)
+            self._insert(ev, snapshot)
 
 
 
@@ -120,28 +139,31 @@
         self.precision = precision
         self._next_event_time = None
         super().__init__(target = self._scheduler_thread, name = 'Scheduler')
-        dreamtime.add_callback(self, self._wakeup_callback)
+        dreamtime.add_callback(self, self._sleepwake_callback)
 
     def _scheduler_thread(self):
         logger.debug("Scheduler thread started")
         with self._cond:
             while not self._terminate:
-                now = time.monotonic()
+                snapshot = dreamtime.Snapshot()
+                now = snapshot.monotonic()
                 if not self._list:
                     timeout = None
                 else:
                     # Wait at most precision seconds, or until next event if it
                     # comes earlier
-                    timeout=min(self.precision, self._list.when.realtime()-now)
+                    delta = self._list.when.monotonic(snapshot)-now
+                    timeout = min(self.precision, delta)
 
                 if not timeout or timeout>0:
                     logger.debug("Scheduler waiting %s seconds" % str(timeout))
                     self._cond.wait(timeout)
-                    now = time.monotonic()
+                    snapshot = dreamtime.Snapshot()
+                    now = snapshot.monotonic()
 
                 logger.debug("Scheduler timed out")
 
-                while self._list and self._list.when.monotonic() <= now:
+                while self._list and self._list.when.horizon(snapshot) <= now:
                     ev=self._list
                     logger.debug("Scheduler activating %s" % (ev.name or "(unknown)"))
                     # We are only allowed to remove ev from list when ev.cond allows
@@ -154,8 +176,8 @@
                     self._cond.acquire()
 
 
-    def _wakeup_callback(self):
-        logger.debug("Rescheduling events after wakeup")
+    def _sleepwake_callback(self, woke):
+        logger.debug("Rescheduling events after sleep/wakeup")
         with self._cond:
             self._resort()
 
--- a/borgend/ui.py	Sun Feb 04 00:22:20 2018 +0000
+++ b/borgend/ui.py	Sun Feb 04 01:27:38 2018 +0000
@@ -208,7 +208,7 @@
                 self.__status_callback(_index, status, errorlog=errorlog))
             backups[index].set_status_update_callback(cb)
 
-        dreamtime.add_callback(self, self.refresh_ui)
+        dreamtime.add_callback(self, self.__sleepwake_callback)
 
     def __rebuild_menu(self):
         menu=[]
@@ -305,6 +305,10 @@
             notification_workaround(branding.appname_stylised,
                                     msgid, errorlog['message'])
 
+    def __sleepwake_callback(self, woke):
+        if woke:
+            self.refresh_ui()
+
     def pause_resume_all(self):
         with self.lock:
             try:

mercurial