Skip to content

Mount: retry hook copy and tolerate transiently-locked hooks - #2075

Merged
tyrielv merged 1 commit into
microsoft:masterfrom
tyrielv:tyvella/fix-mount-hook-copy-retry
Aug 10, 2026
Merged

Mount: retry hook copy and tolerate transiently-locked hooks#2075
tyrielv merged 1 commit into
microsoft:masterfrom
tyrielv:tyvella/fix-mount-hook-copy-retry

Conversation

@tyrielv

@tyrielv tyrielv commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Problem

On the GVFS 2.0 line, mount startup fails roughly twice as often as the 1.0.26014.1 LKG 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/hooks 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 flags that failure retryable, but the mount-time call site failed immediately with no retry — unlike the clone-time InstallHooks path, which retries with exponential backoff via TryHooksInstallationAction. The failed update calls FailMountAndExit, so the mount process exits and the client's WaitUntilMounted handshake reports "Pipe is broken" / "exited before pipe ready".

Telemetry: inner exception Win32Exception (5): Failed to move '<hook>' to '<hook>' wrapped in RetryableException on 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 CopyHook in the existing TryHooksInstallationAction retry helper (3× exponential backoff), matching the clone-time path. A transient ACCESS_DENIED rename 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 static FileVersionInfo call, so the mount-time path can be unit-tested. The unused FileVersionInfo-returning GetVersionInfo/FileVersionsMatch/ProductVersionsMatch (and their mock overrides) are removed.

Observability

The locked-but-already-correct success path and the compare-failure path emit with Keywords.Telemetry and a stable HookUpdateResult field, so rollout can measure how often mounts coast through a transient lock.

Misc

Worktree behavior is unchanged (InProcessMount already skips hook install for worktrees). PhysicalFileSystem.TryCopyToTempFileAndRename is made virtual so the copy path can be unit-tested.

Tests

Adds GVFS.UnitTests.CommandLine.HooksInstallerMountUpdateTests:

  • identical hook is not copied
  • different / missing hook is copied
  • a transient copy failure is retried and then succeeds
  • a locked-but-already-correct hook does not fail the mount
  • a persistent copy failure still fails the mount
  • a compare failure refreshes the hook without failing the mount
  • a missing installed hook fails without copying

Full unit suite: 884 passed, 0 failed (11 pre-existing skips).

@tyrielv
tyrielv force-pushed the tyvella/fix-mount-hook-copy-retry branch 3 times, most recently from c8d9908 to df90e90 Compare August 5, 2026 22:51
@tyrielv tyrielv changed the title Mount: retry hook copy and stop unnecessary every-mount re-copy Mount: retry hook copy and tolerate transiently-locked hooks Aug 5, 2026
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
tyrielv force-pushed the tyvella/fix-mount-hook-copy-retry branch from df90e90 to b9b3ae1 Compare August 7, 2026 22:06
@tyrielv
tyrielv marked this pull request as ready for review August 7, 2026 22:31
@tyrielv
tyrielv merged commit c7fa968 into microsoft:master Aug 10, 2026
35 checks passed
This was referenced Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants