@@ -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