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