Semi-working menu items.

Sat, 20 Jan 2018 19:57:05 +0000

author
Tuomo Valkonen <tuomov@iki.fi>
date
Sat, 20 Jan 2018 19:57:05 +0000
changeset 10
76dbfb06eba0
parent 9
aa121291eb0e
child 11
0bff53095f28

Semi-working menu items.

NOTES:

Python closures suck dog's balls... first and the last program I
write in Python until somebody fixes the brain-damaged scoping
that requires word _b=b hacks etc. to get normal behaviour.
See also http://math.andrej.com/2009/04/09/pythons-lambda-is-broken/

Rumps also sucks, apparently no way to update single menu items, one
always has to rebuild the entire menu. Spent hours trying to get it to
work until giving in.

backup.py file | annotate | diff | comparison | revisions
borgend.py file | annotate | diff | comparison | revisions
instance.py file | annotate | diff | comparison | revisions
ui.py file | annotate | diff | comparison | revisions
--- a/backup.py	Sat Jan 20 15:55:09 2018 +0000
+++ b/backup.py	Sat Jan 20 19:57:05 2018 +0000
@@ -77,9 +77,9 @@
         self.thread_log=None
         self.thread_res=None
         self.timer=None
-        self.timer_operation=None
-        self.timer_time=None
+        self.scheduled_operation=None
         self.lock=Lock()
+        self.status_update_callback=None
 
     def is_running(self):
         with self.lock:
@@ -147,6 +147,11 @@
 
 
     def __result_listener(self):
+        with self.lock:
+            status, callback=self.__status_unlocked()
+        if callback:
+            callback(self, status)
+
         logging.debug('Result listener thread waiting for result')
 
         res=self.borg_instance.read_result()
@@ -165,22 +170,24 @@
         logging.debug('Borg subprocess terminated (success: %s); terminating result listener thread' % str(success))
 
         with self.lock:
-            if self.current_operation=='create':
-                self.lastrun=self.time_started
+            if self.current_operation['operation']=='create':
+                self.lastrun=self.current_operation['when_monotonic']
                 self.lastrun_success=success
             self.thread_res=None
             self.__finish_and_reschedule_if_both_listeners_terminated()
+            status, callback=self.__status_unlocked()
+        if callback:
+            callback(self, status)
 
     def __finish_and_reschedule_if_both_listeners_terminated(self):
         if self.thread_res==None and self.thread_log==None:
             logging.debug('Both threads terminated')
             self.borg_instance=None
-            self.time_started=None
             self.current_operation=None
             self.__schedule_unlocked()
 
-    def __do_launch(self, queue, operation, archive_or_repository, *args):
-        inst=BorgInstance(operation, archive_or_repository, *args)
+    def __do_launch(self, queue, op, archive_or_repository, *args):
+        inst=BorgInstance(op['operation'], archive_or_repository, *args)
         inst.launch()
 
         t_log=Thread(target=self.__log_listener)
@@ -193,13 +200,13 @@
         self.thread_res=t_res
         self.borg_instance=inst
         self.queue=queue
-        self.current_operation=operation
-        self.time_started=time.monotonic()
+        self.current_operation=op
+        self.current_operation['when_monotonic']=time.monotonic()
 
         t_log.start()
         t_res.start()
 
-    def __launch(self, operation, queue):
+    def __launch(self, op, queue):
         if self.__is_running_unlocked():
             logging.info('Cannot start %s: already running %s'
                          % (operation, self.current_operation))
@@ -208,38 +215,39 @@
             if self.timer:
                 logging.debug('Unscheduling timed operation due to launch of operation')
                 self.timer=None
-                self.timer_operation=None
-                self.timer_time=None
+                self.scheduled_operation=None
 
-            logging.debug("Launching '%s' on '%s'" % (operation, self.name))
+            logging.debug("Launching '%s' on '%s'" % (op['operation'], self.name))
 
-            if operation=='create':
+            if op['operation']=='create':
                 archive="%s::%s%s" % (self.repository,
                                       self.archive_prefix,
                                       self.archive_template)
 
