Add Background notifications for Linux (#114)

- Add DBus-based notifications for Linux and BSD desktops (@bpereto)
- Add module name to logging output (@bpereto)
This commit is contained in:
Benjamin Pereto 2019-01-13 02:51:35 +01:00 committed by Manuel Riel
parent 51159c043d
commit c5494f455c
8 changed files with 171 additions and 39 deletions

View File

@ -6,3 +6,4 @@ pytest-xdist
pyinstaller
tox
bump2version
flake8

View File

@ -32,7 +32,7 @@ font-weight: bold;
}</string>
</property>
<property name="currentIndex">
<number>3</number>
<number>0</number>
</property>
<widget class="QWidget" name="schedule">
<property name="geometry">
@ -57,6 +57,9 @@ font-weight: bold;
<string>Schedule</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>2</number>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="sizeConstraint">
@ -285,6 +288,22 @@ font-weight: bold;
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="font">
@ -427,6 +446,14 @@ font-weight: bold;
</layout>
</widget>
<widget class="QWidget" name="page_3">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>663</width>
<height>354</height>
</rect>
</property>
<attribute name="icon">
<iconset resource="../icons/collection.qrc">
<normaloff>:/icons/terminal.svg</normaloff>:/icons/terminal.svg</iconset>

View File

@ -14,7 +14,7 @@ from ..models import EventLogModel, BackupProfileMixin
from ..utils import keyring
mutex = QtCore.QMutex()
logger = logging.getLogger('vorta')
logger = logging.getLogger(__name__)
class BorgThread(QtCore.QThread, BackupProfileMixin):

View File

@ -13,8 +13,20 @@ from .config import LOG_DIR
logger = logging.getLogger('vorta')
logger.setLevel(logging.DEBUG)
# create handlers
fh = logging.FileHandler(os.path.join(LOG_DIR, 'vorta.log'))
# fh.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch = logging.StreamHandler()
ch.setLevel(logging.WARNING)
# create logging format
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
# apply formatter
fh.setFormatter(formatter)
ch.setFormatter(formatter)
# add handlers
logger.addHandler(fh)
logger.addHandler(ch)

View File

