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)