Skip to content

Commit 4ef6893

Browse files
committed
Apply staged upgrade when mount processes exit
The installer replaces GVFS.Service.exe in-place even during a staged upgrade, so the running service is always the new version. Add a PendingUpgradeMonitor that registers Process.Exited callbacks on GVFS.Mount processes and applies the staged upgrade once all have exited. This is event-driven (no polling) and works regardless of which version of gvfs.exe triggered the unmount. Previously only 'gvfs unmount' triggered the upgrade check (via UnregisterRepoRequest). 'gvfs service --unmount-all' sets SkipUnregister=true to preserve automount registration, so no message reached the service and staged upgrades sat in PendingUpgrade\ indefinitely. Changes: - PendingUpgradeHandler: return UpgradeResult enum instead of void, add concurrency lock, add IsPending() helper - PendingUpgradeMonitor: new class — monitors mount process exits, debounces (5s), retries on new mounts, self-stops on completion - GVFSService: start monitor when startup upgrade is deferred, dispose on shutdown - upgrade-tests.yaml: add unmount-all-triggers-upgrade CI scenario Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent 3c18fe5 commit 4ef6893

5 files changed

Lines changed: 409 additions & 22 deletions

File tree

.github/workflows/upgrade-tests.yaml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
- double-staging
3434
- staging-then-clean
3535
- mount-safety-deferral
36+
- unmount-all-triggers-upgrade
3637
fail-fast: false
3738

3839
steps:
@@ -274,6 +275,36 @@ jobs:
274275
Write-Host "PASS: Mount safety deferral works correctly"
275276
}
276277
278+
"unmount-all-triggers-upgrade" {
279+
Write-Host "=== Scenario: unmount-all triggers staged upgrade ==="
280+
# Install LKG, mount, staging upgrade with new installer (which
281+
# replaces GVFS.Service.exe in-place with the new version that
282+
# includes PendingUpgradeMonitor). Then unmount via --unmount-all.
283+
# The new service monitors mount process exits and applies the
284+
# upgrade automatically — no pipe message from gvfs.exe needed.
285+
Install-GVFS $lkgInstaller
286+
Assert-ServiceRunning
287+
$mountPid = Mount-TestRepo
288+
289+
Install-GVFS $newInstaller @("/STAGEIFMOUNTED=true")
290+
Assert-MountAlive $mountPid
291+
Assert-PendingUpgrade $true
292+
293+
# Unmount via --unmount-all (uses LKG gvfs.exe — no new pipe msg)
294+
& "$installDir\gvfs.exe" service --unmount-all 2>&1 | Write-Host
295+
if ($LASTEXITCODE -ne 0) { throw "unmount-all failed" }
296+
297+
# The monitor's debounce timer fires 5s after the last mount
298+
# process exits, then applies the upgrade. Wait for completion.
299+
$deadline = (Get-Date).AddSeconds(30)
300+
while ((Test-Path "$installDir\PendingUpgrade") -and (Get-Date) -lt $deadline) {
301+
Start-Sleep -Seconds 2
302+
}
303+
304+
Assert-PendingUpgrade $false
305+
Write-Host "PASS: unmount-all triggers staged upgrade via process monitor"
306+
}
307+
277308
default {
278309
throw "Unknown scenario: ${{ matrix.scenario }}"
279310
}

GVFS/GVFS.Service/GVFSService.Windows.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class GVFSService : ServiceBase
2626
private RepoRegistry repoRegistry;
2727
private WindowsRequestHandler requestHandler;
2828
private INotificationHandler notificationHandler;
29+
private PendingUpgradeMonitor pendingUpgradeMonitor;
2930

