diff --git a/GVFS/GVFS.Common/Git/GitAuthentication.cs b/GVFS/GVFS.Common/Git/GitAuthentication.cs index 58934b849..4e2feb51b 100644 --- a/GVFS/GVFS.Common/Git/GitAuthentication.cs +++ b/GVFS/GVFS.Common/Git/GitAuthentication.cs @@ -13,9 +13,11 @@ namespace GVFS.Common.Git public class GitAuthentication { private const double MaxBackoffSeconds = 30; + private const int DefaultCredentialTimeoutMs = 30_000; private readonly Lock gitAuthLock = new Lock(); private readonly ICredentialStore credentialStore; + private readonly GitProcess gitProcess; private readonly string repoUrl; private int numberOfAttempts = 0; @@ -29,6 +31,7 @@ public class GitAuthentication public GitAuthentication(GitProcess git, string repoUrl) { this.credentialStore = git; + this.gitProcess = git; this.repoUrl = repoUrl; if (git.TryGetConfigUrlMatch("http", this.repoUrl, out Dictionary configSettings)) @@ -213,6 +216,12 @@ public bool TryInitialize(ITracer tracer, Enlistment enlistment, out string erro /// This saves one HTTP request compared to probing auth separately /// and then querying config, and reuses the same TCP/TLS connection. /// + /// + /// When true, config queries use a single attempt instead of the full + /// retry loop. Use this when a cache server is already configured + /// locally — mount can proceed without config on first failure rather + /// than blocking on retries that will be masked by the fallback. + /// public bool TryInitializeAndQueryGVFSConfig( ITracer tracer, Enlistment enlistment, @@ -220,7 +229,9 @@ public bool TryInitializeAndQueryGVFSConfig( out ServerGVFSConfig serverGVFSConfig, out string errorMessage, out bool isAuthFailure, - Action updateProgress = null) + Action updateProgress = null, + bool skipRetries = false, + int credentialTimeoutMs = DefaultCredentialTimeoutMs) { if (this.isInitialized) { @@ -231,7 +242,11 @@ public bool TryInitializeAndQueryGVFSConfig( errorMessage = null; isAuthFailure = false; - using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) + RetryConfig effectiveRetryConfig = skipRetries + ? new RetryConfig(maxRetries: 0, retryConfig.Timeout) + : retryConfig; + + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, effectiveRetryConfig)) { HttpStatusCode? httpStatus; @@ -256,7 +271,7 @@ public bool TryInitializeAndQueryGVFSConfig( this.IsAnonymous = false; updateProgress?.Invoke("Fetching credentials"); - if (!this.TryCallGitCredential(tracer, out errorMessage)) + if (!this.TryCallGitCredential(tracer, out errorMessage, credentialTimeoutMs)) { isAuthFailure = true; updateProgress?.Invoke("Credential fetch failed: " + errorMessage); @@ -379,11 +394,11 @@ private void UpdateBackoff() this.numberOfAttempts++; } - private bool TryCallGitCredential(ITracer tracer, out string errorMessage) + private bool TryCallGitCredential(ITracer tracer, out string errorMessage, int timeoutMs = -1) { string gitUsername; string gitPassword; - if (!this.credentialStore.TryGetCredential(tracer, this.repoUrl, out gitUsername, out gitPassword, out errorMessage)) + if (!this.gitProcess.TryGetCredential(tracer, this.repoUrl, out gitUsername, out gitPassword, out errorMessage, timeoutMs)) { this.UpdateBackoff(); return false; diff --git a/GVFS/GVFS.Common/Git/GitProcess.cs b/GVFS/GVFS.Common/Git/GitProcess.cs index 81aef41ba..ec4692d47 100644 --- a/GVFS/GVFS.Common/Git/GitProcess.cs +++ b/GVFS/GVFS.Common/Git/GitProcess.cs @@ -298,6 +298,17 @@ public virtual bool TryGetCredential( out string username, out string password, out string errorMessage) + { + return this.TryGetCredential(tracer, repoUrl, out username, out password, out errorMessage, timeoutMs: -1); + } + + public virtual bool TryGetCredential( + ITracer tracer, + string repoUrl, + out string username, + out string password, + out string errorMessage, + int timeoutMs) { username = null; password = null; @@ -311,16 +322,29 @@ public virtual bool TryGetCredential( GenerateCredentialVerbCommand("fill"), stdin => stdin.Write($"url={repoUrl}\n\n"), parseStdOutLine: null, - usePreCommandHook: false); + usePreCommandHook: false, + timeoutMs: timeoutMs); if (gitCredentialOutput.ExitCodeIsFailure) { EventMetadata errorData = new EventMetadata(); - tracer.RelatedWarning( - errorData, - "Git could not get credentials: " + gitCredentialOutput.Errors, - Keywords.Network | Keywords.Telemetry); - errorMessage = gitCredentialOutput.Errors; + + if (gitCredentialOutput.Errors.StartsWith("Operation timed out")) + { + errorMessage = "Credential manager did not respond within " + (timeoutMs / 1000) + " seconds"; + tracer.RelatedWarning( + errorData, + "Git credential fill timed out after " + timeoutMs + "ms", + Keywords.Network | Keywords.Telemetry); + } + else + { + errorMessage = gitCredentialOutput.Errors; + tracer.RelatedWarning( + errorData, + "Git could not get credentials: " + gitCredentialOutput.Errors, + Keywords.Network | Keywords.Telemetry); + } return false; } @@ -1113,7 +1137,8 @@ private Result InvokeGitAgainstDotGitFolder( Action writeStdIn, Action parseStdOutLine, bool usePreCommandHook = true, - string gitObjectsDirectory = null) + string gitObjectsDirectory = null, + int timeoutMs = -1) { // This git command should not need/use the working directory of the repo. // Run git.exe in Environment.SystemDirectory to ensure the git.exe process @@ -1125,7 +1150,7 @@ private Result InvokeGitAgainstDotGitFolder( useReadObjectHook: false, writeStdIn: writeStdIn, parseStdOutLine: parseStdOutLine, - timeoutMs: -1, + timeoutMs: timeoutMs, gitObjectsDirectory: gitObjectsDirectory, usePreCommandHook: usePreCommandHook); } diff --git a/GVFS/GVFS.Common/ReturnCode.cs b/GVFS/GVFS.Common/ReturnCode.cs index 09396a861..53b30c45a 100644 --- a/GVFS/GVFS.Common/ReturnCode.cs +++ b/GVFS/GVFS.Common/ReturnCode.cs @@ -12,5 +12,7 @@ public enum ReturnCode DehydrateFolderFailures = 7, MountAlreadyRunning = 8, AuthenticationError = 9, + CredentialTimeout = 10, + NetworkError = 11, } } diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index d59a5a09f..f32f411c8 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -129,6 +129,7 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords) // and config query into at most 2 HTTP requests (1 for anonymous repos), reusing // the same HttpClient/TCP connection. Stopwatch parallelTimer = Stopwatch.StartNew(); + bool hasCacheServer = this.cacheServer != null && !string.IsNullOrWhiteSpace(this.cacheServer.Url); var networkTask = Task.Run(() => { @@ -140,17 +141,26 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords) if (!this.enlistment.Authentication.TryInitializeAndQueryGVFSConfig( this.tracer, this.enlistment, this.retryConfig, out config, out authConfigError, out isAuthFailure, - updateProgress: message => this.mountProgressMessage = message)) + updateProgress: message => this.mountProgressMessage = message, + skipRetries: hasCacheServer)) { - if (this.cacheServer != null && !string.IsNullOrWhiteSpace(this.cacheServer.Url)) + if (hasCacheServer) { this.tracer.RelatedWarning("Mount will proceed with fallback cache server: " + authConfigError); config = null; } else { + ReturnCode exitCode = ReturnCode.NetworkError; + if (isAuthFailure) + { + exitCode = authConfigError != null && authConfigError.Contains("Credential manager did not respond") + ? ReturnCode.CredentialTimeout + : ReturnCode.AuthenticationError; + } + this.FailMountAndExit( - isAuthFailure ? ReturnCode.AuthenticationError : ReturnCode.GenericError, + exitCode, "Unable to query /gvfs/config" + Environment.NewLine + authConfigError); } }