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>
248 lines
11 KiB
Plaintext
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;
|