Sun, 21 Jan 2018 00:58:06 +0000
Errors as rumps notifications
NOTE: These seem to be broken in rumps, so there's a workaround.
But the whole app crashes on the notification callback, probably due to the
same brokenness. See https://github.com/jaredks/rumps/issues/59
backup.py | file | annotate | diff | comparison | revisions | |
config.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 23:50:36 2018 +0000 +++ b/backup.py Sun Jan 21 00:58:06 2018 +0000 @@ -72,15 +72,20 @@ 'Prune parameters', self.loc, default=[]) - keychain_account=config.check_string(cfg, 'keychain_account', - 'Keychain account', self.loc, - default='') + self.__keychain_account=config.check_string(cfg, 'keychain_account', + 'Keychain account', self.loc, + default='') + + if config.settings['__extract_passphrases_at_startup']: + self.extract_passphrase() - if keychain_account and keychain_account!='': - pw=keyring.get_password("borg-backup", keychain_account) - self.__password=pw + def extract_passphrase(self): + acc=self.__keychain_account + if acc and acc!='': + pw=keyring.get_password("borg-backup", acc) + self.__passphrase=pw else: - self.__password=None + self.__passphrase=None def __init__(self, identifier, cfg): self.identifier=identifier @@ -97,7 +102,7 @@ self.timer=None self.scheduled_operation=None self.lock=Lock() - self.status_update_callback=None + self.__status_update_callback=None def is_running(self): with self.lock: @@ -123,7 +128,7 @@ logging.debug(str(status)) t=status['type'] - errors_this_message=False + errors_this_message=None callback=None if t=='progress_percent': @@ -162,19 +167,15 @@ lvl=translate_loglevel(status['levelname']) logging.log(lvl, status['name'] + ': ' + status['message']) if lvl>=logging.WARNING: - errors_this_message=True - elif t=='exception': - errors_this_message=True - elif t=='unparsed_error': - errors_this_message=True - - if errors_this_message: - with self.lock: - self.current_operation['errors']=True - status, callback=self.__status_unlocked() + errors_this_message=status + with self.lock: + self.current_operation['errors']=True + status, callback=self.__status_unlocked() + else: + logging.debug('Unrecognised log entry %s' % str(status)) if callback: - callback(self, status) + callback(self, status, errors=errors_this_message) logging.debug('Waiting for borg subprocess to terminate in log thread') @@ -221,8 +222,10 @@ callback(self, status) def __do_launch(self, queue, op, archive_or_repository, *args): + self.extract_passphrase() + inst=BorgInstance(op['operation'], archive_or_repository, *args) - inst.launch(password=self.__password) + inst.launch(passphrase=self.__passphrase) t_log=Thread(target=self.__log_listener) t_log.daemon=True @@ -378,17 +381,13 @@ 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 + callback=self.__status_update_callback if self.current_operation: status=self.current_operation status['type']='current' @@ -409,5 +408,8 @@ return status, callback + def set_status_update_callback(self, callback): + with self.lock: + self.__status_update_callback=callback
--- a/config.py Sat Jan 20 23:50:36 2018 +0000 +++ b/config.py Sun Jan 21 00:58:06 2018 +0000 @@ -15,16 +15,19 @@ # defaults={ - # Default: backup every 6 hours (21600 seconds) - 'backup_interval': 21600, - # Default: retry every 15 minutes if unable to connect / unfinished backup - 'retry_interval': 900, - # borg - 'borg': { - 'executable': 'borg', - 'common_parameters': [], - 'create_parameters': [], - 'prune_parameters': [], + # borg + # Default: backup every 6 hours (21600 seconds) + 'backup_interval': 21600, + # Default: retry every 15 minutes if unable to connect / unfinished backup + 'retry_interval': 900, + # Extract passphrases at startup or on demand? + '__extract_passphrases_at_startup': True, + # Borg settings + 'borg': { + 'executable': 'borg', + 'common_parameters': [], + 'create_parameters': [], + 'prune_parameters': [], } } @@ -125,20 +128,22 @@ # Verify basic settings # -if 'borg' not in settings: - settings['borg']=defaults['borg'] -else: - def check_and_set(cfg, field, loc, defa, fn): - cfg[field]=fn(cfg, field, field, loc, defa[field]) - return cfg +def check_and_set(cfg, field, loc, defa, fn): + cfg[field]=fn(cfg, field, field, loc, defa[field]) + +def check_parameters(cmd): + check_and_set(settings['borg'], cmd+'_parameters', + 'borg', defaults['borg'], + check_list_of_dicts) - def check_parameters(cmd): - settings['borg']=check_and_set(settings['borg'], cmd+'_parameters', - 'borg', defaults['borg'], - check_list_of_dicts) - - settings['borg']=check_and_set(settings['borg'], 'executable', 'borg', - defaults['borg'], check_string) +check_and_set(settings, 'backup_interval', 'top-level', defaults, check_nonneg_int) +check_and_set(settings, 'retry_interval', 'top-level', defaults, check_nonneg_int) +check_and_set(settings, '__extract_passphrases_at_startup', 'top-level', defaults, check_nonneg_int) +check_and_set(settings, 'borg', 'top-level', defaults, check_dict) +# Check parameters within 'borg' +if True: + check_and_set(settings['borg'], 'executable', 'borg', + defaults['borg'], check_string) check_parameters('common') check_parameters('create')
--- a/instance.py Sat Jan 20 23:50:36 2018 +0000 +++ b/instance.py Sun Jan 21 00:58:06 2018 +0000 @@ -36,19 +36,19 @@ return cmd+arglistify(self.args)+[self.archive_or_repository]+self.argsl - def launch(self, password=None): + def launch(self, passphrase=None): cmd=self.construct_cmdline() logging.info('Launching ' + str(cmd)) env=None - if password: + if passphrase: env=os.environ.copy() - env['BORG_PASSPHRASE']=password + env['BORG_PASSPHRASE']=passphrase self.proc=Popen(cmd, env=env, stdout=PIPE, stderr=PIPE, stdin=PIPE) - # We don't do password input etc. + # We don't do passphrase input etc. self.proc.stdin.close() def read_result(self): @@ -71,7 +71,11 @@ except err: logging.debug('Pipe read failed: %s' % str(err)) - return {'type': 'exception', 'exception': err} + return {'type': 'log_message', + 'levelname': 'CRITICAL', + 'name': 'borgend.instance.BorgInstance', + 'msgid': 'Borgend.Exception', + 'message': err} if line==b'': @@ -91,7 +95,11 @@ for line in iter(stream.readline, b''): errmsg=errmsg+line - return {'type': 'unparsed_error', 'message': str(errmsg)} + return {'type': 'log_message', + 'levelname': 'ERROR', + 'name': 'borgend.instance.BorgInstance', + 'msgid': 'Borgend.JSONFail', + 'message': str(errmsg)} def terminate(self): self.proc.terminate()
--- a/ui.py Sat Jan 20 23:50:36 2018 +0000 +++ b/ui.py Sun Jan 21 00:58:06 2018 +0000 @@ -7,6 +7,7 @@ import datetime import logging from threading import Lock +import objc INACTIVE=0 ACTIVE=1 @@ -74,7 +75,7 @@ # Operation running progress='' if 'progress_current' in status and 'progress_total' in status: - progress=' %d%%' % (status.progress_current/status.progress_total) + progress=' %d%%' % (status['progress_current']/status['progress_total']) elif 'original_size' in status and 'deduplicated_size' in status: progress=' %s→%s' % (humanbytes(status['original_size']), humanbytes(status['deduplicated_size'])) @@ -107,8 +108,8 @@ 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)) + cb=(lambda obj, status, _index=index, errors=None: + self.__status_callback(obj, _index, status, errors)) b.set_status_update_callback(cb) self.statuses[index]=b.status() @@ -144,8 +145,9 @@ logging.debug("Manually backup '%s'", b.name) b.create(None) - def __status_callback(self, obj, index, status): + def __status_callback(self, obj, index, status, errorlog): logging.debug('Status callbackup %s' % str(status)) + with self.lock: self.statuses[index]=status logging.debug('Rebuilding menu') @@ -154,5 +156,25 @@ self.menu.update(menu) self.title=traynames[active] + if errorlog: + if 'msgid' not in errorlog or not isinstance(errorlog['msgid'], str): + msgid='UnknownError' + else: + msgid=errorlog['msgid'] + + logging.debug('Opening notification for error') + + # Workaround to rumps brokenness + # See https://github.com/jaredks/rumps/issues/59 + NSDictionary = objc.lookUpClass("NSDictionary") + d=NSDictionary() + + rumps.notification('Borgend', msgid, errorlog['message'], data=d) + + @rumps.notifications + def notification_center(data): + pass + +