From 843412d73e7af8b91524a63d889e99f5cb9af149 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20I=C3=B1iguez=20Goia?= Date: Fri, 7 Jan 2022 18:32:17 +0100 Subject: [PATCH] ui, notifications: allow to use system notifications WIP. Until now we used Qt's systray notifications. They couldn't be disabled and didn't integrate well with non-Qt based Desktop Environments. Also we depended on the system tray availability, which is not always available (i3, phosh, ...). Now the user can choose to use Qt's notifications, the system notification service or disabled them completely. Pros: - The notification style is defined by the Desktop Environment. - Can be configured globally from the system settings. - In many DEs, the notifications are grouped into a single view. So if you miss any event, you can go there and check out what happened. - Now we can display notifications on DEs where we couldn't before. - It's a standard supported by major DEs. Cons: - Sometimes we can't connect to the D-Bus mainloop instance. We need to investigate it. TODO: - Deny/Allow new outgoing connections from the notifications, replacing the current pop-ups. Requested here: #468 , #476 and #477 . --- ui/opensnitch/config.py | 9 +- ui/opensnitch/dialogs/preferences.py | 35 +++- ui/opensnitch/notifications.py | 129 +++++++++++++++ ui/opensnitch/res/preferences.ui | 229 +++++++++++++++++++++------ ui/opensnitch/service.py | 82 ++++++---- 5 files changed, 402 insertions(+), 82 deletions(-) create mode 100644 ui/opensnitch/notifications.py diff --git a/ui/opensnitch/config.py b/ui/opensnitch/config.py index e9c4195a08..b963c4e46b 100644 --- a/ui/opensnitch/config.py +++ b/ui/opensnitch/config.py @@ -60,6 +60,11 @@ class Config: DEFAULT_DB_MAX_DAYS = "database/max_days" DEFAULT_DB_PURGE_INTERVAL = "database/purge_interval" + NOTIFICATIONS_ENABLED = "notifications/enabled" + NOTIFICATIONS_TYPE = "notifications/type" + NOTIFICATION_TYPE_SYSTEM = 0 + NOTIFICATION_TYPE_QT = 1 + STATS_GEOMETRY = "statsDialog/geometry" STATS_LAST_TAB = "statsDialog/last_tab" @@ -123,8 +128,8 @@ def setSettings(self, path, value): def getSettings(self, path): return self.settings.value(path) - def getBool(self, path): - return self.settings.value(path, False, type=bool) + def getBool(self, path, default_value=False): + return self.settings.value(path, type=bool, defaultValue=default_value) def getInt(self, path, default_value=0): try: diff --git a/ui/opensnitch/dialogs/preferences.py b/ui/opensnitch/dialogs/preferences.py index 52dec7c6c8..1d2b37c3d2 100644 --- a/ui/opensnitch/dialogs/preferences.py +++ b/ui/opensnitch/dialogs/preferences.py @@ -10,6 +10,7 @@ from opensnitch.nodes import Nodes from opensnitch.database import Database from opensnitch.utils import Message, QuickHelp +from opensnitch.notifications import DesktopNotifications from opensnitch import ui_pb2 @@ -57,14 +58,16 @@ def __init__(self, parent=None): self.cmdDBMaxDaysDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBMaxDays, self.REST)) self.cmdDBPurgesUp.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBPurgeInterval, self.SUM)) self.cmdDBPurgesDown.clicked.connect(lambda: self._cb_cmd_spin_clicked(self.spinDBPurgeInterval, self.REST)) - self.helpButton.setToolTipDuration(10 * 1000) + self.cmdTestNotifs.clicked.connect(self._cb_test_notifs_clicked) + self.helpButton.setToolTipDuration(30 * 1000) if QtGui.QIcon.hasThemeIcon("emblem-default") == False: self.applyButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogApplyButton"))) self.cancelButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogCloseButton"))) self.acceptButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogSaveButton"))) self.dbFileButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DirOpenIcon"))) - if QtGui.QIcon.hasThemeIcon("zoom-in") == False: + + if QtGui.QIcon.hasThemeIcon("list-add") == False: self.cmdTimeoutUp.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowUp"))) self.cmdTimeoutDown.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowDown"))) self.cmdDBMaxDaysUp.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ArrowUp"))) @@ -76,6 +79,7 @@ def showEvent(self, event): super(PreferencesDialog, self).showEvent(event) try: + self._settingsSaved = False self._reset_status_message() self._hide_status_label() self.comboNodes.clear() @@ -143,6 +147,14 @@ def _load_settings(self): self.dstPortCheck.setChecked(self._cfg.getBool(self._cfg.DEFAULT_POPUP_ADVANCED_DSTPORT)) self.uidCheck.setChecked(self._cfg.getBool(self._cfg.DEFAULT_POPUP_ADVANCED_UID)) + self.groupNotifs.setChecked(self._cfg.getBool(Config.NOTIFICATIONS_ENABLED)) + self.radioSysNotifs.setChecked( + True if self._cfg.getInt(Config.NOTIFICATIONS_TYPE) == Config.NOTIFICATION_TYPE_SYSTEM else False + ) + self.radioQtNotifs.setChecked( + True if self._cfg.getInt(Config.NOTIFICATIONS_TYPE) == Config.NOTIFICATION_TYPE_QT else False + ) + self.dbType = self._cfg.getInt(self._cfg.DEFAULT_DB_TYPE_KEY) self.comboDBType.setCurrentIndex(self.dbType) if self.comboDBType.currentIndex() != Database.DB_TYPE_MEMORY: @@ -285,7 +297,7 @@ def _save_settings(self): self._node_needs_update = False self.saved.emit() - + self._settingsSaved = True def _save_db_config(self): dbtype = self.comboDBType.currentIndex() @@ -339,6 +351,11 @@ def _save_ui_config(self): self._cfg.setSettings(self._cfg.DEFAULT_POPUP_ADVANCED_DSTIP, bool(self.dstIPCheck.isChecked())) self._cfg.setSettings(self._cfg.DEFAULT_POPUP_ADVANCED_DSTPORT, bool(self.dstPortCheck.isChecked())) self._cfg.setSettings(self._cfg.DEFAULT_POPUP_ADVANCED_UID, bool(self.uidCheck.isChecked())) + + self._cfg.setSettings(self._cfg.NOTIFICATIONS_ENABLED, bool(self.groupNotifs.isChecked())) + self._cfg.setSettings(self._cfg.NOTIFICATIONS_TYPE, + int(Config.NOTIFICATION_TYPE_SYSTEM if self.radioSysNotifs.isChecked() else Config.NOTIFICATION_TYPE_QT)) + # this is a workaround for not display pop-ups. # see #79 for more information. if self.popupsCheck.isChecked(): @@ -444,7 +461,8 @@ def _cb_db_type_changed(self): def _cb_accept_button_clicked(self): self.accept() - self._save_settings() + if not self._settingsSaved: + self._save_settings() def _cb_apply_button_clicked(self): self._save_settings() @@ -456,7 +474,8 @@ def _cb_help_button_clicked(self): QuickHelp.show( QC.translate("preferences", "Hover the mouse over the texts to display the help

