Mon, 22 Jan 2018 18:56:43 +0000
Logging improvements
# # Borgend MacOS UI # import rumps import time import datetime import logging import borgend import backup from threading import Lock, Timer from config import settings import objc logger=borgend.logger.getChild(__name__) traynames={ backup.INACTIVE: 'B.', backup.SCHEDULED: 'B.', backup.ACTIVE: 'B!', backup.BUSY: 'B⦙', backup.OFFLINE: 'B⦙', backup.ERRORS: 'B?' } statestring={ backup.INACTIVE: 'inactive', backup.SCHEDULED: 'scheduled', backup.ACTIVE: 'active', backup.BUSY: 'busy', backup.OFFLINE: 'offline', backup.ERRORS: 'errors' } # Refresh the menu at most once a second to reduce flicker refresh_interval=1.0 # Workaround to rumps brokenness; # see https://github.com/jaredks/rumps/issues/59 def notification_workaround(title, subtitle, message): try: NSDictionary = objc.lookUpClass("NSDictionary") d=NSDictionary() rumps.notification(title, subtitle, message, data=d) except Exception as err: logger.exception("Failed to display notification") # Based on code snatched from # https://stackoverflow.com/questions/12523586/python-format-size-application-converting-b-to-kb-mb-gb-tb/37423778 def humanbytes(B): 'Return the given bytes as a human friendly KB, MB, GB, or TB string' B = float(B) KB = float(1024) MB = float(KB ** 2) # 1,048,576 GB = float(KB ** 3) # 1,073,741,824 TB = float(KB ** 4) # 1,099,511,627,776 if B < KB: return '{0}B'.format(B) elif KB <= B < MB: return '{0:.2f}KB'.format(B/KB) elif MB <= B < GB: return '{0:.2f}MB'.format(B/MB) elif GB <= B < TB: return '{0:.2f}GB'.format(B/GB) elif TB <= B: return '{0:.2f}TB'.format(B/TB) def make_title(status): state=status['state'] detail='' 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 state>=backup.BUSY and state in statestring: detail=statestring[state] + '; ' if status['detail']!='normal': detail=detail+status['detail']+' ' title="%s (%s%s %s)" % (status['name'], detail, status['operation'], whenstr) elif status['type']=='current': # Operation running progress='' if 'progress_current' in status and 'progress_total' in status: 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'])) title="%s (running: %s%s)" % (status['name'], status['operation'], progress) else: # status['type']=='nothing': # Should be unscheduled, nothing running detail='' if state>=backup.BUSY and state in statestring: detail=' (' + statestring[state] + ')' title=status['name'] + detail return title, state class BorgendTray(rumps.App): def __init__(self, backups): 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, errors=None: self.__status_callback(obj, _index, status, errors)) b.set_status_update_callback(cb) self.statuses[index]=b.status() menu, state=self.__rebuild_menu() self.refresh_timer=None super().__init__(traynames[state], menu=menu, quit_button=None) def __rebuild_menu(self): menu=[] state=backup.INACTIVE for index in range(len(self.backups)): b=self.backups[index] title, this_state=make_title(self.statuses[index]) # 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) if this_state==backup.SCHEDULED: item.state=1 elif this_state>=backup.BUSY: item.state=-1 menu.append(item) state=backup.combine_state(state, this_state) menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog()) menu.append(menu_log) if not settings['no_quit_menu_entry']: menu_quit=rumps.MenuItem("Quit...", callback=lambda _: self.quit()) menu.append(menu_quit) return menu, state def refresh_ui(self): with self.lock: self.refresh_timer=None logger.debug('Rebuilding menu') menu, active=self.__rebuild_menu() self.menu.clear() self.menu.update(menu) self.title=traynames[active] def __status_callback(self, obj, index, status, errorlog): logger.debug('Status callback: %s' % str(status)) with self.lock: self.statuses[index]=status if self.refresh_timer==None: self.refresh_timer=Timer(refresh_interval, self.refresh_ui) self.refresh_timer.start() if errorlog: if 'msgid' not in errorlog or not isinstance(errorlog['msgid'], str): msgid='UnknownError' else: msgid=errorlog['msgid'] logger.debug("Opening notification for error %s '%s'", msgid, errorlog['message']) notification_workaround(borgend.appname_stylised, msgid, errorlog['message']) def quit(self): logging.shutdown() rumps.quit_application() def __menu_select_backup(self, sender, b): #sender.state=not sender.state logger.debug("Manually backup '%s'", b.name) try: b.create() except Exception as err: logger.exception("Failure to initialise backup") notification_workaround(borgend.appname_stylised, err.__class__.__name__, str(err)) # # Log window # logwindow=[None] logwindow_lock=Lock() def showlog(): try: w=None with logwindow_lock: if not logwindow[0]: lines=borgend.fifolog.formatAll() msg="\n".join(lines[0:]) w=rumps.Window(title=borgend.appname_stylised+' log', default_text=msg, ok='Close', dimensions=(640,320)) logwindow[0]=w if w: try: w.run() finally: with logwindow_lock: logwindow[0]=None except Exception as err: logger.exception("Failed to display log") # # Notification click response => show log window # @rumps.notifications def notification_center(_): showlog()