Skip to content

Commit ab0ac7f

Browse files
committed
Installer: non-disruptive upgrade with staging mode
Add staged upgrade mode to the installer, activated by /KEEPMOUNTED=true in silent installs or via dialog in interactive mode. Without this flag, the installer behaves as before. When staging: - Most files go to {app}\PendingUpgrade\ instead of replacing in-place - GVFS.Service.exe is replaced directly (brief stop/start) - Mount processes continue running on old binaries throughout - .ready marker written after all files staged (guards against partial) When not staging (clean upgrade): - CloseApplications=no prevents Restart Manager from killing processes - Force-kill GVFS processes if unmount-all fails to clean up - WaitForServiceProcessToExit polls sc query after sc stop/delete Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent 50b24e6 commit ab0ac7f

1 file changed

Lines changed: 220 additions & 46 deletions

File tree

GVFS/GVFS.Installers/Setup.iss

Lines changed: 220 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ ArchitecturesInstallIn64BitMode=x64compatible
4242
ArchitecturesAllowed=x64compatible
4343
WizardImageStretch=no
4444
WindowResizable=no
45-
CloseApplications=yes
45+
CloseApplications=no
4646
ChangesEnvironment=yes
4747
RestartIfNeededByRun=yes
4848

@@ -59,8 +59,14 @@ Name: "full"; Description: "Full installation"; Flags: iscustom;
5959
Type: files; Name: "{app}\ucrtbase.dll"
6060

6161
[Files]
62-
DestDir: "{app}"; Flags: ignoreversion recursesubdirs; Source:"{#LayoutDir}\*"
63-
DestDir: "{app}"; Flags: ignoreversion; Source:"{#LayoutDir}\GVFS.Service.exe"; AfterInstall: InstallGVFSService
62+
; Normal install: all files go to {app}, service gets AfterInstall callback
63+
DestDir: "{app}"; Flags: ignoreversion recursesubdirs; Source:"{#LayoutDir}\*"; Check: IsNormalInstall
64+
DestDir: "{app}"; Flags: ignoreversion; Source:"{#LayoutDir}\GVFS.Service.exe"; AfterInstall: InstallGVFSService; Check: IsNormalInstall
65+
; Staging install: most files go to {app}\PendingUpgrade, but GVFS.Service.exe
66+
; goes directly to {app} so the restarted service has PendingUpgradeHandler code.
67+
; The service is briefly stopped/restarted (mounts are independent processes).
68+
DestDir: "{app}\PendingUpgrade"; Flags: ignoreversion recursesubdirs; Source:"{#LayoutDir}\*"; Check: IsStagingInstall
69+
DestDir: "{app}"; Flags: ignoreversion; Source:"{#LayoutDir}\GVFS.Service.exe"; AfterInstall: StagingUpdateService; Check: IsStagingInstall
6470

