Skip to content

Commit 58a00e8

Browse files
committed
Fix staged upgrade races and installer error handling
Address review feedback from #1958: - StopService/StagingUpdateService: check sc.exe exit code separately from Exec launch failure. Previously non-zero sc.exe results were silently ignored. - Timer race: wrap pendingUpgradeTimer create/reset in a lock. Two concurrent unmounts on separate pipe threads could both observe null and create duplicate timers, causing parallel TryApplyPendingUpgrade. - .ready race: move service start from AfterInstall hook to ssPostInstall after .ready marker is written. Previously the service could start its 5s debounce timer before .ready existed, skip the upgrade, and leave staged files until the next service restart. Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent 3a0f1b2 commit 58a00e8

2 files changed

Lines changed: 39 additions & 19 deletions

File tree

GVFS/GVFS.Installers/Setup.iss

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ DestDir: "{app}"; Flags: ignoreversion; Source:"{#LayoutDir}\GVFS.Service.exe";
6666
; goes directly to {app} so the restarted service has PendingUpgradeHandler code.
6767
; The service is briefly stopped/restarted (mounts are independent processes).
6868
DestDir: "{app}\PendingUpgrade"; Flags: ignoreversion recursesubdirs; Source:"{#LayoutDir}\*"; Check: IsStagingInstall
69-
DestDir: "{app}"; Flags: ignoreversion; Source:"{#LayoutDir}\GVFS.Service.exe"; AfterInstall: StagingUpdateService; Check: IsStagingInstall
69+
DestDir: "{app}"; Flags: ignoreversion; Source:"{#LayoutDir}\GVFS.Service.exe"; Check: IsStagingInstall
7070

7171
[Dirs]
7272
Name: "{app}\ProgramData\{#ServiceName}"; Permissions: users-readexec
@@ -166,11 +166,17 @@ var
166166
ResultCode: integer;
167167
begin
168168
Log('StopService: stopping: ' + ServiceName);
169-
// ErrorCode 1060 means service not installed, 1062 means service not started
170-
if not Exec(ExpandConstant('{sys}\SC.EXE'), 'stop ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode <> 1060) and (ResultCode <> 1062) then
169+
if not Exec(ExpandConstant('{sys}\SC.EXE'), 'stop ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
171170
begin
171+
Log('StopService: Failed to launch sc.exe');
172172
RaiseException('Fatal: Could not stop service: ' + ServiceName);
173173
end;
174+
// 1060 = service not installed, 1062 = service not started
175+
if (ResultCode <> 0) and (ResultCode <> 1060) and (ResultCode <> 1062) then
176+
begin
177+
Log('StopService: sc stop returned error code ' + IntToStr(ResultCode));
178+
RaiseException('Fatal: Could not stop service: ' + ServiceName + ' (exit code ' + IntToStr(ResultCode) + ')');
179+
end;
174180
end;
175181
176182
procedure WaitForServiceProcessToExit(ServiceName: string);
@@ -328,9 +334,14 @@ begin
328334
329335
try
330336
Log('StagingUpdateService: Starting service with new binary');
331-
if not Exec(ExpandConstant('{sys}\SC.EXE'), 'start GVFS.Service', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
337+
if Exec(ExpandConstant('{sys}\SC.EXE'), 'start GVFS.Service', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then
338+
begin
339+
if ResultCode <> 0 then
340+
Log('StagingUpdateService: Warning - sc start returned error code ' + IntToStr(ResultCode));
341+
end
342+
else
332343
begin
333-
Log('StagingUpdateService: Warning - could not start service: ' + SysErrorMessage(ResultCode));
344+
Log('StagingUpdateService: Warning - could not launch sc.exe');
334345
end;
335346
336347
WriteOnDiskVersion16CapableFile();
@@ -721,6 +732,11 @@ begin
721732
// complete and safe to apply.
722733
SaveStringToFile(ExpandConstant('{app}\PendingUpgrade\.ready'), '', False);
723734
Log('CurStepChanged: Wrote PendingUpgrade .ready marker');
735+
736+
// Start the service AFTER .ready is written. Previously this
737+
// was an AfterInstall hook on GVFS.Service.exe, but that races:
738+
// the service's debounce timer could fire before .ready exists.
739+
StagingUpdateService();
724740
end;
725741
MigrateConfigAndStatusCacheFiles();
726742
if (not KeepMountsRunning) and (ExpandConstant('{param:REMOUNTREPOS|true}') = 'true') then

GVFS/GVFS.Service/Handlers/RequestHandler.cs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class RequestHandler
2929
private ITracer tracer;
3030
private IRepoRegistry repoRegistry;
3131
private Timer pendingUpgradeTimer;
32+
private readonly object pendingUpgradeTimerLock = new object();
3233

3334
public RequestHandler(ITracer tracer, string etwArea, IRepoRegistry repoRegistry)
3435
{
@@ -146,21 +147,24 @@ private void TryDeferredPendingUpgradeCheck(ITracer tracer)
146147
// Debounce: reset the timer on each unmount so the check fires
147148
// once after the last unmount settles. If multiple repos unmount
148149
// in quick succession, only one upgrade attempt runs.
149-
if (this.pendingUpgradeTimer == null)
150+
lock (this.pendingUpgradeTimerLock)
150151
{
151-
this.pendingUpgradeTimer = new Timer(
152-
_ =>
153-
{
154-
tracer.RelatedInfo("TryDeferredPendingUpgradeCheck: Checking pending upgrade after unmount");
155-
PendingUpgradeHandler.TryApplyPendingUpgrade(tracer);
156-
},
157-
null,
158-
PendingUpgradeDelayMs,
159-
Timeout.Infinite);
160-
}
161-
else
162-
{
163-
this.pendingUpgradeTimer.Change(PendingUpgradeDelayMs, Timeout.Infinite);
152+
if (this.pendingUpgradeTimer == null)
153+
{
154+
this.pendingUpgradeTimer = new Timer(
155+
_ =>
156+
{
157+
tracer.RelatedInfo("TryDeferredPendingUpgradeCheck: Checking pending upgrade after unmount");
158+
PendingUpgradeHandler.TryApplyPendingUpgrade(tracer);
159+
},
160+
null,
161+
PendingUpgradeDelayMs,
162+
Timeout.Infinite);
163+
}
164+
else
165+
{
166+
this.pendingUpgradeTimer.Change(PendingUpgradeDelayMs, Timeout.Infinite);
167+
}
164168
}
165169
}
166170
}

0 commit comments

Comments
 (0)