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()) + + +