6571
[Dirs]
6672
Name: "{app}\ProgramData\{#ServiceName}"; Permissions: users-readexec
@@ -84,6 +90,17 @@ Root: HKLM; SubKey: "{#GvFltAutologgerKey}"; Flags: deletekey
8490
[Code]
8591
var
8692
ExitCode: Integer;
93+
KeepMountsRunning: Boolean;
94+
95+
function IsNormalInstall(): Boolean;
96+
begin
97+
Result := not KeepMountsRunning;
98+
end;
99+
100+
function IsStagingInstall(): Boolean;
101+
begin
102+
Result := KeepMountsRunning;
103+
end;
87104
88105
function NeedsAddPath(Param: string): boolean;
89106
var
@@ -156,6 +173,55 @@ begin
156173
end;
157174
end;
158175
176+
procedure WaitForServiceProcessToExit(ServiceName: string);
177+
var
178+
ResultCode: integer;
179+
Attempts: integer;
180+
TempFile: string;
181+
QueryOutput: ansiString;
182+
begin
183+
// sc stop/delete returns before the service process actually exits.
184+
// Poll sc query until the service is fully gone (1060) or stopped.
185+
Attempts := 0;
186+
TempFile := ExpandConstant('{tmp}\~scquery.txt');
187+
while Attempts < 30 do
188+
begin
189+
if Exec(ExpandConstant('{cmd}'), '/C "' + ExpandConstant('{sys}\SC.EXE') + '" query ' + ServiceName + ' > "' + TempFile + '" 2>&1', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
190+
begin
191+
// 1060 = service does not exist (fully deleted and process exited)
192+
if ResultCode = 1060 then
193+
begin
194+
Log('WaitForServiceProcessToExit: Service no longer exists');
195+
break;
196+
end;
197+
if LoadStringFromFile(TempFile, QueryOutput) then
198+
begin
199+
if Pos('STOPPED', QueryOutput) > 0 then
200+
begin
201+
Log('WaitForServiceProcessToExit: Service is stopped');
202+
break;
203+
end;
204+
end;
205+
end
206+
else
207+
begin
208+
Log('WaitForServiceProcessToExit: sc query failed, assuming service is gone');
209+
break;
210+
end;
211+
Attempts := Attempts + 1;
212+
Log('WaitForServiceProcessToExit: Waiting for service to stop (attempt ' + IntToStr(Attempts) + ')');
213+
Sleep(1000);
214+
end;
215+
if Attempts >= 30 then
216+
begin
217+
if LoadStringFromFile(TempFile, QueryOutput) then
218+
Log('WaitForServiceProcessToExit: Timed out. Last sc query output: ' + QueryOutput)
219+
else
220+
Log('WaitForServiceProcessToExit: Timed out waiting for service to stop');
221+
end;
222+
DeleteFile(TempFile);
223+
end;
224+
159225
procedure UninstallService(ServiceName: string; ShowProgress: boolean);
160226
var
161227
ResultCode: integer;
@@ -178,6 +244,8 @@ begin
178244
RaiseException('Fatal: Could not uninstall service: ' + ServiceName);
179245
end;
180246
247+
WaitForServiceProcessToExit(ServiceName);
248+
181249
if (ShowProgress) then
182250
begin
183251
WizardForm.StatusLabel.Caption := 'Waiting for pending ' + ServiceName + ' deletion to complete. This may take a while.';
@@ -245,6 +313,33 @@ begin
245313
end;
246314
end;
247315
316+
procedure StagingUpdateService();
317+
var
318+
ResultCode: integer;
319+
StatusText: string;
320+
begin
321+
// In staging mode: the service was stopped in PrepareToInstall so its exe
322+
// could be replaced. Now start it with the new binary. The new service has
323+
// PendingUpgradeHandler which will complete the upgrade on next restart
324+
// when no mounts are running.
325+
StatusText := WizardForm.StatusLabel.Caption;
326+
WizardForm.StatusLabel.Caption := 'Starting GVFS.Service.';
327+
WizardForm.ProgressGauge.Style := npbstMarquee;
328+
329+
try
330+
Log('StagingUpdateService: Starting service with new binary');
331+
if not Exec(ExpandConstant('{sys}\SC.EXE'), 'start GVFS.Service', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
332+
begin
333+
Log('StagingUpdateService: Warning - could not start service: ' + SysErrorMessage(ResultCode));
334+
end;
335+
336+
WriteOnDiskVersion16CapableFile();
337+
finally
338+
WizardForm.StatusLabel.Caption := StatusText;
339+
WizardForm.ProgressGauge.Style := npbstNormal;
340+
end;
341+
end;
342+
248343
function DeleteFileIfItExists(FilePath: string) : Boolean;
249344
begin
250345
Result := False;
@@ -485,39 +580,6 @@ begin
485580
MigrateFile(CommonAppDataDir + '\{#ServiceName}\{#GVFSStatuscacheTokenFileName}', SecureAppDataDir + '\{#ServiceName}\{#GVFSStatuscacheTokenFileName}');
486581
end;
487582
488-
function ConfirmUnmountAll(): Boolean;
489-
var
490-
MsgBoxResult: integer;
491-
Repos: ansiString;
492-
ResultCode: integer;
493-
MsgBoxText: string;
494-
begin
495-
Result := False;
496-
if ExecWithResult('gvfs.exe', 'service --list-mounted', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, Repos) then
497-
begin
498-
if Repos = '' then
499-
begin
500-
Result := False;
501-
end
502-
else
503-
begin
504-
if ResultCode = 0 then
505-
begin
506-
MsgBoxText := 'The following repos are currently mounted:' + #13#10 + Repos + #13#10 + 'Setup needs to unmount all repos before it can proceed, and those repos will be unavailable while setup is running. Do you want to continue?';
507-
MsgBoxResult := SuppressibleMsgBox(MsgBoxText, mbConfirmation, MB_OKCANCEL, IDOK);
508-
if (MsgBoxResult = IDOK) then
509-
begin
510-
Result := True;
511-
end
512-
else
513-
begin
514-
Abort();
515-
end;
516-
end;
517-
end;
518-
end;
519-
end;
520-
521583
function EnsureGvfsNotRunning(): Boolean;
522584
var
523585
MsgBoxResult: integer;
@@ -647,12 +709,21 @@ begin
647709
case CurStep of
648710
ssInstall:
649711
begin
650-
UninstallService('GVFS.Service', True);
712+
if not KeepMountsRunning then
713+
UninstallService('GVFS.Service', True);
651714
end;
652715
ssPostInstall:
653716
begin
717+
if KeepMountsRunning then
718+
begin
719+
// All staged files have been written to PendingUpgrade.
720+
// Write .ready marker so the service knows the staging is
721+
// complete and safe to apply.
722+
SaveStringToFile(ExpandConstant('{app}\PendingUpgrade\.ready'), '', False);
723+
Log('CurStepChanged: Wrote PendingUpgrade .ready marker');
724+
end;
654725
MigrateConfigAndStatusCacheFiles();
655-
if ExpandConstant('{param:REMOUNTREPOS|true}') = 'true' then
726+
if (not KeepMountsRunning) and (ExpandConstant('{param:REMOUNTREPOS|true}') = 'true') then
656727
begin
657728
MountRepos();
658729
end
@@ -677,22 +748,125 @@ begin
677748
end;
678749
679750
function PrepareToInstall(var NeedsRestart: Boolean): String;
751+
var
752+
MsgBoxResult: integer;
753+
Repos: ansiString;
754+
ResultCode: integer;
755+
HasMounts: Boolean;
680756
begin
681757
NeedsRestart := False;
758+
KeepMountsRunning := False;
682759
Result := '';
683760
SetNuGetFeedIfNecessary();
684-
if ConfirmUnmountAll() then
761+
762+
// Check for mounted repos by querying the service, and also check for
763+
// running GVFS processes (a mount can be running without being registered
764+
// in the service's repo-registry, e.g., after a reinstall).
765+
HasMounts := False;
766+
if ExecWithResult('gvfs.exe', 'service --list-mounted', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, Repos) then
767+
begin
768+
if (ResultCode = 0) and (Repos <> '') then
769+
HasMounts := True;
770+
end;
771+
if (not HasMounts) and IsGVFSRunning() then
685772
begin
686-
if ExpandConstant('{param:REMOUNTREPOS|true}') = 'true' then
773+
HasMounts := True;
774+
Repos := '(GVFS processes detected)';
775+
Log('PrepareToInstall: No registered mounts but GVFS processes are running');
776+
end;
777+
778+
if HasMounts then
779+
begin
780+
if WizardSilent() then
687781
begin
688-
UnmountRepos();
782+
// Silent mode: STAGEIFMOUNTED=true stages files instead of unmounting.
783+
// Default: false (clean upgrade, matching pre-existing behavior).
784+
KeepMountsRunning := ExpandConstant('{param:STAGEIFMOUNTED|false}') = 'true';
785+
if KeepMountsRunning then
786+
Log('PrepareToInstall: Silent mode with mounted repos, KeepMountsRunning=True')
787+
else
788+
Log('PrepareToInstall: Silent mode with mounted repos, KeepMountsRunning=False');
689789
end
790+
else
791+
begin
792+
// Interactive mode: let user choose
793+
MsgBoxResult := SuppressibleMsgBox(
794+
'The following repos are currently mounted:' + #13#10 + Repos + #13#10#13#10 +
795+
'Click Yes to keep repos mounted during the upgrade.' + #13#10 +
796+
'The upgrade will complete automatically when all repos are unmounted.' + #13#10#13#10 +
797+
'Click No to unmount all repos now and upgrade without restart.' + #13#10 +
798+
'Repos will be temporarily unavailable during the upgrade.',
799+
mbConfirmation, MB_YESNOCANCEL, IDYES);
800+
if MsgBoxResult = IDYES then
801+
KeepMountsRunning := True
802+
else if MsgBoxResult = IDNO then
803+
KeepMountsRunning := False
804+
else
805+
begin
806+
Result := 'Installation cancelled.';
807+
exit;
808+
end;
809+
end;
690810
end;
691-
if not EnsureGvfsNotRunning() then
811+
812+
if KeepMountsRunning then
692813
begin
693-
Abort();
814+
// Staging mode: most files go to {app}\PendingUpgrade\ via [Files] entries
815+
// with Check: IsStagingInstall. GVFS.Service.exe goes directly to {app}.
816+
// Clean up any leftover staging dirs from a prior attempt first,
817+
// so we don't mix files from different upgrade versions.
818+
if DirExists(ExpandConstant('{app}\PendingUpgrade')) then
819+
begin
820+
Log('PrepareToInstall: Removing stale PendingUpgrade from prior staging attempt');
821+
DelTree(ExpandConstant('{app}\PendingUpgrade'), True, True, True);
822+
end;
823+
if DirExists(ExpandConstant('{app}\PreviousVersion')) then
824+
begin
825+
Log('PrepareToInstall: Removing stale PreviousVersion from prior staging attempt');
826+
DelTree(ExpandConstant('{app}\PreviousVersion'), True, True, True);
827+
end;
828+
// Stop the service now so its exe is unlocked for replacement.
829+
// Mounts are independent processes and unaffected.
830+
Log('PrepareToInstall: Staging mode. Stopping service for exe replacement.');
831+
StopService('GVFS.Service');
832+
WaitForServiceProcessToExit('GVFS.Service');
833+
end
834+
else
835+
begin
836+
// Clean upgrade: unmount, stop everything, replace files directly.
837+
// Remove any leftover PendingUpgrade or PreviousVersion from a
838+
// previous staging install so stale files don't interfere with
839+
// the fresh install.
840+
if DirExists(ExpandConstant('{app}\PendingUpgrade')) then
841+
begin
842+
Log('PrepareToInstall: Removing leftover PendingUpgrade directory');
843+
DelTree(ExpandConstant('{app}\PendingUpgrade'), True, True, True);
844+
end;
845+
if DirExists(ExpandConstant('{app}\PreviousVersion')) then
846+
begin
847+
Log('PrepareToInstall: Removing leftover PreviousVersion directory');
848+
DelTree(ExpandConstant('{app}\PreviousVersion'), True, True, True);
849+
end;
850+
if HasMounts then
851+
begin
852+
UnmountRepos();
853+
end;
854+
// With CloseApplications=no, Restart Manager won't kill GVFS
855+
// processes. If unmount-all didn't clean up everything (e.g.
856+
// registry was empty), force-kill remaining processes since
857+
// the user already consented to a full upgrade.
858+
if IsGVFSRunning() then
859+
begin
860+
Log('PrepareToInstall: GVFS processes still running after unmount, force-killing');
861+
Exec('powershell.exe', '-NoProfile "Get-Process gvfs,gvfs.mount -ErrorAction SilentlyContinue | Stop-Process -Force"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
862+
Sleep(2000);
863+
end;
864+
if not EnsureGvfsNotRunning() then
865+
begin
866+
Abort();
867+
end;
868+
StopService('GVFS.Service');
869+
UninstallGvFlt();
870+
UninstallProjFSIfNecessary();
694871
end;
695-
StopService('GVFS.Service');
696-
UninstallGvFlt();
697-
UninstallProjFSIfNecessary();
698872
end;

0 commit comments

Comments
 (0)