From 43681f7008efbfd50395e67e689174d28c068b75 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 22 May 2026 14:59:14 -0700 Subject: [PATCH] Add unique exit code for mount authentication failures When GVFS.Mount.exe fails due to authentication (credential fetch failure or 401/403 after credentialed retry), it now exits with ReturnCode.AuthenticationError (9) instead of GenericError (3). The gvfs.exe mount verb captures the GVFS.Mount.exe process handle and propagates its exit code on mount failure, so callers of 'gvfs mount' can distinguish auth failures from other errors. Changes: - Add ReturnCode.AuthenticationError = 9 - Add 'out bool isAuthFailure' to TryInitializeAndQueryGVFSConfig to explicitly classify auth failures (credential fetch failure or 401/403 on credentialed retry) - InProcessMount uses isAuthFailure to select the exit code - StartBackgroundVFS4GProcess returns Process so MountVerb can read the mount process exit code after failure Resolves AB#61375690 Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella --- GVFS/GVFS.Common/GVFSPlatform.cs | 3 +- GVFS/GVFS.Common/Git/GitAuthentication.cs | 16 +++++++-- GVFS/GVFS.Common/ReturnCode.cs | 1 + GVFS/GVFS.Mount/InProcessMount.cs | 7 ++-- GVFS/GVFS.Platform.Windows/WindowsPlatform.cs | 3 +- .../Mock/Common/MockPlatform.cs | 3 +- GVFS/GVFS/CommandLine/GVFSVerb.cs | 3 +- GVFS/GVFS/CommandLine/MountVerb.cs | 34 +++++++++++++++++-- 8 files changed, 59 insertions(+), 11 deletions(-) diff --git a/GVFS/GVFS.Common/GVFSPlatform.cs b/GVFS/GVFS.Common/GVFSPlatform.cs index 6723177ba..579e94955 100644 --- a/GVFS/GVFS.Common/GVFSPlatform.cs +++ b/GVFS/GVFS.Common/GVFSPlatform.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.IO.Pipes; @@ -51,7 +52,7 @@ public static void Register(GVFSPlatform platform) /// This method should only be called by processes whose code we own as the background process must /// do some extra work after it starts. /// - public abstract void StartBackgroundVFS4GProcess(ITracer tracer, string programName, string[] args); + public abstract Process StartBackgroundVFS4GProcess(ITracer tracer, string programName, string[] args); /// /// Adjusts the current process for running in the background. diff --git a/GVFS/GVFS.Common/Git/GitAuthentication.cs b/GVFS/GVFS.Common/Git/GitAuthentication.cs index 0a39715e7..faaafba07 100644 --- a/GVFS/GVFS.Common/Git/GitAuthentication.cs +++ b/GVFS/GVFS.Common/Git/GitAuthentication.cs @@ -200,7 +200,8 @@ public bool TryInitialize(ITracer tracer, Enlistment enlistment, out string erro enlistment, new RetryConfig(), out _, - out errorMessage); + out errorMessage, + out _); } /// @@ -217,7 +218,8 @@ public bool TryInitializeAndQueryGVFSConfig( Enlistment enlistment, RetryConfig retryConfig, out ServerGVFSConfig serverGVFSConfig, - out string errorMessage) + out string errorMessage, + out bool isAuthFailure) { if (this.isInitialized) { @@ -226,6 +228,7 @@ public bool TryInitializeAndQueryGVFSConfig( serverGVFSConfig = null; errorMessage = null; + isAuthFailure = false; using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) { @@ -253,6 +256,7 @@ public bool TryInitializeAndQueryGVFSConfig( if (!this.TryCallGitCredential(tracer, out errorMessage)) { + isAuthFailure = true; tracer.RelatedWarning("{0}: Credential fetch failed: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage); return false; } @@ -260,12 +264,18 @@ public bool TryInitializeAndQueryGVFSConfig( this.isInitialized = true; // Retry with credentials using the same ConfigHttpRequestor (reuses HttpClient/connection) - if (configRequestor.TryQueryGVFSConfig(true, out serverGVFSConfig, out _, out errorMessage)) + HttpStatusCode? retryHttpStatus; + if (configRequestor.TryQueryGVFSConfig(true, out serverGVFSConfig, out retryHttpStatus, out errorMessage)) { tracer.RelatedInfo("{0}: Config obtained with credentials", nameof(this.TryInitializeAndQueryGVFSConfig)); return true; } + if (retryHttpStatus == HttpStatusCode.Unauthorized || retryHttpStatus == HttpStatusCode.Forbidden) + { + isAuthFailure = true; + } + tracer.RelatedWarning("{0}: Config query failed with credentials: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage); return false; } diff --git a/GVFS/GVFS.Common/ReturnCode.cs b/GVFS/GVFS.Common/ReturnCode.cs index 5243cb2f5..09396a861 100644 --- a/GVFS/GVFS.Common/ReturnCode.cs +++ b/GVFS/GVFS.Common/ReturnCode.cs @@ -11,5 +11,6 @@ public enum ReturnCode UnableToRegisterForOfflineIO = 6, DehydrateFolderFailures = 7, MountAlreadyRunning = 8, + AuthenticationError = 9, } } diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index 24041dec3..824940a0f 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -133,10 +133,11 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords) Stopwatch sw = Stopwatch.StartNew(); ServerGVFSConfig config; string authConfigError; + bool isAuthFailure; if (!this.enlistment.Authentication.TryInitializeAndQueryGVFSConfig( this.tracer, this.enlistment, this.retryConfig, - out config, out authConfigError)) + out config, out authConfigError, out isAuthFailure)) { if (this.cacheServer != null && !string.IsNullOrWhiteSpace(this.cacheServer.Url)) { @@ -145,7 +146,9 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords) } else { - this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + authConfigError); + this.FailMountAndExit( + isAuthFailure ? ReturnCode.AuthenticationError : ReturnCode.GenericError, + "Unable to query /gvfs/config" + Environment.NewLine + authConfigError); } } diff --git a/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs b/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs index b2f0b4533..d262a7953 100644 --- a/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs +++ b/GVFS/GVFS.Platform.Windows/WindowsPlatform.cs @@ -137,7 +137,7 @@ public override string GetLogsDirectoryForGVFSComponent(string componentName) return WindowsPlatform.GetLogsDirectoryForGVFSComponentImplementation(componentName); } - public override void StartBackgroundVFS4GProcess(ITracer tracer, string programName, string[] args) + public override Process StartBackgroundVFS4GProcess(ITracer tracer, string programName, string[] args) { string programArguments = string.Empty; try @@ -156,6 +156,7 @@ public override void StartBackgroundVFS4GProcess(ITracer tracer, string programN Process executingProcess = new Process(); executingProcess.StartInfo = processInfo; executingProcess.Start(); + return executingProcess; } catch (Exception ex) { diff --git a/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs b/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs index 7e6e32d3e..8c153a424 100644 --- a/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs +++ b/GVFS/GVFS.UnitTests/Mock/Common/MockPlatform.cs @@ -6,6 +6,7 @@ using GVFS.UnitTests.Mock.Git; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Pipes; using System.Runtime.InteropServices; @@ -152,7 +153,7 @@ public override bool TryGetDefaultLocalCacheRoot(string enlistmentRoot, out stri throw new NotImplementedException(); } - public override void StartBackgroundVFS4GProcess(ITracer tracer, string programName, string[] args) + public override Process StartBackgroundVFS4GProcess(ITracer tracer, string programName, string[] args) { throw new NotSupportedException(); } diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index 9aa112313..c44608daf 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -243,7 +243,8 @@ protected bool TryAuthenticateAndQueryGVFSConfig( enlistment, retryConfig ?? new RetryConfig(), out config, - out error), + out error, + out _), "Authenticating", enlistment.EnlistmentRoot); diff --git a/GVFS/GVFS/CommandLine/MountVerb.cs b/GVFS/GVFS/CommandLine/MountVerb.cs index 8ee8a51ac..edaf0b85c 100644 --- a/GVFS/GVFS/CommandLine/MountVerb.cs +++ b/GVFS/GVFS/CommandLine/MountVerb.cs @@ -4,6 +4,7 @@ using GVFS.Common.Tracing; using GVFS.DiskLayoutUpgrades; using System; +using System.Diagnostics; using System.IO; using System.Threading; @@ -12,6 +13,7 @@ namespace GVFS.CommandLine public class MountVerb : GVFSVerb.ForExistingEnlistment { private const string MountVerbName = "mount"; + private Process mountProcess; public string Verbosity { get; set; } @@ -197,9 +199,37 @@ protected override void Execute(GVFSEnlistment enlistment) () => { return this.TryMount(tracer, enlistment, mountExecutableLocation, out errorMessage); }, "Mounting")) { - this.ReportErrorAndExit(tracer, errorMessage); + ReturnCode mountExitCode = ReturnCode.GenericError; + if (this.mountProcess != null) + { + try + { + if (!this.mountProcess.HasExited) + { + this.mountProcess.WaitForExit(1000); + } + + if (this.mountProcess.HasExited) + { + mountExitCode = (ReturnCode)this.mountProcess.ExitCode; + } + } + catch (InvalidOperationException) + { + } + finally + { + this.mountProcess.Dispose(); + this.mountProcess = null; + } + } + + this.ReportErrorAndExit(tracer, mountExitCode, errorMessage); } + this.mountProcess?.Dispose(); + this.mountProcess = null; + if (!this.Unattended) { tracer.RelatedInfo($"{nameof(this.Execute)}: Registering for automount"); @@ -229,7 +259,7 @@ private bool TryMount(ITracer tracer, GVFSEnlistment enlistment, string mountExe tracer.RelatedInfo($"{nameof(this.TryMount)}: Launching background process('{mountExecutableLocation}') for {mountPath}"); - GVFSPlatform.Instance.StartBackgroundVFS4GProcess( + this.mountProcess = GVFSPlatform.Instance.StartBackgroundVFS4GProcess( tracer, mountExecutableLocation, new[]