Sat, 20 Jan 2018 19:57:05 +0000
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()) + + +