Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb8036df8 | ||
|
|
e781acbf4a | ||
|
|
99e1db5880 | ||
|
|
9e72fa4155 |
@@ -6,6 +6,26 @@ Ce projet suit le [Versionnage Sémantique](https://semver.org/lang/fr/).
|
||||
|
||||
---
|
||||
|
||||
## [0.12.1] — 2026-09-08
|
||||
|
||||
### Corrigé
|
||||
- **Sous Windows, l'application n'était jamais relancée après une mise à jour.** Trois choses y concouraient : le service lance l'installeur avec `/VERYSILENT`, l'entrée `[Run]` qui rouvre l'application porte `skipifsilent` et s'en trouve écartée, et `RestartApplications=no` laisse le Restart Manager fermer sans rouvrir. Le service annonçait pourtant que l'application redémarrerait. Lui rendre l'entrée `[Run]` n'aurait rien réglé : l'installeur étant lancé par le service, il tourne en LocalSystem dans la session 0, où l'application se serait ouverte sans bureau visible et avec les privilèges du système. La relance revient donc au service, seul composant capable d'ouvrir un processus dans la session d'un autre, et que l'installeur redémarre une fois les binaires en place. Le jeton est celui de la session console : l'application repart avec les droits de l'utilisateur.
|
||||
- **Le marqueur de relance est posé par l'installeur, non par le service.** Le service ne saurait qu'avoir lancé un installeur, quand celui-ci sait que l'installation a abouti. Il porte de surcroît déjà la correction, si bien que la première mise à jour en bénéficie — un marqueur écrit par l'ancien service ne serait apparu qu'à la deuxième. Il n'est posé qu'en installation silencieuse : une installation lancée à la main rouvre déjà l'application par son entrée `[Run]`.
|
||||
- **L'installeur copiait par-dessus un binaire encore verrouillé.** L'attente censée laisser mourir le service demandait à `RenameFile` de renommer le fichier sur lui-même et tenait l'échec pour un verrou. Elle ne mesurait rien : Windows autorise le renommage de l'image d'un processus vivant — c'est ce qui rend `restartreplace` possible — et `MoveFile` refuse de toute façon une destination existante. La réponse était donc la même quel que soit l'état réel du service. La copie échouait, `restartreplace` reportait le remplacement au redémarrage suivant, et l'installeur s'achevait en annonçant une réussite que rien n'avait accomplie. L'attente porte maintenant sur la disparition du processus, relevée par `tasklist` — dont le nom d'image, contrairement aux libellés d'état de `sc.exe`, n'est pas traduit sur un Windows français.
|
||||
|
||||
---
|
||||
|
||||
## [0.12.0] — 2026-09-08
|
||||
|
||||
### Ajouté
|
||||
- **Une saisie de code réussie se voit enfin à l'écran.** Le message du serveur partait dans le seul journal, pendant que la fenêtre de saisie se refermait : rien ne distinguait un code accepté d'un code ignoré, et la durée obtenue — précisément ce que l'utilisateur venait de demander — restait invisible. Une confirmation l'annonce désormais avec son échéance et le temps restant.
|
||||
- **Le client vérifie ensuite que le trafic atteint réellement le réseau distant.** Le serveur peut enregistrer l'autorisation sans que la règle de pare-feu s'applique ; le réseau reste alors muet malgré un code accepté, et l'utilisateur ne pouvait que le découvrir seul, plus tard. La vérification observe ce qui passe, là où le sondage se contente de demander au serveur ce qu'il a enregistré. Son verdict rejoint le journal, succès comme échec, en nommant la cible et en renvoyant à l'administrateur.
|
||||
|
||||
### Corrigé
|
||||
- **Un refus du serveur pouvait faire lever une exception au lieu de s'afficher.** Contre un serveur antérieur à la v0.3.2, qui exige encore un identifiant que ce poste ne transmet plus, `POST /api/session` répond 422 — et le `detail` d'une erreur de validation FastAPI est une *liste*, non une phrase. Elle atterrissait telle quelle dans le champ `message` de la réponse, que l'interface passe ensuite à Qt : le refus n'était pas montré, il interrompait le traitement de la réponse. Seule une chaîne est désormais retenue comme message, et ce cas précis explique ce qu'il faut faire — mettre le serveur à jour. Un corps JSON qui n'est pas un objet, qui provoquait la même interruption un cran plus tôt, est écarté de même.
|
||||
|
||||
---
|
||||
|
||||
## [0.11.0] — 2026-09-08
|
||||
|
||||
### Ajouté
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
## ──────────────────────────────────────────────
|
||||
|
||||
APP := wgsecure
|
||||
VERSION := 0.11.0
|
||||
VERSION := 0.12.1
|
||||
VENV := .venv
|
||||
PYTHON := $(VENV)/bin/python3
|
||||
PIP := $(VENV)/bin/pip
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 🛡️ WGSecure (WGS)
|
||||
|
||||
🚀 v0.11.0 · 🐍 · 🪟🐧 · 🔐 Accès validé par le serveur · 🛡️ WireGuard
|
||||
🚀 v0.12.1 · 🐍 · 🪟🐧 · 🔐 Accès validé par le serveur · 🛡️ WireGuard
|
||||
|
||||
**WGSecure** est une interface graphique multiplateforme (Windows & Linux) pour gérer une connexion WireGuard dont l'accès aux ressources distantes est ouvert par un code à 6 chiffres validé **par le serveur**.
|
||||
|
||||
@@ -210,4 +210,4 @@ WGSecure/
|
||||
## 👤 Auteur
|
||||
|
||||
Développé par **Johnny** — [JT-Tools](https://github.com/JT-Tools)
|
||||
Version :  — Septembre 2026
|
||||
Version :  — Septembre 2026
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
__version__ = "0.11.0"
|
||||
__version__ = "0.12.1"
|
||||
APP_NAME = "WGSecure"
|
||||
APP_SHORT = "WGS"
|
||||
|
||||
+30
-10
@@ -101,6 +101,23 @@ def _parse_until(raw: object) -> datetime | None:
|
||||
return moment if moment.tzinfo else moment.replace(tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _error_detail(exc: urllib.error.HTTPError) -> str:
|
||||
"""Le motif du refus tel que le serveur l'énonce, ou "" s'il n'en dit rien.
|
||||
|
||||
FastAPI répond aux erreurs de validation (422) par un `detail` qui est une
|
||||
*liste* d'erreurs, pas une phrase. Relayée telle quelle, elle atterrissait
|
||||
dans un champ `message` que l'interface passe ensuite à Qt : le refus
|
||||
n'était alors pas affiché, il levait une exception au moment de le lire.
|
||||
Seule une chaîne est un message ; tout autre corps ne dit rien
|
||||
d'affichable, et le message générique de l'appelant vaut mieux.
|
||||
"""
|
||||
try:
|
||||
detail = json.loads(exc.read().decode("utf-8")).get("detail", "")
|
||||
except (ValueError, OSError, AttributeError):
|
||||
return "" # corps illisible, ou JSON sans objet
|
||||
return detail if isinstance(detail, str) else ""
|
||||
|
||||
|
||||
def api_base_url(cfg: Config) -> str:
|
||||
"""URL de l'API du serveur, telle qu'atteignable depuis le tunnel.
|
||||
|
||||
@@ -179,7 +196,7 @@ def _routed_through_tunnel(cfg: Config, target: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def _remote_network_reachable(cfg: Config) -> bool:
|
||||
def remote_network_reachable(cfg: Config) -> bool:
|
||||
"""Le trafic passe-t-il réellement vers le réseau distant ?
|
||||
|
||||
Repli pour les serveurs antérieurs à la v0.3.0, qui n'exposent pas
|
||||
@@ -243,11 +260,8 @@ def session_status(cfg: Config) -> AuthResult:
|
||||
return AuthResult(True, "Accès réseau déjà ouvert jusqu'à "
|
||||
f"{until.astimezone():%d/%m/%Y %H:%M}", until)
|
||||
except urllib.error.HTTPError as exc:
|
||||
try:
|
||||
detail = json.loads(exc.read().decode("utf-8")).get("detail", "")
|
||||
except (ValueError, OSError):
|
||||
detail = ""
|
||||
if exc.code == 404 and _remote_network_reachable(cfg):
|
||||
detail = _error_detail(exc)
|
||||
if exc.code == 404 and remote_network_reachable(cfg):
|
||||
# Serveur antérieur à la v0.3.0 : la route n'existe pas, mais le
|
||||
# trafic atteint le réseau distant — l'accès est donc bien ouvert.
|
||||
# Aucune échéance en revanche : seul le serveur la connaît, et il
|
||||
@@ -324,10 +338,16 @@ def authenticate(cfg: Config, code: str) -> AuthResult:
|
||||
# Le serveur explique lui-même le refus (code invalide, compte
|
||||
# verrouillé, expiré…) : le relayer tel quel vaut mieux qu'un
|
||||
# message générique qui obligerait à consulter les journaux.
|
||||
try:
|
||||
detail = json.loads(exc.read().decode("utf-8")).get("detail", "")
|
||||
except (ValueError, OSError):
|
||||
detail = ""
|
||||
detail = _error_detail(exc)
|
||||
if exc.code == 422:
|
||||
# Serveur antérieur à la v0.3.2 : il exige encore un identifiant
|
||||
# dans le corps, que ce poste ne transmet plus. Son `detail` est
|
||||
# la liste d'erreurs de validation de FastAPI — `_error_detail`
|
||||
# l'écarte, et il n'en resterait qu'un « code 422 » énigmatique.
|
||||
return AuthResult(False, "Ce serveur attend un identifiant VPN "
|
||||
"que ce poste ne transmet plus. Mettez "
|
||||
"le serveur à jour (v0.3.2 ou "
|
||||
"supérieur).")
|
||||
return AuthResult(False, detail
|
||||
or f"Authentification refusée (code {exc.code})")
|
||||
except urllib.error.URLError as exc:
|
||||
|
||||
@@ -26,6 +26,7 @@ import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from logging.handlers import RotatingFileHandler
|
||||
@@ -38,6 +39,9 @@ import win32event
|
||||
import win32file
|
||||
import win32pipe
|
||||
import win32security
|
||||
import win32process
|
||||
import win32profile
|
||||
import win32ts
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
@@ -66,6 +70,21 @@ _LOG_FILE = os.path.join(_LOG_DIR, "helper.log")
|
||||
# poser une règle DNS valable pour toute la machine.
|
||||
_OWNER_FILE = os.path.join(_LOG_DIR, "owner.sid")
|
||||
|
||||
# Posé par l'installeur au terme d'une installation silencieuse, relu par ce
|
||||
# service qu'il redémarre juste après. L'installeur en est l'auteur et non le
|
||||
# service : lui seul sait qu'une installation s'est réellement achevée, là où
|
||||
# le service ne saurait que l'avoir lancée. Et comme il porte déjà la version
|
||||
# corrigée, la toute première mise à jour en bénéficie — un marqueur écrit par
|
||||
# le service ne serait apparu qu'à la deuxième.
|
||||
#
|
||||
# Sa date de modification fait foi pour la péremption : elle est posée par
|
||||
# l'écriture elle-même, sans que l'auteur ait à l'inscrire.
|
||||
_PENDING_RELAUNCH = os.path.join(_LOG_DIR, "relaunch.pending")
|
||||
# Au-delà, le marqueur est tenu pour un reliquat : un service qui redémarre
|
||||
# des jours plus tard, pour une tout autre raison, n'a pas à ouvrir une
|
||||
# fenêtre sur le bureau de l'utilisateur.
|
||||
_RELAUNCH_WINDOW = 30 * 60
|
||||
|
||||
# Directives refusées dans un `.conf` : elles font exécuter des commandes par
|
||||
# le service, donc en LocalSystem. `wireguard.exe` les ignore sous Windows,
|
||||
# mais le fichier n'a aucune raison d'en porter — les refuser coûte une ligne
|
||||
@@ -526,6 +545,85 @@ def _handle_connection(handle, logger: logging.Logger) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _relaunch_after_update(logger: logging.Logger) -> None:
|
||||
"""Relance l'interface dans la session de l'utilisateur, après une mise à jour.
|
||||
|
||||
Rien ne la relançait. L'installeur est lancé par ce service, donc en
|
||||
LocalSystem et dans la session 0 ; son entrée `[Run]` porte `skipifsilent`
|
||||
et le `/VERYSILENT` de l'appel l'écarte. La lui rendre ne réglerait rien :
|
||||
elle démarrerait l'interface là où elle tourne, session 0, sans bureau
|
||||
visible et avec les privilèges du système — une fenêtre que personne ne
|
||||
voit, tenant le VPN en SYSTEM.
|
||||
|
||||
Le geste revient donc ici. Ce service est le seul composant à pouvoir
|
||||
ouvrir un processus dans la session d'un autre, et l'installeur le
|
||||
redémarre une fois les binaires en place, après avoir posé le marqueur que
|
||||
voici : le service qui le lit est déjà la nouvelle version.
|
||||
|
||||
Le jeton vient de la session console : l'interface repart avec les droits
|
||||
de l'utilisateur, pas ceux du service.
|
||||
"""
|
||||
try:
|
||||
age = time.time() - os.path.getmtime(_PENDING_RELAUNCH)
|
||||
with open(_PENDING_RELAUNCH, encoding="utf-8") as f:
|
||||
version = f.read().strip()
|
||||
except OSError:
|
||||
return # cas ordinaire : pas de mise à jour
|
||||
finally:
|
||||
# Effacé quoi qu'il arrive, y compris si la relance échoue : mieux
|
||||
# vaut une interface à rouvrir à la main qu'un marqueur qui la
|
||||
# rouvrirait à chaque démarrage du service.
|
||||
try:
|
||||
os.unlink(_PENDING_RELAUNCH)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if age > _RELAUNCH_WINDOW:
|
||||
logger.info("Marqueur de relance périmé (%d s), ignoré", age)
|
||||
return
|
||||
|
||||
exe = os.path.join(os.path.dirname(sys.executable), "wgsecure.exe")
|
||||
if not os.path.isfile(exe):
|
||||
logger.warning("Relance impossible : %s introuvable", exe)
|
||||
return
|
||||
|
||||
session = win32ts.WTSGetActiveConsoleSessionId()
|
||||
if session == 0xFFFFFFFF:
|
||||
# Mise à jour appliquée alors que personne n'est connecté : il n'y a
|
||||
# pas de bureau où ouvrir la fenêtre, et l'utilisateur retrouvera
|
||||
# l'application à jour à sa prochaine ouverture de session.
|
||||
logger.info("Aucune session console : relance sans objet")
|
||||
return
|
||||
|
||||
token = env = None
|
||||
try:
|
||||
token = win32ts.WTSQueryUserToken(session)
|
||||
env = win32profile.CreateEnvironmentBlock(token, False)
|
||||
startup = win32process.STARTUPINFO()
|
||||
startup.lpDesktop = r"winsta0\default"
|
||||
handles = win32process.CreateProcessAsUser(
|
||||
token, exe, None, None, None, False,
|
||||
win32con.CREATE_UNICODE_ENVIRONMENT, env,
|
||||
os.path.dirname(exe), startup)
|
||||
for h in handles[:2]: # processus et thread, inutiles ici
|
||||
win32api.CloseHandle(h)
|
||||
logger.info("Interface relancée en %s dans la session %d",
|
||||
version or "version inconnue", session)
|
||||
except pywintypes.error as e:
|
||||
logger.warning("Relance de l'interface impossible : %s", e)
|
||||
finally:
|
||||
if env is not None:
|
||||
try:
|
||||
win32profile.DestroyEnvironmentBlock(env)
|
||||
except pywintypes.error:
|
||||
pass
|
||||
if token is not None:
|
||||
try:
|
||||
win32api.CloseHandle(token)
|
||||
except pywintypes.error:
|
||||
pass
|
||||
|
||||
|
||||
class WGSecureHelperService(win32serviceutil.ServiceFramework):
|
||||
_svc_name_ = "WGSecureHelper"
|
||||
_svc_display_name_ = "WGSecure Helper"
|
||||
@@ -575,6 +673,14 @@ class WGSecureHelperService(win32serviceutil.ServiceFramework):
|
||||
raise
|
||||
|
||||
logger.info("Service démarré")
|
||||
# Avant de servir : ce démarrage est peut-être celui que l'installeur
|
||||
# vient de provoquer. Protégé à part — une relance qui échoue ne doit
|
||||
# pas empêcher le service de faire son travail.
|
||||
try:
|
||||
_relaunch_after_update(logger)
|
||||
except Exception:
|
||||
logger.exception("Relance post-mise à jour en échec")
|
||||
|
||||
try:
|
||||
self._serve_forever(logger)
|
||||
except Exception:
|
||||
|
||||
+54
-1
@@ -88,6 +88,10 @@ class MainWindow(QMainWindow):
|
||||
# déclenché sans invite est déjà parti.
|
||||
self._probe_worker: ValueWorker | None = None
|
||||
self._probe_prompt = False
|
||||
# Vérification, après un code accepté, que le trafic atteint bien le
|
||||
# réseau distant. Distincte du sondage : celui-ci demande au serveur
|
||||
# ce qu'il a enregistré, celle-là observe ce qui passe réellement.
|
||||
self._reach_worker: ValueWorker | None = None
|
||||
# Date du dernier sondage abouti, et nombre de sondages consécutifs
|
||||
# ayant trouvé fermé un accès qu'on croyait ouvert (cf.
|
||||
# `_access_stale` et `_on_probe_reply`). `_access_message` évite de
|
||||
@@ -1369,6 +1373,7 @@ class MainWindow(QMainWindow):
|
||||
self._access_denials = 0
|
||||
self._probe_retried = False
|
||||
self._add_log(message, "success")
|
||||
self._announce_access_granted()
|
||||
else:
|
||||
# Abandon : l'état d'accès n'est pas touché. Il l'était, et à tort
|
||||
# — un sondage abouti pendant que la fenêtre était ouverte (les
|
||||
@@ -1383,6 +1388,53 @@ class MainWindow(QMainWindow):
|
||||
"accès distant fermé", "warning")
|
||||
self._refresh_status()
|
||||
|
||||
def _announce_access_granted(self):
|
||||
"""Confirme la validation, en disant jusqu'à quand l'accès tient.
|
||||
|
||||
Le message du serveur partait jusqu'ici dans le seul journal, pendant
|
||||
que la fenêtre se refermait : rien ne distinguait à l'écran un code
|
||||
accepté d'un code ignoré, et la durée obtenue restait invisible alors
|
||||
que c'est précisément ce que l'utilisateur vient de demander.
|
||||
"""
|
||||
if self._access_until is not None:
|
||||
fin = self._access_until.astimezone()
|
||||
texte = (f"Code accepté.\n\nAccès au réseau distant ouvert "
|
||||
f"jusqu'au {fin:%d/%m/%Y à %H:%M} "
|
||||
f"(soit {self._remaining_text()}).")
|
||||
else:
|
||||
texte = ("Code accepté.\n\nAccès au réseau distant ouvert. Le "
|
||||
"serveur n'a pas annoncé d'échéance.")
|
||||
QMessageBox.information(self, "Accès ouvert", texte)
|
||||
# Le serveur peut avoir enregistré l'autorisation sans que le trafic
|
||||
# passe pour autant — règle de pare-feu non appliquée, par exemple.
|
||||
# Le constater vaut mieux que laisser l'utilisateur découvrir seul un
|
||||
# réseau resté muet après un code pourtant accepté.
|
||||
self._verify_access_effective()
|
||||
|
||||
def _verify_access_effective(self):
|
||||
"""Vérifie en fond que le trafic atteint réellement le réseau distant."""
|
||||
if self._reach_worker is not None:
|
||||
return
|
||||
self._reach_worker = ValueWorker(vpn_session.remote_network_reachable,
|
||||
self._cfg, parent=self)
|
||||
self._reach_worker.done.connect(self._on_reach_reply)
|
||||
self._reach_worker.finished.connect(self._reach_worker.deleteLater)
|
||||
self._reach_worker.start()
|
||||
|
||||
def _on_reach_reply(self, joignable: bool):
|
||||
self._reach_worker = None
|
||||
cible = (self._cfg.wg.get("split_dns_server") or "").strip()
|
||||
if not cible:
|
||||
return # rien à sonder, donc rien à conclure
|
||||
if joignable:
|
||||
self._add_log(f"Trafic vers le réseau distant confirmé ({cible})",
|
||||
"success")
|
||||
return
|
||||
self._add_log(
|
||||
f"Code accepté, mais {cible} reste injoignable : le serveur a "
|
||||
"enregistré l'autorisation sans que le trafic passe. Prévenez "
|
||||
"l'administrateur.", "warning")
|
||||
|
||||
def _begin_task(self, label: str):
|
||||
self._connecting = True
|
||||
self._btn_connect.setEnabled(False)
|
||||
@@ -1505,7 +1557,7 @@ class MainWindow(QMainWindow):
|
||||
# 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, self._probe_worker,
|
||||
self._update_worker):
|
||||
self._reach_worker, self._update_worker):
|
||||
try:
|
||||
if worker is not None and worker.isRunning():
|
||||
worker.wait(15000)
|
||||
@@ -1520,6 +1572,7 @@ class MainWindow(QMainWindow):
|
||||
self._worker = None
|
||||
self._ping_worker = None
|
||||
self._probe_worker = None
|
||||
self._reach_worker = None
|
||||
self._update_worker = None
|
||||
|
||||
if self._session_id is not None:
|
||||
|
||||
+71
-7
@@ -5,7 +5,7 @@
|
||||
; ──────────────────────────────────────────────
|
||||
|
||||
#define MyAppName "WGSecure"
|
||||
#define MyAppVersion "0.11.0"
|
||||
#define MyAppVersion "0.12.1"
|
||||
#define MyAppPublisher "WGSecure"
|
||||
#define MyAppExeName "wgsecure.exe"
|
||||
|
||||
@@ -35,6 +35,13 @@ PrivilegesRequired=admin
|
||||
; remplacement au prochain démarrage si un verrou subsiste malgré tout.
|
||||
CloseApplications=yes
|
||||
CloseApplicationsFilter=*.exe
|
||||
; Le Restart Manager ferme l'application, il ne la rouvre pas. C'est
|
||||
; volontaire : lors d'une mise à jour automatique, cet installeur est lancé
|
||||
; par le service, donc en LocalSystem depuis la session 0 — ce que le Restart
|
||||
; Manager y relancerait n'aurait ni bureau visible ni les droits de
|
||||
; l'utilisateur. La relance est confiée au service, qui repart une fois les
|
||||
; binaires en place et sait ouvrir un processus dans la session console
|
||||
; (cf. `_relaunch_after_update` dans app/service/helper_service.py).
|
||||
RestartApplications=no
|
||||
|
||||
[Languages]
|
||||
@@ -71,6 +78,11 @@ Filename: "{tmp}\VC_redist.x64.exe"; Parameters: "/install /quiet /norestart"; S
|
||||
; (PrivilegesRequired=admin ci-dessus), donc pas d'UAC supplémentaire non plus.
|
||||
; `waituntilterminated` : l'app ne doit pas démarrer avant que wg.exe existe.
|
||||
Filename: "{tmp}\wireguard-installer.exe"; StatusMsg: "Installation de WireGuard…"; Check: WireGuardNeedsInstall; Flags: waituntilterminated
|
||||
; `skipifsilent` est délibéré : cette entrée ne vaut que pour une installation
|
||||
; lancée à la main, où elle démarre l'application sous le compte qui installe.
|
||||
; La mise à jour automatique, elle, passe par `/VERYSILENT` sous LocalSystem —
|
||||
; l'application y serait ouverte dans la session 0, invisible et en SYSTEM.
|
||||
; C'est le service qui la relance dans la bonne session (cf. RestartApplications).
|
||||
Filename: "{app}\{#MyAppExeName}"; Description: "Lancer {#MyAppName}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[Code]
|
||||
@@ -131,6 +143,31 @@ begin
|
||||
ewWaitUntilTerminated, ResultCode);
|
||||
end;
|
||||
|
||||
// Le processus du service est-il encore vivant ?
|
||||
//
|
||||
// L'attente précédente demandait à RenameFile() de renommer le binaire sur
|
||||
// lui-même, en tenant l'échec pour un verrou. Elle ne mesurait rien : Windows
|
||||
// laisse renommer l'image d'un processus vivant — c'est même ce qui rend
|
||||
// possible le `restartreplace` — et MoveFile() refuse de toute façon une
|
||||
// destination existante, verrou ou pas. La boucle rendait donc toujours la
|
||||
// même réponse, quel que soit l'état réel du service, et la copie partait sur
|
||||
// un fichier encore tenu : elle échouait, `restartreplace` reportait le
|
||||
// remplacement au redémarrage suivant, et l'installeur s'achevait en
|
||||
// annonçant une réussite que rien n'avait accomplie.
|
||||
//
|
||||
// tasklist interroge la liste des processus, et le nom d'image n'est pas
|
||||
// traduit — contrairement aux libellés d'état de sc.exe, qui le sont sur un
|
||||
// Windows français.
|
||||
function HelperStillRunning: Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := Exec(ExpandConstant('{cmd}'),
|
||||
'/C tasklist /FI "IMAGENAME eq wgsecure-helper.exe" | find /I "wgsecure-helper.exe"',
|
||||
'', SW_HIDE, ewWaitUntilTerminated, ResultCode)
|
||||
and (ResultCode = 0);
|
||||
end;
|
||||
|
||||
// Arrêt de tout ce qui tient les fichiers, avant la copie. Le service se
|
||||
// remplace lui-même : il doit donc être à l'arrêt, et `sc stop` rend la main
|
||||
// avant que le processus n'ait fini de mourir — d'où l'attente qui suit.
|
||||
@@ -156,24 +193,51 @@ begin
|
||||
Exec(ExpandConstant('{sys}\sc.exe'), 'stop WGSecureHelper', '', SW_HIDE,
|
||||
ewWaitUntilTerminated, ResultCode);
|
||||
// `sc stop` demande l'arrêt sans l'attendre : copier tout de suite
|
||||
// retomberait sur un binaire encore verrouillé.
|
||||
for I := 1 to 20 do
|
||||
// retomberait sur un binaire encore verrouillé. On attend donc la
|
||||
// disparition du *processus*, seule chose qui tienne le verrou — le service
|
||||
// peut être annoncé arrêté alors que son image l'est encore.
|
||||
for I := 1 to 60 do
|
||||
begin
|
||||
if not FileExists(ExpandConstant('{app}\wgsecure-helper.exe')) then
|
||||
Break;
|
||||
if RenameFile(ExpandConstant('{app}\wgsecure-helper.exe'),
|
||||
ExpandConstant('{app}\wgsecure-helper.exe')) then
|
||||
if not HelperStillRunning then
|
||||
Break;
|
||||
Sleep(500);
|
||||
end;
|
||||
end;
|
||||
|
||||
// Signale au service qu'il devra rouvrir l'interface. Uniquement en mode
|
||||
// silencieux : une installation lancée à la main rouvre l'application par son
|
||||
// entrée [Run], sous le compte qui installe, et poser le marqueur ferait
|
||||
// alors deux ouvertures pour une seule installation.
|
||||
//
|
||||
// Écrit ici plutôt que par le service qui a lancé cet installeur : le service
|
||||
// ne sait que l'avoir lancé, cet installeur sait que l'installation a abouti.
|
||||
// Il porte de surcroît déjà la correction, donc la première mise à jour en
|
||||
// profite — un marqueur posé par l'ancien service n'aurait rien produit.
|
||||
//
|
||||
// Avant `InstallHelperService`, qui démarre le service : c'est ce démarrage
|
||||
// qui le relit.
|
||||
procedure MarkRelaunchPending;
|
||||
var
|
||||
Dir: String;
|
||||
begin
|
||||
if not WizardSilent then
|
||||
exit;
|
||||
Dir := ExpandConstant('{commonappdata}\WGSecure');
|
||||
if not ForceDirectories(Dir) then
|
||||
exit; // pas de marqueur, pas de relance
|
||||
SaveStringToFile(Dir + '\relaunch.pending',
|
||||
'{#MyAppVersion}', False);
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
begin
|
||||
if CurStep = ssInstall then
|
||||
StopForUpgrade;
|
||||
if CurStep = ssPostInstall then
|
||||
begin
|
||||
MarkRelaunchPending;
|
||||
InstallHelperService;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
|
||||
|
||||
Reference in New Issue
Block a user