-                self.__do_launch(queue, operation, archive,
+                self.__do_launch(queue, op, archive,
                                  self.common_parameters+self.create_parameters,
                                  self.paths)
-            elif operation=='prune':
-                self.__do_launch(queue, 'prune', self.repository,
+            elif op['operation']=='prune':
+                self.__do_launch(queue, op, self.repository,
                                  ([{'prefix': self.archive_prefix}] + 
                                   self.common_parameters +
                                   self.prune_parameters))
             else:
-                logging.error("Invalid operaton '%s'" % operation)
+                logging.error("Invalid operaton '%s'" % op['operation'])
                 self.__schedule_unlocked()
 
             return True
 
     def create(self, queue):
+        op={'operation': 'create', 'detail': 'manual'}
         with self.lock:
-            res=self.__launch('create', queue)
+            res=self.__launch(op, queue)
         return res
 
     def prune(self, queue):
+        op={'operation': 'prune', 'detail': 'manual'}
         with self.lock:
-            res=self.__launch('prune', queue)
+            res=self.__launch(op, queue)
         return res
 
     # TODO: Decide exact (manual) abort mechanism. Perhaps two stages
@@ -274,9 +282,8 @@
 
     def __queue_timed_operation(self):
         with self.lock:
-            operation=self.timer_operation
-            self.timer_operation=None
-            self.timer_time=None
+            op=self.scheduled_operation
+            self.scheduled_operation=None
             self.timer=None
 
             if self.__is_running_unlocked():
@@ -285,42 +292,85 @@
                 # TODO: Queue on 'repository' and online status for SSH, etc.
 
                 # TODO: UI comms. queue?
-                self.__launch(operation, None)
-
-    def __schedule_unlocked(self):
-        if self.current_operation:
-            return self.current_operation, None
-        else:
-            operation, when=self.__next_operation_unlocked()
-
-            if operation:
-                now=time.monotonic()
-                delay=max(0, when-now)
-                logging.info("Scheduling '%s' of '%s' in %d seconds" %
-                             (operation, self.name, delay))
-                tmr=Timer(delay, self.__queue_timed_operation)
-                self.timer_operation=operation
-                self.timer_time=when
-                self.timer=tmr
-                tmr.start()
-
-            return operation, time
+                self.__launch(op, None)
 
     def __next_operation_unlocked(self):
         # TODO: pruning as well
         now=time.monotonic()
         if not self.lastrun:
-            return 'create', now+self.retry_interval
+            initial_interval=self.retry_interval
+            if initial_interval==0:
+                initial_interval=self.backup_interval
+            if initial_interval==0:
+                return None
+            else:
+                return {'operation': 'create',
+                        'detail': 'initial',
+                        'when_monotonic': now+initial_interval}
         elif not self.lastrun_success:
-            return 'create', self.lastrun+self.retry_interval
+            if self.retry_interval==0:
+                return None
+            else:
+                return {'operation': 'create',
+                        'detail': 'retry',
+                        'when_monotonic': self.lastrun+self.retry_interval}
         else:
             if self.backup_interval==0:
-                return 'none', 0
+                return None
             else:
-                return 'create', self.lastrun+self.backup_interval
+                return {'operation': 'create',
+                        'detail': None,
+                        'when_monotonic': self.lastrun+self.backup_interval}
+
+    def __schedule_unlocked(self):
+        if self.current_operation:
+            return self.current_operation
+        else:
+            op=self.__next_operation_unlocked()
+
+            if op:
+                now=time.monotonic()
+                delay=max(0, op['when_monotonic']-now)
+                logging.info("Scheduling '%s' (detail: %s) of '%s' in %d seconds" %
+                             (op['operation'], op['detail'], self.name, delay))
+                tmr=Timer(delay, self.__queue_timed_operation)
+                self.scheduled_operation=op
+                self.timer=tmr
+                tmr.start()
+
+            return op
 
     def schedule(self):
         with self.lock:
             return self.__schedule_unlocked()
 
+    def set_status_update_callback(self, callback):
+        with self.lock:
+            self.status_update_callback=callback
 
+    def status(self):
+        with self.lock:
+            res=self.__status_unlocked()
+        return res[0]
+
+    def __status_unlocked(self):
+        callback=self.status_update_callback
+        if self.current_operation:
+            status=self.current_operation
+            status['type']='current'
+        elif self.scheduled_operation:
+            status=self.scheduled_operation
+            status['type']='scheduled'
+        else:
+            status={'type': 'nothing'}
+
+        status['name']=self.name
+
+        if 'when_monotonic' in status:
+            status['when']=(status['when_monotonic']
+                            -time.monotonic()+time.time())
+
+        return status, callback
+
+
+
--- a/borgend.py	Sat Jan 20 15:55:09 2018 +0000
+++ b/borgend.py	Sat Jan 20 19:57:05 2018 +0000
@@ -19,17 +19,13 @@
 for i in range(len(backupconfigs)):
     logging.info('Setting up backup set %d' % i)
     backups[i]=Backup(i, backupconfigs[i])
-
-queue=Queue()
-#print(backups[0].create(queue))
-backups[0].schedule()
-
-#backups[0].join()
+    backups[i].schedule()
 
 if __name__ == "__main__":
     #print(settings)
     tray=BorgendTray("Borgend", backups);
     tray.run()
+    pass
 
     #
     # This shit is fucked, disables ^C etc., and threading doesn't seem to help
--- a/instance.py	Sat Jan 20 15:55:09 2018 +0000
+++ b/instance.py	Sat Jan 20 19:57:05 2018 +0000
@@ -40,7 +40,7 @@
 
         logging.info('Launching ' + str(cmd))
 
-        self.proc=Popen(cmd, stdout=PIPE, stderr=PIPE)
+        self.proc=Popen(cmd, stdout=PIPE, stderr=PIPE, stdin=None)
 
     def read_result(self):
         stream=self.proc.stdout
--- a/ui.py	Sat Jan 20 15:55:09 2018 +0000
+++ b/ui.py	Sat Jan 20 19:57:05 2018 +0000
@@ -3,16 +3,101 @@
 #
 
 import rumps
+import time
+import datetime
+import logging
+from threading import Lock
+
+def make_title(status):
+    if status['type']=='scheduled':
+        # Operation scheduled
+        when=status['when']
+        now=time.time()
+        if when<now:
+            whenstr='overdue'
+        else:
+            diff=datetime.timedelta(seconds=when-now)
+            if diff.days>0:
+                whenday=datetime.date.fromtimestamp(when)
+                whenstr='on %s' % whenday.isoformat()
+            else:
+                twhen=time.localtime(when)
+                if twhen.tm_sec>30:
+                    # Round up minute display to avoid user confusion
+                    twhen=time.localtime(when+30)
+                whenstr='at %02d:%02d' % (twhen.tm_hour, twhen.tm_min)
+        detail=''
+        if 'detail' in status and status['detail']:
+            detail=status['detail']+' '
+        return "%s (%s%s %s)" % (status['name'], detail, status['operation'], whenstr)
+    elif status['type']=='current':
+        # Operation running
+       return "%s (running: %s)" % (status['name'], status['operation'])
+    else: # status['type']=='nothing':
+        # Should be unscheduled, nothing running
+        return status['name']
+
 
 class BorgendTray(rumps.App):
     def __init__(self, name, backups):
-        menu=[rumps.MenuItem(b.name, callback=self.silly) for b in backups]
-        menu = menu + [rumps.MenuItem("Quit...", callback=self.my_quit_button)]
-        super().__init__(name, menu=menu, quit_button=None)
+        self.lock=Lock()
+        self.backups=backups
+        self.statuses=[None]*len(backups)
+
+        with self.lock:
+            # Initialise reporting callbacks and local status copy
+            # (since rumps doesn't seem to be able to update menu items
+            # without rebuilding the whole menu, and we don't want to lock
+            # when calling Backup.status(), especially as we will be called
+            # from Backup reporting its status, we keep a copy to allow
+            # rebuilding the entire menu
+            for index in range(len(backups)):
+                b=backups[index]
+                # Python closures suck dog's balls; hence the _index=index hack
+                # See also http://math.andrej.com/2009/04/09/pythons-lambda-is-broken/
+                cb=(lambda obj, status, _index=index:
+                    self.__status_callback(obj, _index, status))
+                b.set_status_update_callback(cb)
+                self.statuses[index]=b.status()
+
+            menu=self.__rebuild_menu()
 
-    def my_quit_button(self, _):
+            super().__init__(name, menu=menu, quit_button=None)
+
+    def __rebuild_menu(self):
+        menu=[]
+        for index in range(len(self.backups)):
+            b=self.backups[index]
+            title=make_title(self.statuses[index])
+            logging.info('TITLE: %s' % title)
+            # Python closures suck dog's balls...
+            # first and the last program I write in Python until somebody
+            # fixes this brain damage
+            cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b)
+            item=rumps.MenuItem(title, callback=cbm)
+            menu.append(item)
+
+        menu_quit=rumps.MenuItem("Quit...", callback=self.my_quit)
+        menu.append(menu_quit)
+
+        return menu
+
+
+    def my_quit(self, _):
         rumps.quit_application()
 
-    def silly(self, sender):
-        sender.state=not sender.state
+    def __menu_select_backup(self, sender, b):
+        #sender.state=not sender.state
+        logging.debug("Manually backup '%s'", b.name)
+        b.create(None)
 
+    def __status_callback(self, obj, index, status):
+        logging.debug('Status callbackup %s' % str(status))
+        with self.lock:
+            self.statuses[index]=status
+            logging.debug('Rebuilding menu')
+            self.menu.clear()
+            self.menu.update(self.__rebuild_menu())
+
+
+

mercurial