Mount: retry hook copy and tolerate transiently-locked hooks - #2075
Merged
tyrielv merged 1 commit intoAug 10, 2026
Conversation
tyrielv
force-pushed
the
tyvella/fix-mount-hook-copy-retry
branch
3 times, most recently
from
August 5, 2026 22:51
c8d9908 to
df90e90
Compare
On the GVFS 2.0 line, mount startup failed roughly twice as often as the 1.0.26014.1 LKG. The dominant 2.0-specific cause is the hook update on mount. After a GVFS upgrade changes a native hook binary, the next mount re-copies it into the enlistment via a copy-to-temp-then-rename (HooksInstaller.TryUpdateHook). The rename can fail transiently with Win32Exception (5) ERROR_ACCESS_DENIED when the existing enlistment hook is locked (open handle, AV scan). CopyHook wraps that in a retryable RetryableException, but the mount-time call site failed immediately with no retry - unlike the clone-time InstallHooks path, which retries with backoff. So a transient lock failed the whole mount. Fix 1 - retry at mount time (primary): wrap the mount-time CopyHook in the existing TryHooksInstallationAction retry helper (3x exponential backoff), matching the clone-time path. A transient ACCESS_DENIED rename is now retried, not fatal. Fix 2 - tolerate locked-but-already-correct: after retries are exhausted, if the enlistment hook already matches the installed one, treat it as success instead of failing the mount. Fix 3 - compare path resilience: reading the hook version opens the hook files, which can be transiently locked too. A compare failure no longer hard-fails the mount; it logs a telemetry warning and falls through to the resilient copy path. Change detection compares the hook FileVersion. These native (C++) hook binaries embed their GVFS version in the PE version resource, so the version differs only when a GVFS upgrade changed the hook - which is rare (roughly monthly) compared to daily mounts - so the common mount does no copy. The comparison reads the version through a small context.FileSystem.GetFileVersion seam instead of a direct static FileVersionInfo call, so the mount-time path can be unit-tested. A null/empty version is treated as "cannot confirm identical" (not a match) so a version-less binary forces the resilient copy rather than being assumed correct. The unused FileVersionInfo-returning GetVersionInfo/FileVersionsMatch/ ProductVersionsMatch (and their mock overrides) are removed. Every mount-hook outcome (MissingFromEnlistment, CompareFailed, LockedButAlreadyCorrect, CopyFailed) now emits with Keywords.Telemetry and a stable HookUpdateResult field so all outcomes are queryable together; previously the missing-hook warning bound to the params-object overload and silently dropped its metadata. Worktree behavior is unchanged (InProcessMount already skips hook install for worktrees). PhysicalFileSystem.TryCopyToTempFileAndRename is made virtual so the copy path can be unit-tested. Adds GVFS.UnitTests HooksInstallerMountUpdateTests covering: identical hook not copied, different/missing hook copied, copy-when-version-differs-despite-same- content, no-copy-when-version-matches-despite-different-content, copy-when-both- versions-null, transient copy failure retried then succeeds, locked-but-already- correct hook does not fail the mount, persistent copy failure still fails, a compare failure refreshes the hook without failing, and a missing installed hook fails without copying. Full unit suite: 887 passed. Assisted-by: Claude Opus 4.8 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
tyrielv
force-pushed
the
tyvella/fix-mount-hook-copy-retry
branch
from
August 7, 2026 22:06
df90e90 to
b9b3ae1
Compare
tyrielv
marked this pull request as ready for review
August 7, 2026 22:31
Keith Klein (KeithIsSleeping)
approved these changes
Aug 8, 2026
This was referenced Aug 10, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On the GVFS 2.0 line, mount startup fails roughly twice as often as the
1.0.26014.1LKG it will replace. The dominant 2.0-specific actionable cause is the mount-time git-hook update.After a GVFS upgrade changes a native hook binary, the next mount re-copies it into the enlistment's
.git/hooksvia a copy-to-temp-then-rename (HooksInstaller.TryUpdateHook). The rename can fail transiently withWin32Exception (5)ERROR_ACCESS_DENIEDwhen the existing enlistment hook is locked (open handle, AV scan).CopyHookflags that failure retryable, but the mount-time call site failed immediately with no retry — unlike the clone-timeInstallHookspath, which retries with exponential backoff viaTryHooksInstallationAction. The failed update callsFailMountAndExit, so the mount process exits and the client'sWaitUntilMountedhandshake reports "Pipe is broken" / "exited before pipe ready".Telemetry: inner exception
Win32Exception (5): Failed to move '<hook>' to '<hook>'wrapped inRetryableExceptionon the "Failed to copy<hook>to enlistment" error; 28 machines / 332 events, all on 2.x, zero on 1.x.Fix
1 — Retry the hook copy at mount time (primary). Wrap the mount-time
CopyHookin the existingTryHooksInstallationActionretry helper (3× exponential backoff), matching the clone-time path. A transientACCESS_DENIEDrename is now retried, not fatal to the mount.2 — Tolerate locked-but-already-correct. After retries are exhausted, if the enlistment hook already matches the installed one, treat it as success instead of
FailMountAndExit— a locked-but-already-correct hook must not kill the mount.3 — Compare-path resilience. Reading the hook version opens the enlistment hook, which can be transiently locked too. A compare failure no longer hard-fails the mount; it logs a telemetry warning and falls through to the resilient copy path (retry, then the already-correct recheck).
Change detection
The change detection still compares the hook FileVersion. These native (C++) hook binaries embed their GVFS version in the PE version resource, so the version differs only when a GVFS upgrade changed the hook — rare (~monthly) compared to daily mounts — so the common mount does no copy, and a copy happens on the first mount after an upgrade. (Confirmed against the installed 2.0.26196.1 hooks: the version resource is present and correct, so the FileVersion check is reliable for these binaries.)
The comparison now reads the version through a small
context.FileSystem.GetFileVersion(path)seam instead of a direct staticFileVersionInfocall, so the mount-time path can be unit-tested. The unusedFileVersionInfo-returningGetVersionInfo/FileVersionsMatch/ProductVersionsMatch(and their mock overrides) are removed.Observability
The locked-but-already-correct success path and the compare-failure path emit with
Keywords.Telemetryand a stableHookUpdateResultfield, so rollout can measure how often mounts coast through a transient lock.Misc
Worktree behavior is unchanged (
InProcessMountalready skips hook install for worktrees).PhysicalFileSystem.TryCopyToTempFileAndRenameis madevirtualso the copy path can be unit-tested.Tests
Adds
GVFS.UnitTests.CommandLine.HooksInstallerMountUpdateTests:Full unit suite: 884 passed, 0 failed (11 pre-existing skips).