@ -207,14 +207,20 @@ def init_db(con):
'value': True,
'type': 'checkbox',
'label': 'Send errors to Sentry. This helps us quickly find bugs.'
},
{
'key': 'enable_notifications', 'value': True, 'type': 'checkbox',
'label': 'Display notifications when background tasks fail.'
},
{
'key': 'enable_notifications_success', 'value': False, 'type': 'checkbox',
'label': 'Also notify about successful background tasks.'
}
]
if sys.platform == 'darwin':
settings += [
{'key': 'autostart', 'value': False, 'type': 'checkbox',
'label': 'Add Vorta to Login Items in Preferences > Users and Groups > Login Items.'},
{'key': 'enable_notifications', 'value': True, 'type': 'checkbox',
'label': 'Display notifications when background tasks fail.'},
{'key': 'check_for_updates', 'value': True, 'type': 'checkbox',
'label': 'Check for updates on startup.'},
{'key': 'updates_include_beta', 'value': False, 'type': 'checkbox',

View File

@ -1,6 +1,10 @@
import sys
import logging
from PyQt5 import QtCore, QtDBus
from vorta.models import SettingsModel
logger = logging.getLogger(__name__)
class VortaNotifications:
"""
@ -12,33 +16,89 @@ class VortaNotifications:
@classmethod
def pick(cls):
if sys.platform == 'darwin':
return DarwinNotifications
elif sys.platform == 'linux':
return LinuxNotifications
else: # Save to sqlite as fallback?
return LinuxNotifications
return DarwinNotifications()
elif QtDBus.QDBusConnection.sessionBus().isConnected():
return DBusNotifications()
else:
logger.warning('could not pick valid notification class')
return cls()
def deliver(self, title, text, level='info'):
"""Dummy notifier if we're not on macOS or Linux notifier isn't available."""
pass
def notifications_suppressed(self, level):
"""Decide if notification is sent or not based on settings and level."""
if not SettingsModel.get(key='enable_notifications').value:
logger.debug('notifications suppressed')
return True
if level == 'info' and not SettingsModel.get(key='enable_notifications_success').value:
logger.debug('success notifications suppressed')
return True
logger.debug('notification not suppressed')
return False
class DarwinNotifications(VortaNotifications):
def deliver(self, title, text):
if SettingsModel.get(key='enable_notifications').value:
from Foundation import NSUserNotification
from Foundation import NSUserNotificationCenter
notification = NSUserNotification.alloc().init()
notification.setTitle_(title)
notification.setInformativeText_(text)
center = NSUserNotificationCenter.defaultUserNotificationCenter()
if center is not None: # Only works when run from app bundle.
center.deliverNotification_(notification)
class LinuxNotifications(VortaNotifications):
"""
Could use the Gnome libs or the binary
https://wiki.archlinux.org/index.php/Desktop_notifications#Python
http://manpages.ubuntu.com/manpages/cosmic/man1/notify-send.1.html
Notify via notification center and pyobjc bridge.
"""
def deliver(self, title, text):
def deliver(self, title, text, level='info'):
if self.notifications_suppressed(level):
return
from Foundation import NSUserNotification, NSUserNotificationCenter
notification = NSUserNotification.alloc().init()
notification.setTitle_(title)
notification.setInformativeText_(text)
center = NSUserNotificationCenter.defaultUserNotificationCenter()
if center is not None: # Only works when run from app bundle.
return center.deliverNotification_(notification)
class DBusNotifications(VortaNotifications):
"""
Use qt-dbus to send notifications.
Adapted from http://codito.in/notifications-in-qt-over-dbus/
"""
URGENCY = {'info': 1, 'error': 2}
def __init__(self):
pass
def _dbus_notify(self, header, msg, level='info'):
item = "org.freedesktop.Notifications"
path = "/org/freedesktop/Notifications"
interface = "org.freedesktop.Notifications"
app_name = "vorta"
v = QtCore.QVariant(12321) # random int to identify all notifications
if v.convert(QtCore.QVariant.UInt):
id_replace = v
icon = ""
title = header
text = msg
actions_list = QtDBus.QDBusArgument([], QtCore.QMetaType.QStringList)
hint = {'urgency': self.URGENCY[level]}
time = 5000 # milliseconds for display timeout
bus = QtDBus.QDBusConnection.sessionBus()
notify = QtDBus.QDBusInterface(item, path, interface, bus)
if notify.isValid():
x = notify.call(QtDBus.QDBus.AutoDetect, "Notify", app_name,
id_replace, icon, title, text,
actions_list, hint, time)
if x.errorName():
logger.warning("Failed to send notification!")
logger.warning(x.errorMessage())
else:
logger.warning("Invalid dbus interface")
def deliver(self, title, text, level='info'):
if self.notifications_suppressed(level):
return
self._dbus_notify(title, text)

View File

@ -1,16 +1,17 @@
import logging
from datetime import date, timedelta
from apscheduler.schedulers.qt import QtScheduler
from apscheduler.triggers import cron
from vorta.borg.create import BorgCreateThread
from .models import BackupProfileModel, EventLogModel
from vorta.borg.prune import BorgPruneThread
from vorta.borg.list_repo import BorgListRepoThread
from vorta.borg.check import BorgCheckThread
from vorta.borg.create import BorgCreateThread
from vorta.borg.list_repo import BorgListRepoThread
from vorta.borg.prune import BorgPruneThread
from .models import BackupProfileModel, EventLogModel
from .notifications import VortaNotifications
logger = logging.getLogger('vorta')
logger = logging.getLogger(__name__)
class VortaScheduler(QtScheduler):
@ -45,7 +46,7 @@ class VortaScheduler(QtScheduler):
minute=profile.schedule_fixed_minute)
if self.get_job(job_id) is not None and trigger is not None:
self.reschedule_job(job_id, trigger=trigger)
notifier = VortaNotifications.pick()()
notifier = VortaNotifications.pick()
notifier.deliver('Vorta Scheduler', 'Background scheduler was changed.')
logger.debug('Job for profile %s was rescheduled.', profile.name)
elif trigger is not None:
@ -85,9 +86,12 @@ class VortaScheduler(QtScheduler):
return job.next_run_time.strftime('%Y-%m-%d %H:%M')
def create_backup(self, profile_id):
notifier = VortaNotifications.pick()()
notifier = VortaNotifications.pick()
profile = BackupProfileModel.get(id=profile_id)
logger.info('Starting background backup for %s', profile.name)
notifier.deliver('Vorta Backup', 'Starting background backup for %s.' % profile.name, level='info')
msg = BorgCreateThread.prepare(profile)
if msg['ok']:
logger.info('Preparation for backup successful.')
@ -95,14 +99,16 @@ class VortaScheduler(QtScheduler):
thread.start()
thread.wait()
if thread.process.returncode in [0, 1]:
notifier.deliver('Vorta Backup', 'Backup successful for %s.' % profile.name, level='info')
logger.info('Backup creation successful.')
self.post_backup_tasks(profile_id)
else:
notifier.deliver('Vorta Backup', 'Error during backup creation.')
notifier.deliver('Vorta Backup', 'Error during backup creation.', level='error')
logger.error('Error during backup creation.')
else:
logger.error('Conditions for backup not met. Aborting.')
logger.error(msg['message'])
notifier.deliver('Vorta Backup', msg['message'])
notifier.deliver('Vorta Backup', msg['message'], level='error')
def post_backup_tasks(self, profile_id):
"""

View File

@ -0,0 +1,20 @@
import sys
import pytest
from PyQt5 import QtDBus
import vorta.borg
import vorta.models
import vorta.notifications
@pytest.mark.skipif(sys.platform != 'linux', reason="DBus notifications only on Linux")
def test_linux_background_notifications(app, mocker):
"""We can't see notifications, but we watch for exceptions and errors."""
notifier = vorta.notifications.VortaNotifications.pick()
assert isinstance(notifier, vorta.notifications.DBusNotifications)
notifier.deliver('Vorta Test', 'test notification', level='error')
mocker.spy(QtDBus.QDBusInterface, 'call')
notifier.deliver('Vorta Test', 'test notification', level='info') # fails if called.
assert QtDBus.QDBusInterface.call.call_count == 0