Errors as rumps notifications

Sun, 21 Jan 2018 00:58:06 +0000

author
Tuomo Valkonen <tuomov@iki.fi>
date
Sun, 21 Jan 2018 00:58:06 +0000
changeset 21
c36e549a7f12
parent 20
fdfbe5d7b677
child 22
c3e95212e3f0

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

mercurial