Files
WGSecure/wgsecure.iss
T
tuxgyverandClaude Opus 5 e781acbf4a fix(update): sous Windows, l'application est relancée et la copie attend vraiment
Rien ne rouvrait l'application après une mise à jour. Trois choses y
concouraient : le service lance l'installeur avec `/VERYSILENT`, l'entrée
`[Run]` qui la rouvre porte `skipifsilent` et s'en trouve écartée, et
`RestartApplications=no` laisse le Restart Manager fermer sans rouvrir. Le
service annonçait pourtant qu'elle redémarrerait.

Lui rendre l'entrée `[Run]` n'aurait rien réglé : l'installeur est lancé par
le service, donc 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 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 vient de la session console : l'application repart avec les
droits de l'utilisateur, pas ceux du service.

Le marqueur qui la commande est posé par l'installeur et non par le service :
celui-ci ne saurait qu'avoir lancé un installeur, quand l'installeur sait que
l'installation a abouti. Il porte de surcroît déjà cette 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. Posé en installation silencieuse
seulement, l'entrée `[Run]` couvrant déjà l'installation manuelle.

L'attente qui devait laisser mourir le service avant la copie ne mesurait par
ailleurs rien : elle demandait à `RenameFile` de renommer le binaire sur
lui-même et tenait l'échec pour un verrou, alors que Windows autorise le
renommage de l'image d'un processus vivant — c'est ce qui rend
`restartreplace` possible — et que `MoveFile` refuse de toute façon une
destination existante. La copie partait donc sur un fichier encore tenu,
échouait, `restartreplace` reportait le remplacement au redémarrage suivant,
et l'installeur s'achevait en annonçant une réussite. L'attente porte
désormais 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.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 18:14:00 +02:00

248 lines
11 KiB
Plaintext

