ui: allow to stop/start interception from systray

- removed unused import time.
- allow to pause/start interception from tray contextual menu.
- improved case when the daemon is in paused state, and the GUI is
  launched.

There's more work yet to do to improve the states when there're several
nodes connected.

closes #398
This commit is contained in:
Gustavo Iñiguez Goia 2021-04-17 22:09:16 +02:00
parent 23b61f5341
commit 966342cbbc
3 changed files with 140 additions and 40 deletions

View file

@ -225,7 +225,7 @@ func acceptOrDeny(packet *netfilter.Packet, con *conman.Connection) *rule.Rule {
// 1) connected and running and 2) we are not already asking
if uiClient.Connected() == false || uiClient.GetIsAsking() == true {
applyDefaultAction(packet)
log.Debug("UI is not running or busy")
log.Debug("UI is not running or busy, connected: %v, running: %v", uiClient.Connected(), uiClient.GetIsAsking())
return nil
}
@ -379,6 +379,9 @@ func main() {
}
repeatPktChan = repeatQueue.Packets()
// queue is ready, run firewall rules
firewall.Init(&queueNum)
uiClient = ui.NewClient(uiSocket, stats, rules)
if overwriteLogging() {
setupLogging()
@ -390,9 +393,6 @@ func main() {
}
monitor.Init()
// queue is ready, run firewall rules
firewall.Init(&queueNum)
log.Info("Running on netfilter queue #%d ...", queueNum)
for {
select {

View file

@ -919,14 +919,13 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
self.startButton.setIcon(self.iconStart)
return
self.update_interception_status(self.startButton.isChecked())
self._status_changed_trigger.emit(self.startButton.isChecked())
if self.startButton.isChecked():
self._update_status_label(running=True, text=self.FIREWALL_RUNNING)
nid, noti = self._nodes.start_interception(_callback=self._notification_callback)
self._status_changed_trigger.emit(False)
else:
self._update_status_label(running=False, text=self.FIREWALL_DISABLED)
nid, noti = self._nodes.stop_interception(_callback=self._notification_callback)
self._status_changed_trigger.emit(True)
self._notifications_sent[nid] = noti
@ -1382,6 +1381,14 @@ class StatsDialog(QtWidgets.QDialog, uic.loadUiType(DIALOG_UI_PATH)[0]):
return tableWidget
def update_interception_status(self, enabled):
self.startButton.setDown(enabled)
self.startButton.setChecked(enabled)
if enabled:
self._update_status_label(running=True, text=self.FIREWALL_RUNNING)
else:
self._update_status_label(running=False, text=self.FIREWALL_DISABLED)
# launched from a thread
def update(self, is_local=True, stats=None, need_query_update=True):
# lock mandatory when there're multiple clients

View file

@ -3,7 +3,6 @@ from PyQt5 import QtWidgets, QtGui, QtCore
from datetime import datetime, timedelta
from threading import Thread, Lock, Event
import grpc
import time
import os
import socket
import fcntl
@ -30,7 +29,13 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
_new_remote_trigger = QtCore.pyqtSignal(str, ui_pb2.PingRequest)
_update_stats_trigger = QtCore.pyqtSignal(str, str, ui_pb2.PingRequest)
_version_warning_trigger = QtCore.pyqtSignal(str, str)
_status_change_trigger = QtCore.pyqtSignal()
_notification_callback = QtCore.pyqtSignal(ui_pb2.NotificationReply)
MENU_ENTRY_STATS = QtCore.QCoreApplication.translate("contextual_menu", "Statistics")
MENU_ENTRY_FW_ENABLE = QtCore.QCoreApplication.translate("contextual_menu", "Enable")
MENU_ENTRY_FW_DISABLE = QtCore.QCoreApplication.translate("contextual_menu", "Disable")
MENU_ENTRY_HELP = QtCore.QCoreApplication.translate("contextual_menu", "Help")
MENU_ENTRY_CLOSE = QtCore.QCoreApplication.translate("contextual_menu", "Close")
def __init__(self, app, on_exit):
super(UIService, self).__init__()
@ -46,6 +51,7 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._version_warning_shown = False
self._asking = False
self._connected = False
self._fw_enabled = False
self._path = os.path.abspath(os.path.dirname(__file__))
self._app = app
self._on_exit = on_exit
@ -94,7 +100,6 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
# https://stackoverflow.com/questions/40288921/pyqt-after-messagebox-application-quits-why
self._app.setQuitOnLastWindowClosed(False)
self._version_warning_trigger.connect(self._on_diff_versions)
self._status_change_trigger.connect(self._on_status_change)
self._new_remote_trigger.connect(self._on_new_remote)
self._update_stats_trigger.connect(self._on_update_stats)
self._stats_dialog._shown_trigger.connect(self._on_stats_dialog_shown)
@ -122,23 +127,24 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
def _setup_tray(self):
self._menu = QtWidgets.QMenu()
self._stats_action = self._menu.addAction(QtCore.QCoreApplication.translate("contextual_menu","Statistics"))
self._tray = QtWidgets.QSystemTrayIcon(self.off_icon)
self._tray.setContextMenu(self._menu)
self._tray.activated.connect(self._on_tray_icon_activated)
self._menu.addAction(QtCore.QCoreApplication.translate("contextual_menu", "Help")).triggered.connect(
self._menu.addAction(self.MENU_ENTRY_STATS).triggered.connect(self._show_stats_dialog)
self._menu_enable_fw = self._menu.addAction(self.MENU_ENTRY_FW_DISABLE)
self._menu_enable_fw.setEnabled(False)
self._menu_enable_fw.triggered.connect(self._on_enable_interception_clicked)
self._menu.addAction(self.MENU_ENTRY_HELP).triggered.connect(
lambda: QtGui.QDesktopServices.openUrl(QtCore.QUrl(Config.HELP_URL))
)
self._stats_action.triggered.connect(self._show_stats_dialog)
self._menu.addAction(QtCore.QCoreApplication.translate("contextual_menu", "Close")).triggered.connect(self._on_close)
self._menu.addAction(self.MENU_ENTRY_CLOSE).triggered.connect(self._on_close)
self._tray.show()
if not self._tray.isSystemTrayAvailable():
self._stats_dialog.show()
def _on_tray_icon_activated(self, reason):
if reason == QtWidgets.QSystemTrayIcon.Trigger or reason == QtWidgets.QSystemTrayIcon.MiddleClick:
if self._stats_dialog.isVisible() and not self._stats_dialog.isMinimized():
@ -157,25 +163,13 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
self._on_exit()
def _show_stats_dialog(self):
if self._connected:
if self._connected and not self._fw_enabled:
self._tray.setIcon(self.white_icon)
self._stats_dialog.show()
@QtCore.pyqtSlot(bool)
def _on_stats_status_changed(self, paused):
if paused:
self._tray.setIcon(self.pause_icon)
else:
self._tray.setIcon(self.white_icon)
@QtCore.pyqtSlot()
def _on_status_change(self):
self._stats_dialog.daemon_connected = self._connected
self._stats_dialog.update_status()
if self._connected:
self._tray.setIcon(self.white_icon)
else:
self._tray.setIcon(self.off_icon)
def _on_stats_status_changed(self, enabled):
self._update_fw_status(enabled)
@QtCore.pyqtSlot(str, str)
def _on_diff_versions(self, daemon_ver, ui_ver):
@ -207,13 +201,95 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
@QtCore.pyqtSlot()
def _on_stats_dialog_shown(self):
if self._connected:
self._tray.setIcon(self.white_icon)
if self._fw_enabled:
self._tray.setIcon(self.white_icon)
else:
self._tray.setIcon(self.pause_icon)
else:
self._tray.setIcon(self.off_icon)
@QtCore.pyqtSlot(ui_pb2.NotificationReply)
def _on_notification_reply(self, reply):
if reply.code == ui_pb2.ERROR:
self._tray.showMessage("Error",
reply.data,
QtWidgets.QSystemTrayIcon.Information,
5000)
def _on_remote_stats_menu(self, address):
self._remote_stats[address]['dialog'].show()
def _on_enable_interception_clicked(self):
self._enable_interception(self._fw_enabled)
def _update_fw_status(self, enabled):
"""_update_fw_status updates the status of the menu entry
to disable or enable the firewall of the daemon.
"""
if self._connected == False:
return
self._stats_dialog.update_interception_status(enabled)
if enabled:
self._tray.setIcon(self.white_icon)
self._menu_enable_fw.setText(self.MENU_ENTRY_FW_DISABLE)
else:
self._tray.setIcon(self.pause_icon)
self._menu_enable_fw.setText(self.MENU_ENTRY_FW_ENABLE)
self._fw_enabled = enabled
def _set_daemon_connected(self, connected):
"""_set_daemon_connected only updates the connection status of the daemon(s),
regardless if the fw is enabled or not.
There're 3 states:
- daemon connected
- daemon not connected
- daemon connected and firewall enabled/disabled
"""
self._stats_dialog.daemon_connected = connected
self._connected = connected
# if there're more than 1 node, override connection status
if self._nodes.count() >= 1:
self._connected = True
self._stats_dialog.daemon_connected = True
if self._nodes.count() == 1:
self._menu_enable_fw.setEnabled(True)
if self._nodes.count() == 0 or self._nodes.count() > 1:
self._menu_enable_fw.setEnabled(False)
self._stats_dialog.update_status()
if self._connected:
self._tray.setIcon(self.white_icon)
else:
self._fw_enabled = False
self._tray.setIcon(self.off_icon)
def _enable_interception(self, enable):
if self._connected == False:
return
if self._nodes.count() == 0:
self._tray.showMessage("No nodes connected",
"",
QtWidgets.QSystemTrayIcon.Information,
5000)
return
if self._nodes.count() > 1:
print("enable interception for all nodes not supported yet")
return
if enable:
nid, noti = self._nodes.stop_interception(_callback=self._notification_callback)
else:
nid, noti = self._nodes.start_interception(_callback=self._notification_callback)
self._fw_enabled = not enable
self._stats_dialog._status_changed_trigger.emit(not enable)
def _check_versions(self, daemon_version):
lMayor, lMinor, lPatch = version.split(".")
rMayor, rMinor, rPatch = daemon_version.split(".")
@ -435,6 +511,16 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
QtWidgets.QSystemTrayIcon.Information,
5000)
n = self._nodes.add(context, node_config)
if n != None:
self._set_daemon_connected(True)
# if there're more than one node, we can't update the status
# based on the fw status, only if the daemon is running or not
if self._nodes.count() <= 1:
self._update_fw_status(node_config.isFirewallRunning)
else:
self._update_fw_status(True)
except Exception as e:
print("[Notifications] exception adding new node:", e)
context.cancel()
@ -457,17 +543,24 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
stop_event.set()
self._delete_node(context.peer())
if self._nodes.count() == 0:
self._connected = False
self._status_change_trigger.emit()
self._set_daemon_connected(False)
# TODO: handle the situation when a node disconnects, and the
# remaining node has the fw disabled.
#if self._nodes.count() == 1:
# nd = self._nodes.get_nodes()
# if nd[0].get_config().isFirewallRunning:
if self._is_local_request(proto, addr) == False:
self._tray.showMessage("node exited",
"({0})".format(context.peer()),
QtWidgets.QSystemTrayIcon.Information,
5000)
context.add_callback(_on_client_closed)
# TODO: move to notifications.py
def new_node_message():
print("new node connected, listening for client responses...", addr)
self._connected = True
self._status_change_trigger.emit()
while self._exit == False:
try:
@ -481,9 +574,9 @@ class UIService(ui_pb2_grpc.UIServicer, QtWidgets.QGraphicsObject):
print("[Notifications] Node {0} exited".format(addr))
break
except grpc.RpcError as e:
print("[Notifications] grpc exception new_node_message(): ", addr)
print("[Notifications] grpc exception new_node_message(): ", addr, in_message)
except Exception as e:
print("[Notifications] unexpected exception new_node_message(): ", addr, e)
print("[Notifications] unexpected exception new_node_message(): ", addr, e, in_message)
read_thread = Thread(target=new_node_message)
read_thread.daemon = True