Don't forget to visit the wiki: {0}" - ).format(Config.HELP_URL)) + ).format(Config.HELP_URL) + ) def _cb_popups_check_toggled(self, checked): self.spinUITimeout.setEnabled(not checked) @@ -480,3 +499,9 @@ def _cb_cmd_spin_clicked(self, spinWidget, operation): spinWidget.setValue(spinWidget.value() + 1) else: spinWidget.setValue(spinWidget.value() - 1) + + def _cb_test_notifs_clicked(self): + if self.radioSysNotifs.isChecked(): + DesktopNotifications().show("title", "body") + else: + pass diff --git a/ui/opensnitch/notifications.py b/ui/opensnitch/notifications.py new file mode 100644 index 0000000000..daa4ee1c6c --- /dev/null +++ b/ui/opensnitch/notifications.py @@ -0,0 +1,129 @@ +#!/usr/bin/python + +from PyQt5.QtCore import QCoreApplication as QC +import os +from utils import Utils +from opensnitch.config import Config + +class DesktopNotifications(): + """DesktopNotifications display informative pop-ups using the system D-Bus. + The notifications are handled and configured by the system. + + The notification daemon also decides where to show the notifications, as well + as how to group them. + + The body of a notification supports markup (if the implementation supports it): + https://people.gnome.org/~mccann/docs/notification-spec/notification-spec-latest.html#markup + Basically: , , , and . New lines can be added with the regular \n. + + It also support actions (buttons). + + https://notify2.readthedocs.io/en/latest/ + """ + + _cfg = Config.init() + + # list of hints: + # https://people.gnome.org/~mccann/docs/notification-spec/notification-spec-latest.html#hints + HINT_DESKTOP_ENTRY = "desktop-entry" + CATEGORY_NETWORK = "network" + + EXPIRES_DEFAULT = 0 + NEVER_EXPIRES = -1 + + def __init__(self): + self.ACTION_ALLOW = QC.translate("popups", "Allow") + self.ACTION_DENY = QC.translate("popups", "Deny") + self.IS_LIBNOTIFY_AVAILABLE = True + self.DOES_SUPPORT_ACTIONS = True + + try: + import notify2 + self.ntf2 = notify2 + mloop = 'glib' + + # First try to initialise the D-Bus connection with the given + # mainloop. + # If it fails, we'll try to initialise it without it. + try: + self.ntf2.init("opensnitch", mainloop=mloop) + except Exception: + self.DOES_SUPPORT_ACTIONS = False + + # usually because dbus mainloop is not initiated, specially + # with 'qt' + # FIXME: figure out how to init it, or how to connect to an + # existing session. + print("DesktopNotifications(): system doesn't support actions. Available capabilities:") + print(self.ntf2.get_server_caps()) + + self.ntf2.init("opensnitch") + + # Example: ['actions', 'action-icons', 'body', 'body-markup', 'icon-static', 'persistence', 'sound'] + if ('actions' not in self.ntf2.get_server_caps()): + self.DOES_SUPPORT_ACTIONS = False + + except Exception as e: + print("DesktopNotifications not available (install python3-notify2):", e) + self.IS_LIBNOTIFY_AVAILABLE = False + + def is_available(self): + return self.IS_LIBNOTIFY_AVAILABLE and self._cfg.getBool(Config.NOTIFICATIONS_ENABLED) + + def support_actions(self): + """Returns true if the notifications daemon support actions(buttons). + This depends on 2 factors: + - If the notification server actually supports it (get_server_caps()). + - If there's a dbus instance running. + """ + return self.DOES_SUPPORT_ACTIONS + + def show(self, title, body, icon="dialog-information"): + ntf = self.ntf2.Notification(title, body, icon) + + # timeouts seems to be ignored (on Cinnamon at least) + timeout = self._cfg.getInt(Config.DEFAULT_TIMEOUT_KEY, 15) + ntf.set_timeout(timeout) + ntf.timeout = timeout + + ntf.set_category(self.CATEGORY_NETWORK) + # used to display our app icon an name. + ntf.set_hint(self.HINT_DESKTOP_ENTRY, "opensnitch_ui") + ntf.show() + + # TODO: + # - construct a rule with the default configured parameters. + # - create a common dialogs/prompt.py:_send_rule(), maybe in utils.py + def ask(self, connection, timeout, callback): + c = connection + title = QC.translate("popups", "New outgoing connection") + body = """ + {0} + + {1} {2}:{3} -> {4}:{5} + UID: {6} PID: {7} + """.format(c.process_path, c.protocol.upper(), + c.src_port, c.src_ip, + c.dst_host if c.dst_host != "" else c.dst_ip, c.dst_port, + c.user_id, c.process_id + ) + ntf = self.ntf2.Notification(title, body, "dialog-warning") + timeout = self._cfg.getInt(Config.DEFAULT_TIMEOUT_KEY, 15) + ntf.set_timeout(timeout * 1000) + ntf.timeout = timeout * 1000 + if self.DOES_SUPPORT_ACTIONS: + ntf.set_urgency(self.ntf2.URGENCY_CRITICAL) + ntf.add_action("allow", self.ACTION_ALLOW, callback, connection) + ntf.add_action("deny", self.ACTION_DENY, callback, connection) + #ntf.add_action("open-gui", QC.translate("popups", "View"), callback, connection) + ntf.set_category(self.CATEGORY_NETWORK) + ntf.set_hint(self.HINT_DESKTOP_ENTRY, "opensnitch_ui") + ntf.show() + + @staticmethod + def areEnabled(): + """Return if notifications are enabled. + + Default: True + """ + return DesktopNotifications._cfg.getBool(DesktopNotifications._cfg.NOTIFICATIONS_ENABLED, True) diff --git a/ui/opensnitch/res/preferences.ui b/ui/opensnitch/res/preferences.ui index 9a99a27d9d..cb04a653ea 100644 --- a/ui/opensnitch/res/preferences.ui +++ b/ui/opensnitch/res/preferences.ui @@ -41,6 +41,12 @@ + + + 0 + 0 + + @@ -124,7 +130,7 @@ - ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup + ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup @@ -133,7 +139,7 @@ - ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup + ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup @@ -385,7 +391,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup true @@ -424,7 +430,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup true @@ -436,7 +442,7 @@ - + 0 0 @@ -449,10 +455,10 @@ - + - Disable pop-ups, only display an alert + Disable pop-ups, only display an notification true @@ -480,14 +486,117 @@ - + + + + <html><head/><body><p>When this option is selected, the rules of the selected duration won't be added to the list of temporary rules in the GUI.</p><p><br/></p><p>Temporary rules will still be valid, and you can use them when prompted to allow/deny a new connection.</p></body></html> + + + Don't save rules of duration + + + + + + + + 0 + 0 + + + + Desktop notifications + + + true + + + true + + + + + + + 0 + 0 + + + + Use system notifications + + + true + + + + + + + + 0 + 0 + + + + Use Qt notifications + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 20 + 20 + + + + + + + + Test + + + + + + + + + + + + 0 + 0 + + Events tab columns + + + 0 + 0 + + Time @@ -496,60 +605,96 @@ - - + + + + + 0 + 0 + + - Destination + Rule true - - + + + + + 0 + 0 + + - Protocol + Node true - - + + + + + 0 + 0 + + - Process + Protocol true - - + + + + + 0 + 0 + + - Rule + Action true - - + + + + + 0 + 0 + + - Node + Destination true - - + + + + + 0 + 0 + + - Action + Process true @@ -559,16 +704,6 @@ - - - - <html><head/><body><p>When this option is selected, the rules of the selected duration won't be added to the list of temporary rules in the GUI.</p><p><br/></p><p>Temporary rules will still be valid, and you can use them when prompted to allow/deny a new connection.</p></body></html> - - - Don't save rules of duration - - - @@ -771,7 +906,7 @@ - ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup + ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup @@ -780,7 +915,7 @@ - ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup + ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup @@ -966,7 +1101,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup @@ -1030,7 +1165,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup true @@ -1116,7 +1251,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup true @@ -1136,7 +1271,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup true @@ -1156,7 +1291,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup true @@ -1184,7 +1319,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup true @@ -1211,7 +1346,7 @@ - ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup + ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup @@ -1222,7 +1357,7 @@ - .. + ../../../../../../../../../../.designer/backup../../../../../../../../../../.designer/backup @@ -1233,7 +1368,7 @@ - ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup + ../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../../.designer/backup diff --git a/ui/opensnitch/service.py b/ui/opensnitch/service.py index d9455cc4a2..63cb88169c 100644 --- a/ui/opensnitch/service.py +++ b/ui/opensnitch/service.py @@ -16,6 +16,7 @@ from opensnitch.dialogs.prompt import PromptDialog from opensnitch.dialogs.stats import StatsDialog +from opensnitch.notifications import DesktopNotifications from opensnitch.nodes import Nodes from opensnitch.config import Config from opensnitch.version import version @@ -30,7 +31,7 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject): _version_warning_trigger = QtCore.pyqtSignal(str, str) _status_change_trigger = QtCore.pyqtSignal(bool) _notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply) - _show_message_trigger = QtCore.pyqtSignal(str, str, int, int) + _show_message_trigger = QtCore.pyqtSignal(str, str, int) # .desktop filename located under /usr/share/applications/ DESKTOP_FILENAME = "opensnitch_ui.desktop" @@ -69,10 +70,6 @@ def __init__(self, app, on_exit): QtWidgets.QMessageBox.Warning) sys.exit(-1) - self._cleaner = None - if self._cfg.getBool(Config.DEFAULT_DB_PURGE_OLDEST): - self._start_db_cleaner() - self._db_sqlite = self._db.get_db() self._last_ping = None self._version_warning_shown = False @@ -88,6 +85,7 @@ def __init__(self, app, on_exit): self._remote_lock = Lock() self._remote_stats = {} + self._desktop_notifications = DesktopNotifications() self._setup_interfaces() self._setup_icons() self._stats_dialog = StatsDialog(dbname="general", db=self._db) @@ -105,6 +103,12 @@ def __init__(self, app, on_exit): 'users':{} } + self._show_gui_if_tray_not_available() + + self._cleaner = None + if self._cfg.getBool(Config.DEFAULT_DB_PURGE_OLDEST): + self._start_db_cleaner() + # https://gist.github.com/pklaus/289646 def _setup_interfaces(self): namestr, outbytes = Utils.get_interfaces() @@ -146,11 +150,12 @@ def _setup_icons(self): self._app.setWindowIcon(self.white_icon) self._app.setDesktopFileName(self.DESKTOP_FILENAME) - self._prompt_dialog.setWindowIcon(self.white_icon) def _setup_tray(self): - self._menu = QtWidgets.QMenu() self._tray = QtWidgets.QSystemTrayIcon(self.off_icon) + self._tray.show() + + self._menu = QtWidgets.QMenu() self._tray.setContextMenu(self._menu) self._tray.activated.connect(self._on_tray_icon_activated) @@ -163,9 +168,6 @@ def _setup_tray(self): ) self._menu.addAction(self.MENU_ENTRY_CLOSE).triggered.connect(self._on_close) - self._tray.show() - self._show_gui_if_tray_not_available() - def _show_gui_if_tray_not_available(self): """If the system tray is not available or ready, show the GUI after 10s. This delay helps to skip showing up the GUI when DEs' autologin is on. @@ -260,11 +262,17 @@ def _on_notification_reply(self, reply): def _on_remote_stats_menu(self, address): self._remote_stats[address]['dialog'].show() - @QtCore.pyqtSlot(str, str, int, int) - def _show_systray_message(self, title, body, icon, timeout): + @QtCore.pyqtSlot(str, str, int) + def _show_systray_message(self, title, body, icon): + if DesktopNotifications.areEnabled(): + if self._desktop_notifications.is_available() and self._cfg.getInt(Config.NOTIFICATIONS_TYPE) == Config.NOTIFICATION_TYPE_SYSTEM: + self._desktop_notifications.show(title, body, os.path.join(self._path, "res/icon-white.svg")) + else: + timeout = self._cfg.getInt(Config.DEFAULT_TIMEOUT_KEY, 15) + self._tray.showMessage(title, body, icon, timeout) + if icon == QtWidgets.QSystemTrayIcon.NoIcon: self._tray.setIcon(self.alert_icon) - self._tray.showMessage(title, body, icon, timeout) def _on_enable_interception_clicked(self): self._enable_interception(self._fw_enabled) @@ -564,6 +572,12 @@ def Ping(self, request, context): return ui_pb2.PingReply(id=request.id) def AskRule(self, request, context): + #def callback(ntf, action, connection): + # TODO + + #if self._desktop_notifications.support_actions(): + # self._desktop_notifications.ask(request, callback) + # TODO: allow connections originated from ourselves: os.getpid() == request.pid) self._asking = True proto, addr = self._get_peer(context.peer()) @@ -576,18 +590,29 @@ def AskRule(self, request, context): node_text = "" if self._is_local_request(proto, addr) else "on node {0}:{1}".format(proto, addr) self._show_message_trigger.emit(_title, - "{0} action applied {1}\nArguments: {2}" - .format(rule.action, node_text, request.process_args), - QtWidgets.QSystemTrayIcon.NoIcon, - 0) + "{0} action applied {1}\nCommand line: {2}" + .format(rule.action, node_text, " ".join(request.process_args)), + QtWidgets.QSystemTrayIcon.NoIcon) self._last_ping = datetime.now() self._asking = False if rule.duration in Config.RULES_DURATION_FILTER: - self._node_actions_trigger.emit({'action': self.DELETE_RULE, 'name': rule.name, 'addr': context.peer()}) + self._node_actions_trigger.emit( + { + 'action': self.DELETE_RULE, + 'name': rule.name, + 'addr': context.peer() + } + ) else: - self._node_actions_trigger.emit({'action': self.ADD_RULE, 'peer': context.peer(), 'rule': rule}) + self._node_actions_trigger.emit( + { + 'action': self.ADD_RULE, + 'peer': context.peer(), + 'rule': rule + } + ) return rule @@ -599,18 +624,20 @@ def Subscribe(self, node_config, context): @doc: https://grpc.github.io/grpc/python/grpc.html#service-side-context """ try: - proto, addr = self._get_peer(context.peer()) - if self._is_local_request(proto, addr) == False: - self._show_message_trigger.emit("New node connected", - "({0})".format(context.peer()), - QtWidgets.QSystemTrayIcon.Information, - 5000) - self._node_actions_trigger.emit({ 'action': self.NODE_ADD, 'peer': context.peer(), 'node_config': node_config }) + # force events processing, to add the node ^ before the + # Notifications() call arrives. + QtWidgets.QApplication.processEvents() + + proto, addr = self._get_peer(context.peer()) + if self._is_local_request(proto, addr) == False: + self._show_message_trigger.emit("New node connected", + "({0})".format(context.peer()), + QtWidgets.QSystemTrayIcon.Information) except Exception as e: print("[Notifications] exception adding new node:", e) context.cancel() @@ -650,8 +677,7 @@ def _on_client_closed(): if self._is_local_request(proto, addr) == False: self._show_message_trigger.emit("node exited", "({0})".format(context.peer()), - QtWidgets.QSystemTrayIcon.Information, - 5000) + QtWidgets.QSystemTrayIcon.Information) context.add_callback(_on_client_closed)