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>
This commit is contained in:
+70
-6
@@ -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