fix: DNS coupée après arrêt du tunnel, gel UI, dialogues sudo à répétition
- wg-quick down échoue silencieusement si l'interface a déjà disparu (crash, kill, veille) et ne retire donc jamais l'entrée DNS posée par wg-quick up : la résolution reste pointée sur un résolveur mort. Ajout d'un nettoyage DNS inconditionnel (app/core/dns.py) déclenché à la connexion, la déconnexion, la fermeture et au démarrage suivant. - is_connected() se basait sur `wg show`, qui rend un code 0 même en cas d'échec de permission pour un utilisateur non-root : l'app se croyait déconnectée en permanence et ne démontait donc jamais le tunnel à la fermeture. Détection réécrite via sysfs (sans privilèges). - Connexion/déconnexion et ping tournaient sur le thread Qt : gel de l'UI pendant les élévations de privilèges. Déportés dans des QThread (app/ui/worker.py). - Restauration réseau garantie sur toutes les sorties (croix, tray, SIGINT/SIGTERM, atexit, exception) via app/core/shutdown.py. - run_privileged() : la détection "sudo veut un mot de passe" ratait en session non-anglophone et sur les comptes ALL=(ALL) ALL ; réécrite pour se fier au message d'erreur réel plutôt qu'à `sudo -l`. - Écriture de la config WireGuard dans /etc/wireguard (root:root 0700) passe maintenant par install -D via la chaîne de privilèges complète au lieu d'un tee non autorisé par les sudoers. - Ajout d'un diagnostic des droits (make check-privileges, bouton Admin) et mise à jour de setup-sudoers pour couvrir toutes les commandes désormais utilisées.
This commit is contained in:
@@ -9,6 +9,7 @@ from PyQt6.QtGui import QFont
|
||||
|
||||
from app.core.config import Config
|
||||
from app.core import wireguard as wg_core
|
||||
from app.core import dns as dns_util
|
||||
from app.core import mfa as mfa_core
|
||||
|
||||
# CSS sombre commun à tous les onglets
|
||||
@@ -465,6 +466,13 @@ class AdminWindow(QDialog):
|
||||
btn_dns = QPushButton("🔍 Analyser le DNS")
|
||||
btn_dns.clicked.connect(self._run_dns_test)
|
||||
g2.addWidget(btn_dns)
|
||||
btn_dns_fix = QPushButton("🛠️ Réparer le DNS (tunnel arrêté)")
|
||||
btn_dns_fix.setToolTip(
|
||||
"Retire la configuration DNS laissée par un tunnel arrêté "
|
||||
"brutalement, qui empêche toute résolution de noms."
|
||||
)
|
||||
btn_dns_fix.clicked.connect(self._repair_dns)
|
||||
g2.addWidget(btn_dns_fix)
|
||||
self._dns_result = QLabel("")
|
||||
self._dns_result.setWordWrap(True)
|
||||
self._dns_result.setStyleSheet(
|
||||
@@ -472,6 +480,24 @@ class AdminWindow(QDialog):
|
||||
)
|
||||
g2.addWidget(self._dns_result)
|
||||
lay.addWidget(grp2)
|
||||
|
||||
# Droits d'exécution
|
||||
grp3 = QGroupBox("🔐 Droits d'exécution")
|
||||
g3 = QVBoxLayout(grp3)
|
||||
g3.addWidget(QLabel(
|
||||
"Sans règle sudo, chaque connexion ouvre un dialogue d'authentification."
|
||||
))
|
||||
btn_priv = QPushButton("🔐 Vérifier les droits")
|
||||
btn_priv.clicked.connect(self._check_privileges)
|
||||
g3.addWidget(btn_priv)
|
||||
self._priv_result = QLabel("")
|
||||
self._priv_result.setWordWrap(True)
|
||||
self._priv_result.setStyleSheet(
|
||||
"color: white; padding: 8px; border-radius: 6px; font-size: 11px;"
|
||||
)
|
||||
g3.addWidget(self._priv_result)
|
||||
lay.addWidget(grp3)
|
||||
|
||||
lay.addStretch()
|
||||
return w
|
||||
|
||||
@@ -948,6 +974,12 @@ class AdminWindow(QDialog):
|
||||
elif status == "leak":
|
||||
lines.append("⚠️ Fuite DNS potentielle !")
|
||||
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
|
||||
elif status == "down":
|
||||
lines.append("🚫 Résolution DNS hors service — utilisez « Réparer le DNS »")
|
||||
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
|
||||
elif not r.get("tunnel_up", False):
|
||||
lines.append("ℹ️ Tunnel inactif : la comparaison n'a pas de sens")
|
||||
css = "color: #aed6f1;"
|
||||
else:
|
||||
lines.append("❓ Statut inconnu")
|
||||
css = "color: #aed6f1;"
|
||||
@@ -962,6 +994,71 @@ class AdminWindow(QDialog):
|
||||
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
|
||||
)
|
||||
|
||||
def _check_privileges(self):
|
||||
"""Liste les commandes privilégiées et celles qui exigeront un dialogue."""
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from app.utils.platform_utils import privilege_report
|
||||
self._priv_result.setText("Vérification en cours…")
|
||||
QApplication.processEvents()
|
||||
|
||||
rows = privilege_report()
|
||||
if not rows:
|
||||
self._priv_result.setText(
|
||||
"✅ Exécution avec les privilèges root : rien à configurer."
|
||||
)
|
||||
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
|
||||
else:
|
||||
lines = [
|
||||
("✅ " if ok else "⚠️ ") + f"{name} — {role}"
|
||||
+ ("" if ok else " (dialogue à chaque appel)")
|
||||
for name, ok, role in rows
|
||||
]
|
||||
missing = [n for n, ok, _ in rows if not ok]
|
||||
if missing:
|
||||
lines.append("")
|
||||
lines.append("Pour supprimer ces dialogues : make setup-sudoers")
|
||||
css = "background: rgba(241,196,15,0.2); color: #f9e79f;"
|
||||
else:
|
||||
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
|
||||
self._priv_result.setText("\n".join(lines))
|
||||
self._priv_result.setStyleSheet(
|
||||
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
|
||||
)
|
||||
|
||||
def _repair_dns(self):
|
||||
"""Retire la configuration DNS résiduelle d'un tunnel mal arrêté.
|
||||
|
||||
`wg-quick down` refuse de s'exécuter dès que l'interface a disparu et
|
||||
ne retire donc jamais l'entrée DNS qu'il avait posée : la résolution
|
||||
reste dirigée vers un serveur injoignable.
|
||||
"""
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
self._dns_result.setText("Réparation en cours…")
|
||||
QApplication.processEvents()
|
||||
|
||||
actions = wg_core.force_cleanup(self._cfg)
|
||||
works = dns_util.resolution_works()
|
||||
|
||||
if works:
|
||||
msg = "✅ Résolution DNS fonctionnelle"
|
||||
if actions:
|
||||
msg += "\nActions : " + ", ".join(actions)
|
||||
css = "background: rgba(39,174,96,0.2); color: #a9dfbf;"
|
||||
elif actions:
|
||||
msg = ("⚠️ Résidus retirés (" + ", ".join(actions) +
|
||||
") mais la résolution échoue toujours.\n"
|
||||
"Vérifiez la connexion réseau physique.")
|
||||
css = "background: rgba(241,196,15,0.2); color: #f9e79f;"
|
||||
else:
|
||||
msg = ("🚫 Aucun résidu du tunnel détecté et la résolution échoue :"
|
||||
" le problème vient du réseau, pas de WGSecure.")
|
||||
css = "background: rgba(231,76,60,0.2); color: #f1948a;"
|
||||
|
||||
self._dns_result.setText(msg)
|
||||
self._dns_result.setStyleSheet(
|
||||
f"padding: 8px; border-radius: 6px; font-size: 11px; {css}"
|
||||
)
|
||||
|
||||
def _set_admin_password(self):
|
||||
p1 = self._admin_pw1.text()
|
||||
p2 = self._admin_pw2.text()
|
||||
|
||||
+208
-82
@@ -10,8 +10,11 @@ from PyQt6.QtGui import QFont, QCloseEvent, QColor
|
||||
|
||||
from app.core.config import Config
|
||||
from app.core import wireguard as wg_core
|
||||
from app.core import dns as dns_util
|
||||
from app.core import log as conn_log
|
||||
from app.core import history as hist
|
||||
from app.core import shutdown as shutdown_guard
|
||||
from app.ui.worker import TaskWorker, ValueWorker
|
||||
from app.ui.mfa_dialog import MFADialog
|
||||
from app.ui.admin_window import AdminWindow
|
||||
from app.ui.bw_graph import BandwidthGraph
|
||||
@@ -47,6 +50,12 @@ class MainWindow(QMainWindow):
|
||||
self._prev_tx: int | None = None
|
||||
self._auto_reconnect_active = False
|
||||
self._session_id: int | None = None
|
||||
self._worker: TaskWorker | None = None
|
||||
self._ping_worker: ValueWorker | None = None
|
||||
self._is_connected = False
|
||||
self._reconnect_failures = 0
|
||||
self._quitting = False
|
||||
self._warned_foreign: set[str] = set()
|
||||
|
||||
self.setWindowTitle("WGSecure")
|
||||
self.setFixedSize(430, 680)
|
||||
@@ -82,6 +91,7 @@ class MainWindow(QMainWindow):
|
||||
self._refresh_status()
|
||||
self._reload_log()
|
||||
self._reload_profiles()
|
||||
self._warn_foreign_interfaces()
|
||||
|
||||
if self._cfg.get("ui", "auto_connect_on_startup") and self._cfg.configured:
|
||||
QTimer.singleShot(500, self._on_connect)
|
||||
@@ -227,7 +237,7 @@ class MainWindow(QMainWindow):
|
||||
self._log_list.setFixedHeight(78)
|
||||
self._log_list.setStyleSheet(
|
||||
f"QListWidget {{ background: {_DARK2}; color: #aed6f1;"
|
||||
" border: none; font-size: 10px; font-family: Courier; }}"
|
||||
" border: none; font-size: 10px; font-family: Courier; }"
|
||||
"QListWidget::item { padding: 1px 6px; }"
|
||||
)
|
||||
self._log_list.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||
@@ -390,47 +400,68 @@ class MainWindow(QMainWindow):
|
||||
rx_bps = max(0, rx_now - self._prev_rx) / dt
|
||||
tx_bps = max(0, tx_now - self._prev_tx) / dt
|
||||
self._prev_rx, self._prev_tx = rx_now, tx_now
|
||||
if wg_core.is_connected(self._cfg):
|
||||
self._bw_graph.push(rx_bps, tx_bps)
|
||||
else:
|
||||
self._bw_graph.push(0, 0)
|
||||
# `_is_connected` est rafraîchi par _refresh_status : inutile de
|
||||
# relancer un `wg show` (un sous-processus) toutes les 3 secondes.
|
||||
self._bw_graph.push(rx_bps if self._is_connected else 0,
|
||||
tx_bps if self._is_connected else 0)
|
||||
|
||||
def _update_ping(self):
|
||||
host = self._cfg.wg.get("server_endpoint", "")
|
||||
if not host or not wg_core.is_connected(self._cfg):
|
||||
self._ping_badge.setText("— ms")
|
||||
self._ping_badge.setStyleSheet(
|
||||
f"border-radius: 11px; font-size: 10px; font-weight: bold; {_PING_CSS['offline']}"
|
||||
)
|
||||
if not host or not self._is_connected:
|
||||
self._set_ping_badge(None, "— ms", "offline")
|
||||
return
|
||||
ms = wg_core.ping_server(host)
|
||||
if ms is None:
|
||||
css = _PING_CSS["bad"]
|
||||
txt = "hors ligne"
|
||||
if self._ping_worker is not None and self._ping_worker.isRunning():
|
||||
return
|
||||
# `ping` bloque jusqu'à 4 s : exécuté dans le thread Qt, il faisait
|
||||
# saccader toute l'interface toutes les 10 secondes.
|
||||
self._ping_worker = ValueWorker(wg_core.ping_server, host, 2, parent=self)
|
||||
self._ping_worker.done.connect(self._on_ping_done)
|
||||
# Détruire le QThread depuis `finished` et non depuis `done` : `done`
|
||||
# est émis avant la sortie de run(), un deleteLater() à ce moment
|
||||
# provoque « QThread: Destroyed while thread is still running ».
|
||||
self._ping_worker.finished.connect(self._ping_worker.deleteLater)
|
||||
self._ping_worker.start()
|
||||
|
||||
def _on_ping_done(self, ms):
|
||||
self._ping_worker = None
|
||||
if not self._is_connected:
|
||||
self._set_ping_badge(None, "— ms", "offline")
|
||||
elif ms is None:
|
||||
self._set_ping_badge(None, "hors ligne", "bad")
|
||||
elif ms < 50:
|
||||
css = _PING_CSS["good"]
|
||||
txt = f"{ms} ms"
|
||||
self._set_ping_badge(ms, f"{ms} ms", "good")
|
||||
elif ms < 200:
|
||||
css = _PING_CSS["medium"]
|
||||
txt = f"{ms} ms"
|
||||
self._set_ping_badge(ms, f"{ms} ms", "medium")
|
||||
else:
|
||||
css = _PING_CSS["bad"]
|
||||
txt = f"{ms} ms"
|
||||
self._ping_badge.setText(txt)
|
||||
self._set_ping_badge(ms, f"{ms} ms", "bad")
|
||||
|
||||
def _set_ping_badge(self, _ms, text: str, css_key: str):
|
||||
self._ping_badge.setText(text)
|
||||
self._ping_badge.setStyleSheet(
|
||||
f"border-radius: 11px; font-size: 10px; font-weight: bold; {css}"
|
||||
"border-radius: 11px; font-size: 10px; font-weight: bold; "
|
||||
+ _PING_CSS[css_key]
|
||||
)
|
||||
|
||||
def _check_reconnect(self):
|
||||
if not self._cfg.get("ui", "auto_reconnect"):
|
||||
if not self._cfg.get("ui", "auto_reconnect") or self._quitting:
|
||||
return
|
||||
try:
|
||||
connected = wg_core.is_connected(self._cfg)
|
||||
except Exception:
|
||||
connected = False
|
||||
if not connected and self._cfg.configured:
|
||||
self._add_log("Auto-reconnexion…", "warning")
|
||||
self._on_connect()
|
||||
if self._worker is not None:
|
||||
return # une (dé)connexion est déjà en cours
|
||||
if self._cfg.mfa_enabled:
|
||||
# Rouvrir une fenêtre MFA modale toutes les 30 s rendrait
|
||||
# l'application inutilisable : la reconnexion doit rester manuelle.
|
||||
return
|
||||
if not self._cfg.configured or self._is_connected:
|
||||
self._reconnect_failures = 0
|
||||
return
|
||||
if self._reconnect_failures >= 5:
|
||||
# Arrêt du harcèlement : chaque tentative demande des privilèges.
|
||||
if self._reconnect_failures == 5:
|
||||
self._add_log("Auto-reconnexion suspendue après 5 échecs", "error")
|
||||
self._reconnect_failures += 1
|
||||
return
|
||||
self._add_log("Auto-reconnexion…", "warning")
|
||||
self._start_connect(silent=True)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Statut WireGuard
|
||||
@@ -461,6 +492,9 @@ class MainWindow(QMainWindow):
|
||||
except Exception:
|
||||
connected = False
|
||||
info = {}
|
||||
self._is_connected = connected
|
||||
if connected:
|
||||
self._reconnect_failures = 0
|
||||
|
||||
if connected:
|
||||
if self._connected_since is None:
|
||||
@@ -519,67 +553,129 @@ class MainWindow(QMainWindow):
|
||||
self._add_log("Clé publique copiée dans le presse-papiers", "info")
|
||||
|
||||
def _on_connect(self):
|
||||
if self._connecting:
|
||||
return
|
||||
try:
|
||||
connected = wg_core.is_connected(self._cfg)
|
||||
except Exception:
|
||||
connected = False
|
||||
|
||||
if connected:
|
||||
ok, msg = wg_core.disconnect(self._cfg)
|
||||
if ok:
|
||||
if self._session_id is not None:
|
||||
rx = self._prev_rx or 0
|
||||
tx = self._prev_tx or 0
|
||||
hist.end_session(self._session_id, rx, tx)
|
||||
self._session_id = None
|
||||
self._add_log("Tunnel WireGuard désactivé", "warning")
|
||||
else:
|
||||
self._add_log(f"Erreur déconnexion : {msg}", "error")
|
||||
QMessageBox.warning(self, "Erreur", msg)
|
||||
"""Bascule connexion/déconnexion. Le travail réel part dans un thread."""
|
||||
if self._worker is not None:
|
||||
return # opération déjà en cours
|
||||
if self._is_connected or wg_core.is_connected(self._cfg):
|
||||
self._start_disconnect()
|
||||
else:
|
||||
if not self._cfg.configured:
|
||||
self._start_connect(silent=False)
|
||||
|
||||
# ── Connexion ────────────────────────────────────────────────────────
|
||||
def _start_connect(self, silent: bool = False):
|
||||
if self._worker is not None:
|
||||
return
|
||||
if not self._cfg.configured:
|
||||
if not silent:
|
||||
QMessageBox.information(
|
||||
self, "Configuration manquante",
|
||||
"WireGuard n'est pas encore configuré.\n"
|
||||
"Ouvrez le panneau Administrateur."
|
||||
)
|
||||
return
|
||||
return
|
||||
|
||||
if self._cfg.mfa_enabled:
|
||||
if not self._cfg.mfa_secret:
|
||||
if self._cfg.mfa_enabled:
|
||||
if not self._cfg.mfa_secret:
|
||||
if not silent:
|
||||
QMessageBox.warning(self, "MFA non configuré",
|
||||
"MFA activé mais aucun secret défini.")
|
||||
return
|
||||
dlg = MFADialog(self._cfg.mfa_secret, self)
|
||||
if dlg.exec() != MFADialog.DialogCode.Accepted or not dlg.is_verified():
|
||||
self._add_log("MFA annulé ou échoué", "warning")
|
||||
return
|
||||
self._add_log("Authentification MFA réussie ✓", "success")
|
||||
return
|
||||
if silent:
|
||||
return # jamais de fenêtre MFA sur minuterie
|
||||
dlg = MFADialog(self._cfg.mfa_secret, self)
|
||||
if dlg.exec() != MFADialog.DialogCode.Accepted or not dlg.is_verified():
|
||||
self._add_log("MFA annulé ou échoué", "warning")
|
||||
return
|
||||
self._add_log("Authentification MFA réussie ✓", "success")
|
||||
|
||||
self._btn_connect.setEnabled(False)
|
||||
self._btn_connect.setText("Connexion…")
|
||||
self._connecting = True
|
||||
QApplication.processEvents()
|
||||
self._begin_task("Connexion…")
|
||||
shutdown_guard.arm() # le tunnel devra être rendu à la sortie
|
||||
self._worker = TaskWorker(wg_core.connect, self._cfg,
|
||||
tag="connect" if not silent else "connect-silent",
|
||||
parent=self)
|
||||
self._worker.done.connect(self._on_task_done)
|
||||
self._worker.finished.connect(self._worker.deleteLater)
|
||||
self._worker.start()
|
||||
|
||||
ok, msg = wg_core.connect(self._cfg)
|
||||
self._connecting = False
|
||||
self._btn_connect.setEnabled(True)
|
||||
# ── Déconnexion ──────────────────────────────────────────────────────
|
||||
def _start_disconnect(self):
|
||||
if self._worker is not None:
|
||||
return
|
||||
self._begin_task("Déconnexion…")
|
||||
self._worker = TaskWorker(wg_core.disconnect, self._cfg,
|
||||
tag="disconnect", parent=self)
|
||||
self._worker.done.connect(self._on_task_done)
|
||||
self._worker.finished.connect(self._worker.deleteLater)
|
||||
self._worker.start()
|
||||
|
||||
def _begin_task(self, label: str):
|
||||
self._connecting = True
|
||||
self._btn_connect.setEnabled(False)
|
||||
self._btn_connect.setText(label)
|
||||
|
||||
def _on_task_done(self, ok: bool, msg: str, tag: str):
|
||||
self._worker = None
|
||||
self._connecting = False
|
||||
self._btn_connect.setEnabled(True)
|
||||
silent = tag.endswith("-silent")
|
||||
|
||||
if tag.startswith("connect"):
|
||||
if ok:
|
||||
self._reconnect_failures = 0
|
||||
self._connected_since = QDateTime.currentDateTime()
|
||||
endpoint = self._cfg.wg.get("server_endpoint", "")
|
||||
profile = self._cfg.active_profile
|
||||
self._session_id = hist.start_session(endpoint, profile)
|
||||
self._session_id = hist.start_session(endpoint,
|
||||
self._cfg.active_profile)
|
||||
self._add_log(f"Connexion établie → {endpoint}", "success")
|
||||
QTimer.singleShot(5000, self._update_ping)
|
||||
else:
|
||||
self._reconnect_failures += 1
|
||||
self._add_log(f"Erreur : {msg}", "error")
|
||||
QMessageBox.warning(self, "Erreur de connexion", msg)
|
||||
if not silent:
|
||||
QMessageBox.warning(self, "Erreur de connexion", msg)
|
||||
else: # disconnect
|
||||
if ok:
|
||||
if self._session_id is not None:
|
||||
hist.end_session(self._session_id,
|
||||
self._prev_rx or 0, self._prev_tx or 0)
|
||||
self._session_id = None
|
||||
self._add_log("Tunnel WireGuard désactivé", "warning")
|
||||
else:
|
||||
self._add_log(f"Erreur déconnexion : {msg}", "error")
|
||||
if not silent:
|
||||
QMessageBox.warning(self, "Erreur", msg)
|
||||
|
||||
self._check_dns_health()
|
||||
self._warn_foreign_interfaces()
|
||||
self._refresh_status()
|
||||
|
||||
def _warn_foreign_interfaces(self):
|
||||
"""Signale un tunnel WireGuard monté hors de WGSecure.
|
||||
|
||||
Il impose son propre DNS au système et WGSecure ne le démontera pas :
|
||||
c'est une cause fréquente de « plus d'accès internet » après coup.
|
||||
"""
|
||||
try:
|
||||
foreign = wg_core.foreign_wg_interfaces(self._cfg)
|
||||
except Exception:
|
||||
return
|
||||
for iface in foreign:
|
||||
if iface in self._warned_foreign:
|
||||
continue
|
||||
self._warned_foreign.add(iface)
|
||||
self._add_log(
|
||||
f"Tunnel « {iface} » actif hors de WGSecure "
|
||||
f"(interface configurée : {self._cfg.wg.get('interface_name','wgs0')})",
|
||||
"warning",
|
||||
)
|
||||
|
||||
def _check_dns_health(self):
|
||||
"""Signale — et répare — une résolution DNS restée détournée."""
|
||||
iface = self._cfg.wg.get("interface_name", "wgs0")
|
||||
repaired, msg = dns_util.repair_if_broken(iface, self._cfg.wg.get("dns", ""))
|
||||
if msg:
|
||||
self._add_log(msg, "warning" if repaired else "error")
|
||||
|
||||
def _open_history(self):
|
||||
HistoryDialog(self).exec()
|
||||
|
||||
@@ -600,19 +696,49 @@ class MainWindow(QMainWindow):
|
||||
self._reload_profiles()
|
||||
self._refresh_status()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Fermeture — restitution de l'état réseau
|
||||
# ------------------------------------------------------------------ #
|
||||
def prepare_quit(self) -> None:
|
||||
"""Arrête minuteries et threads, puis rend le réseau à son état initial.
|
||||
|
||||
Appelé par toutes les voies de sortie (croix de la fenêtre, « Quitter »
|
||||
du systray, signal système). Synchrone : la restauration doit être
|
||||
terminée avant que le processus ne disparaisse, sinon la configuration
|
||||
DNS du tunnel survit à l'application et la machine perd la résolution
|
||||
de noms.
|
||||
"""
|
||||
if self._quitting:
|
||||
return
|
||||
self._quitting = True
|
||||
|
||||
for timer in (self._status_timer, self._clock_timer, self._bw_timer,
|
||||
self._ping_timer, self._reconnect_timer):
|
||||
timer.stop()
|
||||
|
||||
# Laisser les threads en cours se terminer avant de démonter le tunnel :
|
||||
# une connexion et une déconnexion simultanées laisseraient un état mixte.
|
||||
for worker in (self._worker, self._ping_worker):
|
||||
try:
|
||||
if worker is not None and worker.isRunning():
|
||||
worker.wait(15000)
|
||||
except RuntimeError:
|
||||
pass # objet Qt déjà détruit
|
||||
self._worker = None
|
||||
self._ping_worker = None
|
||||
|
||||
if self._session_id is not None:
|
||||
hist.end_session(self._session_id, self._prev_rx or 0,
|
||||
self._prev_tx or 0)
|
||||
self._session_id = None
|
||||
|
||||
shutdown_guard.restore_network("fermeture de l'application")
|
||||
|
||||
def closeEvent(self, event: QCloseEvent):
|
||||
if self._cfg.get("ui", "minimize_to_tray"):
|
||||
if self._cfg.get("ui", "minimize_to_tray") and not self._quitting:
|
||||
# Simple masquage : ce n'est pas une fermeture, le tunnel reste actif.
|
||||
event.ignore()
|
||||
self.hide()
|
||||
else:
|
||||
self._status_timer.stop()
|
||||
self._clock_timer.stop()
|
||||
self._bw_timer.stop()
|
||||
self._ping_timer.stop()
|
||||
self._reconnect_timer.stop()
|
||||
try:
|
||||
if wg_core.is_connected(self._cfg):
|
||||
wg_core.disconnect(self._cfg)
|
||||
except Exception:
|
||||
pass
|
||||
event.accept()
|
||||
return
|
||||
self.prepare_quit()
|
||||
event.accept()
|
||||
|
||||
+8
-11
@@ -104,18 +104,15 @@ class SystemTray(QSystemTrayIcon):
|
||||
|
||||
def _toggle_connection(self):
|
||||
self._win._on_connect()
|
||||
self._update_status()
|
||||
# La (dé)connexion est désormais asynchrone : l'icône sera mise à jour
|
||||
# par la scrutation périodique une fois l'opération terminée.
|
||||
|
||||
def _quit(self):
|
||||
self._poll.stop()
|
||||
try:
|
||||
if wg_core.is_connected(self._cfg):
|
||||
wg_core.disconnect(self._cfg)
|
||||
except Exception:
|
||||
pass
|
||||
self._win._status_timer.stop()
|
||||
self._win._clock_timer.stop()
|
||||
self._win._bw_timer.stop()
|
||||
self._win._ping_timer.stop()
|
||||
self._win._reconnect_timer.stop()
|
||||
self.hide()
|
||||
# prepare_quit() arrête les minuteries, attend les threads en cours,
|
||||
# démonte le tunnel et retire la configuration DNS qu'il avait posée.
|
||||
# Sans cela, quitter en étant connecté laissait la machine sans
|
||||
# résolution de noms.
|
||||
self._win.prepare_quit()
|
||||
QApplication.quit()
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Exécution de tâches bloquantes hors du thread graphique.
|
||||
|
||||
`wg-quick up/down` passe par sudo ou pkexec : entre l'attente du dialogue
|
||||
d'authentification et le montage du tunnel, l'appel peut durer plusieurs
|
||||
dizaines de secondes. Lancé depuis le thread Qt, il gèle toute l'interface
|
||||
(fenêtre grise, « ne répond pas »). Ces workers déportent l'appel.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
|
||||
from PyQt6.QtCore import QThread, pyqtSignal
|
||||
|
||||
|
||||
class TaskWorker(QThread):
|
||||
"""Exécute `fn(*args)` dans un thread et émet son résultat.
|
||||
|
||||
`done` porte (ok, message, tag) ; `tag` permet à l'appelant de savoir de
|
||||
quelle tâche il s'agit lorsqu'un même slot en traite plusieurs.
|
||||
"""
|
||||
|
||||
done = pyqtSignal(bool, str, str)
|
||||
|
||||
def __init__(self, fn: Callable[..., tuple[bool, str]], *args: Any,
|
||||
tag: str = "", parent=None):
|
||||
super().__init__(parent)
|
||||
self._fn = fn
|
||||
self._args = args
|
||||
self._tag = tag
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
ok, msg = self._fn(*self._args)
|
||||
except Exception as e: # noqa: BLE001 — remonté à l'UI
|
||||
ok, msg = False, str(e)
|
||||
self.done.emit(bool(ok), str(msg), self._tag)
|
||||
|
||||
|
||||
class ValueWorker(QThread):
|
||||
"""Variante pour une fonction qui retourne une valeur quelconque."""
|
||||
|
||||
done = pyqtSignal(object)
|
||||
|
||||
def __init__(self, fn: Callable[..., Any], *args: Any, parent=None):
|
||||
super().__init__(parent)
|
||||
self._fn = fn
|
||||
self._args = args
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
result = self._fn(*self._args)
|
||||
except Exception: # noqa: BLE001
|
||||
result = None
|
||||
self.done.emit(result)
|
||||
Reference in New Issue
Block a user