3031
public GVFSService(JsonTracer tracer)
3132
{
@@ -46,8 +47,14 @@ public void Run()
4647
// Check for a staged upgrade before doing anything else.
4748
// If no GVFS.Mount processes are running (typical at boot or after
4849
// unmount-all), copy staged files in-place and proceed normally.
49-
// If mounts ARE running, the upgrade is deferred to next restart.
50-
PendingUpgradeHandler.TryApplyPendingUpgrade(this.tracer);
50+
// If mounts ARE running, start a monitor that will apply the
51+
// upgrade when all mount processes exit.
52+
UpgradeResult upgradeResult = PendingUpgradeHandler.TryApplyPendingUpgrade(this.tracer);
53+
if (upgradeResult == UpgradeResult.DeferredMountsRunning)
54+
{
55+
this.pendingUpgradeMonitor = new PendingUpgradeMonitor(this.tracer);
56+
this.pendingUpgradeMonitor.Start();
57+
}
5158

5259
this.repoRegistry = new RepoRegistry(
5360
this.tracer,
@@ -99,6 +106,12 @@ public void StopRunning()
99106
this.tracer.RelatedInfo("Stopping");
100107
}
101108

109+
if (this.pendingUpgradeMonitor != null)
110+
{
111+
this.pendingUpgradeMonitor.Dispose();
112+
this.pendingUpgradeMonitor = null;
113+
}
114+
102115
if (this.serviceStopped != null)
103116
{
104117
this.serviceStopped.Set();

GVFS/GVFS.Service/Handlers/RequestHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ namespace GVFS.Service.Handlers
1515
/// </summary>
1616
public class RequestHandler
1717
{
18-
private const int PendingUpgradeDelayMs = 5000;
18+
private const int PendingUpgradeDelayMs = 2000;
1919

2020
protected const string EnableProjFSRequestDescription = "attach volume";
2121
protected string requestDescription;

GVFS/GVFS.Service/PendingUpgradeHandler.cs

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ namespace GVFS.Service
1414
/// When the installer runs with mounts active, it stages new files to
1515
/// {installDir}\PendingUpgrade\ instead of replacing files in-place.
1616
/// This class applies the upgrade when no GVFS.Mount processes are
17-
/// running — either on service start (before automount) or after a
18-
/// repo unmount (via deferred check from RequestHandler).
17+
/// running — either on service start (before automount), after a
18+
/// repo unmount (via deferred check from RequestHandler), or when
19+
/// PendingUpgradeMonitor detects all mount processes have exited.
1920
///
2021
/// 1. Move old files from install dir → PreviousVersion\
2122
/// 2. Move new files from PendingUpgrade\ → install dir
@@ -36,6 +37,9 @@ public static class PendingUpgradeHandler
3637
private const string Phase1CompleteMarkerFileName = ".phase1-complete";
3738
private const string ServiceExeName = "GVFS.Service.exe";
3839
private const string MountProcessName = "GVFS.Mount";
40+
private const string MountExeName = "GVFS.Mount.exe";
41+
42+
private static readonly object ApplyLock = new object();
3943

4044
// Executables that users or the service can launch to start new
4145
// mount/hook processes. During upgrade these are moved out first
@@ -53,22 +57,89 @@ public static class PendingUpgradeHandler
5357
/// <summary>
5458
/// Checks for and applies a pending staged upgrade.
5559
/// </summary>
56-
public static void TryApplyPendingUpgrade(ITracer tracer)
60+
public static UpgradeResult TryApplyPendingUpgrade(ITracer tracer)
61+
{
62+
lock (ApplyLock)
63+
{
64+
return TryApplyPendingUpgradeLocked(tracer);
65+
}
66+
}
67+
68+
/// <summary>
69+
/// Returns true if a PendingUpgrade directory with a .ready marker exists.
70+
/// </summary>
71+
public static bool IsPending()
72+
{
73+
string pendingUpgradeDir = Path.Combine(Configuration.AssemblyPath, PendingUpgradeDirectoryName);
74+
if (!Directory.Exists(pendingUpgradeDir))
75+
{
76+
return false;
77+
}
78+
79+
string readyMarker = Path.Combine(pendingUpgradeDir, ReadyMarkerFileName);
80+
return File.Exists(readyMarker);
81+
}
82+
83+
/// <summary>
84+
/// Returns GVFS.Mount processes whose executable is in the install
85+
/// directory. Processes from dev builds or other installs are excluded
86+
/// so they don't block upgrades of the system install. If a process's
87+
/// path cannot be read (access denied, 32/64-bit mismatch), it is
88+
/// included conservatively.
89+
/// Caller must dispose the returned Process objects.
90+
/// </summary>
91+
public static List<Process> GetInstalledMountProcesses(ITracer tracer)
92+
{
93+
string installDir = Configuration.AssemblyPath;
94+
string expectedPath = Path.Combine(installDir, MountExeName);
95+
Process[] allMountProcesses = Process.GetProcessesByName(MountProcessName);
96+
List<Process> installed = new List<Process>();
97+
98+
foreach (Process process in allMountProcesses)
99+
{
100+
bool include = true;
101+
try
102+
{
103+
string processPath = process.MainModule?.FileName;
104+
if (processPath != null &&
105+
!PathComparer.Equals(processPath, expectedPath))
106+
{
107+
include = false;
108+
tracer.RelatedInfo(
109+
$"{nameof(PendingUpgradeHandler)}: Skipping GVFS.Mount PID {process.Id} " +
110+
$"(path: {processPath}, not in install dir)");
111+
}
112+
}
113+
catch (Exception)
114+
{
115+
// Access denied or process exited — include conservatively
116+
}
117+
118+
if (include)
119+
{
120+
installed.Add(process);
121+
}
122+
else
123+
{
124+
process.Dispose();
125+
}
126+
}
127+
128+
return installed;
129+
}
130+
131+
private static UpgradeResult TryApplyPendingUpgradeLocked(ITracer tracer)
57132
{
58133
string installDir = Configuration.AssemblyPath;
59134
string pendingUpgradeDir = Path.Combine(installDir, PendingUpgradeDirectoryName);
60135
string previousVersionDir = Path.Combine(installDir, PreviousVersionDirectoryName);
61136

62137
if (!Directory.Exists(pendingUpgradeDir))
63138
{
64-
// No pending upgrade. Clean up PreviousVersion if it exists
65-
// (leftover from a completed upgrade where cleanup was interrupted).
66139
TryDeleteDirectory(tracer, previousVersionDir, "leftover PreviousVersion");
67-
return;
140+
return UpgradeResult.NoPending;
68141
}
69142

70-
// Installer writes .ready marker as its last step. If missing,
71-
// the installer was interrupted mid-write — don't apply partial files.
72143
string readyMarker = Path.Combine(pendingUpgradeDir, ReadyMarkerFileName);
73144
if (!File.Exists(readyMarker))
74145
{
@@ -79,28 +150,25 @@ public static void TryApplyPendingUpgrade(ITracer tracer)
79150
$"{nameof(PendingUpgradeHandler)}: PendingUpgrade directory exists but {ReadyMarkerFileName} marker " +
80151
"is missing — installer was likely interrupted. Skipping until next install completes.",
81152
Keywords.Telemetry);
82-
return;
153+
return UpgradeResult.NotReady;
83154
}
84155

85156
tracer.RelatedInfo($"{nameof(PendingUpgradeHandler)}: Pending upgrade detected at {pendingUpgradeDir}");
86157

87-
// Don't apply if GVFS.Mount processes are still running — their
88-
// executables are locked and moves would fail. Upgrade will be
89-
// retried on next service start when no mounts are active.
90-
Process[] mountProcesses = Array.Empty<Process>();
158+
List<Process> mountProcesses = new List<Process>();
91159
try
92160
{
93-
mountProcesses = Process.GetProcessesByName(MountProcessName);
94-
if (mountProcesses.Length > 0)
161+
mountProcesses = GetInstalledMountProcesses(tracer);
162+
if (mountProcesses.Count > 0)
95163
{
96164
EventMetadata deferMetadata = new EventMetadata();
97-
deferMetadata.Add("MountProcessCount", mountProcesses.Length);
165+
deferMetadata.Add("MountProcessCount", mountProcesses.Count);
98166
tracer.RelatedEvent(
99167
EventLevel.Informational,
100168
$"{nameof(PendingUpgradeHandler)}_Deferred",
101169
deferMetadata,
102170
Keywords.Telemetry);
103-
return;
171+
return UpgradeResult.DeferredMountsRunning;
104172
}
105173
}
106174
finally
@@ -217,7 +285,7 @@ public static void TryApplyPendingUpgrade(ITracer tracer)
217285
$"{nameof(PendingUpgradeHandler)}_Complete",
218286
successMetadata,
219287
Keywords.Telemetry);
220-
return;
288+
return UpgradeResult.Applied;
221289
}
222290
catch (Exception ex)
223291
{
@@ -229,7 +297,7 @@ public static void TryApplyPendingUpgrade(ITracer tracer)
229297
"PendingUpgrade retained for retry on next service start. " +
230298
"If PreviousVersion exists, old files are preserved for manual recovery.",
231299
Keywords.Telemetry);
232-
return;
300+
return UpgradeResult.Failed;
233301
}
234302
}
235303

@@ -440,4 +508,13 @@ private static void TryDeleteDirectory(ITracer tracer, string path, string descr
440508
}
441509
}
442510
}
511+
512+
public enum UpgradeResult
513+
{
514+
NoPending,
515+
Applied,
516+
DeferredMountsRunning,
517+
NotReady,
518+
Failed,
519+
}
443520
}

0 commit comments

Comments
 (0)