; ──────────────────────────────────────────────
; WGSecure (WGS) — Script Inno Setup
; Installeur Windows complet : WGSecure + WireGuard +
; Visual C++ Redistributable, chacun installé en silence si absent.
; ──────────────────────────────────────────────
#define MyAppName "WGSecure"
#define MyAppVersion "0.12.0"
#define MyAppPublisher "WGSecure"
#define MyAppExeName "wgsecure.exe"
[Setup]
AppId={{8F3B2C1A-7D4E-4A9B-9C6F-1E2D3F4A5B6C}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
OutputDir=dist
OutputBaseFilename=wgsecure-{#MyAppVersion}-setup
Compression=lzma2
SolidCompression=yes
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
PrivilegesRequired=admin
; Mise à jour d'une installation existante : les deux exécutables à remplacer
; sont en cours d'exécution — wgsecure.exe côté utilisateur, wgsecure-helper.exe
; en service. Windows verrouille l'image d'un processus vivant : sans ce qui
; suit, la copie échoue et l'installeur repart en ayant l'air d'avoir réussi.
;
; Le Restart Manager ferme l'application qui détient les fichiers ; le service,
; lui, est arrêté explicitement avant la copie (cf. StopForUpgrade). Les
; fichiers reçoivent en secours le drapeau `restartreplace`, qui programme le
; 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]
Name: "french"; MessagesFile: "compiler:Languages\French.isl"
[Files]
Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion restartreplace
; Service privilégié optionnel (cf. app/service/, `make helper`) : installe/
; désinstalle le tunnel et pose/retire les règles NRPT (split-DNS) en
; LocalSystem, pour éviter une invite d'authentification à chaque connexion/
; déconnexion. skipifsourcedoesntexist : le build peut être absent (composant
; optionnel, pas encore généré sur toutes les machines de build) — dans ce
; cas l'app retombe simplement sur son chemin élevé existant.
Source: "dist\wgsecure-helper.exe"; DestDir: "{app}"; Flags: ignoreversion restartreplace skipifsourcedoesntexist
; VC++ Redistributable x64 — téléchargé automatiquement par `make installer-deps` :
; https://aka.ms/vs/17/release/vc_redist.x64.exe → installer\VC_redist.x64.exe
Source: "installer\VC_redist.x64.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall
; WireGuard pour Windows — prérequis à WGSecure (fournit wg.exe, le pilote
; tunnel et le service WireGuardTunnel$ que wireguard.py pilote). Téléchargé
; automatiquement par `make installer-deps` :
; https://download.wireguard.com/windows-client/wireguard-installer.exe
; → installer\wireguard-installer.exe
Source: "installer\wireguard-installer.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
[Run]
; Installation silencieuse du VC++ Redistributable si absent (corrige "Failed to load Python DLL: python312.dll")
Filename: "{tmp}\VC_redist.x64.exe"; Parameters: "/install /quiet /norestart"; StatusMsg: "Installation du composant Visual C++ Redistributable…"; Check: VCRedistNeedsInstall
; L'installeur officiel WireGuard s'exécute déjà sans assistant (juste une
; fenêtre de progression) : notre propre installeur tourne en admin
; (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]
function VCRedistNeedsInstall: Boolean;
var
Version: String;
begin
// Clé présente si VC++ 2015-2022 x64 (v14+) déjà installé
Result := not RegQueryStringValue(HKLM64, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\X64', 'Version', Version);
end;
function WireGuardNeedsInstall: Boolean;
begin
// WireGuard s'installe toujours dans le Program Files 64 bits, quelle que
// soit l'architecture de WGSecure : {pf} y pointe bien puisque
// ArchitecturesInstallIn64BitMode=x64compatible ci-dessus.
Result := not FileExists(ExpandConstant('{pf}\WireGuard\wireguard.exe'));
end;
// Enregistre et démarre le service privilégié (cf. app/service/) via
// Exec()+AddQuotes plutôt que des entrées [Run] à guillemets échappés
// ("" imbriqués) : une tentative précédente basée sur l'échappement de
// chaîne Inno Setup produisait une commande sc.exe invalide (silencieuse,
// aucune erreur visible pendant l'installation) alors que la même commande
// fonctionnait telle quelle lancée à la main — AddQuotes() gère le
// guillemetage de façon fiable, sans juggler des niveaux d'échappement
// imbriqués à la main.
procedure InstallHelperService;
var
ResultCode: Integer;
ExePath, Params: String;
begin
ExePath := ExpandConstant('{app}\wgsecure-helper.exe');
// Optionnel (skipifsourcedoesntexist dans [Files]) : n'enregistrer le
// service que si le binaire a bien été copié.
if not FileExists(ExePath) then
exit;
Params := 'create WGSecureHelper binPath= ' + AddQuotes(ExePath) +
' start= auto DisplayName= ' + AddQuotes('WGSecure Helper');
Exec(ExpandConstant('{sys}\sc.exe'), Params, '', SW_HIDE,
ewWaitUntilTerminated, ResultCode);
Exec(ExpandConstant('{sys}\sc.exe'), 'start WGSecureHelper', '', SW_HIDE,
ewWaitUntilTerminated, ResultCode);
end;
// sc.exe ignore silencieusement une commande sur un service déjà absent
// (ex. installeur généré sans le composant service) : pas besoin de tester
// son existence avant d'arrêter/supprimer.
procedure UninstallHelperService;
var
ResultCode: Integer;
begin
Exec(ExpandConstant('{sys}\sc.exe'), 'stop WGSecureHelper', '', SW_HIDE,
ewWaitUntilTerminated, ResultCode);
Exec(ExpandConstant('{sys}\sc.exe'), 'delete WGSecureHelper', '', SW_HIDE,
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.
// Les tunnels sont arrêtés au passage : leur service tourne en SYSTEM et
// n'est pas remplacé ici, mais laisser le VPN monté pendant que son pilote
// disparaît de l'écran n'aurait aucun sens pour l'utilisateur.
procedure StopForUpgrade;
var
ResultCode, I: Integer;
Names: TArrayOfString;
J: Integer;
begin
// Tunnels WireGuard posés par WGSecure : le nom d'interface est réglable,
// on interroge donc le registre des services plutôt que de deviner « wgs0 ».
if RegGetSubkeyNames(HKEY_LOCAL_MACHINE, 'SYSTEM\CurrentControlSet\Services', Names) then
begin
for J := 0 to GetArrayLength(Names) - 1 do
if Pos('WireGuardTunnel$', Names[J]) = 1 then
Exec(ExpandConstant('{sys}\sc.exe'), 'stop ' + AddQuotes(Names[J]),
'', SW_HIDE, ewWaitUntilTerminated, ResultCode);
end;
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é. 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 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);
begin
if CurUninstallStep = usUninstall then
UninstallHelperService;
end;