From 589c6578bdd0ae5bcd92858047b643a2257dc8c0 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 12 Sep 2025 08:20:29 -0700 Subject: [PATCH 1/2] Warn-and-continue if gvfs/config download fails The verbs 'clone', 'mount', 'prefetch', and 'cache-server' each perform a query to gvfs/config endpoint for two purposes: to get the list of allowed client versions for the server, and to get the list of preconfigured cache servers. Currently, if the query fails then the verb fails. The most common reason for the query to fail appears to be transient network/authentication issues, making automount in particular more flaky than desired. Mount and Prefetch verbs should always have a cache server configuration available already, and Clone and Cache-Server both have command-line options to allow specifying the cache server configuration. This change modifies the verbs so that if a cache server url is already available (from local config or command-line option) then the verb will warn that the version could not be checked and continue instead of failing if the query to gvfs/config fails. --- GVFS/GVFS.Common/Http/CacheServerResolver.cs | 6 +- GVFS/GVFS/CommandLine/CacheServerVerb.cs | 59 ++++++++++----- GVFS/GVFS/CommandLine/CloneVerb.cs | 14 ++-- GVFS/GVFS/CommandLine/GVFSVerb.cs | 42 +++++++++++ GVFS/GVFS/CommandLine/MountVerb.cs | 40 +++++----- GVFS/GVFS/CommandLine/PrefetchVerb.cs | 78 +++++++++++--------- 6 files changed, 160 insertions(+), 79 deletions(-) diff --git a/GVFS/GVFS.Common/Http/CacheServerResolver.cs b/GVFS/GVFS.Common/Http/CacheServerResolver.cs index 9f3a7d3116..bc1df9727b 100644 --- a/GVFS/GVFS.Common/Http/CacheServerResolver.cs +++ b/GVFS/GVFS.Common/Http/CacheServerResolver.cs @@ -54,12 +54,12 @@ public bool TryResolveUrlFromRemote( if (cacheServerName.Equals(CacheServerInfo.ReservedNames.Default, StringComparison.OrdinalIgnoreCase)) { cacheServer = - serverGVFSConfig.CacheServers.FirstOrDefault(cache => cache.GlobalDefault) + serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.GlobalDefault) ?? this.CreateNone(); } else { - cacheServer = serverGVFSConfig.CacheServers.FirstOrDefault(cache => + cacheServer = serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.Name.Equals(cacheServerName, StringComparison.OrdinalIgnoreCase)); if (cacheServer == null) @@ -87,7 +87,7 @@ public CacheServerInfo ResolveNameFromRemote( } return - serverGVFSConfig.CacheServers.FirstOrDefault(cache => cache.Url.Equals(cacheServerUrl, StringComparison.OrdinalIgnoreCase)) + serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.Url.Equals(cacheServerUrl, StringComparison.OrdinalIgnoreCase)) ?? new CacheServerInfo(cacheServerUrl, CacheServerInfo.ReservedNames.UserDefined); } diff --git a/GVFS/GVFS/CommandLine/CacheServerVerb.cs b/GVFS/GVFS/CommandLine/CacheServerVerb.cs index 55edb88537..86754ae672 100644 --- a/GVFS/GVFS/CommandLine/CacheServerVerb.cs +++ b/GVFS/GVFS/CommandLine/CacheServerVerb.cs @@ -48,34 +48,25 @@ protected override void Execute(GVFSEnlistment enlistment) this.ReportErrorAndExit(tracer, "Authentication failed: " + authErrorMessage); } - ServerGVFSConfig serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); + ServerGVFSConfig serverGVFSConfig = null; string error = null; - if (this.CacheToSet != null) + // Handle the three operation types: list, set, and get (default) + if (this.ListCacheServers) { - CacheServerInfo cacheServer = cacheServerResolver.ParseUrlOrFriendlyName(this.CacheToSet); - cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); + // For listing, require config endpoint to succeed + serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); - if (!cacheServerResolver.TrySaveUrlToLocalConfig(cacheServer, out error)) - { - this.ReportErrorAndExit("Failed to save cache to config: " + error); - } - - this.Output.WriteLine("You must remount GVFS for this to take effect."); - } - else if (this.ListCacheServers) - { List cacheServers = serverGVFSConfig.CacheServers.ToList(); if (cacheServers != null && cacheServers.Any()) { this.Output.WriteLine(); this.Output.WriteLine("Available cache servers for: " + enlistment.RepoUrl); - foreach (CacheServerInfo cacheServer in cacheServers) + foreach (CacheServerInfo cacheServerInfo in cacheServers) { - this.Output.WriteLine(cacheServer); + this.Output.WriteLine(cacheServerInfo); } } else @@ -83,12 +74,42 @@ protected override void Execute(GVFSEnlistment enlistment) this.Output.WriteLine("There are no available cache servers for: " + enlistment.RepoUrl); } } + else if (this.CacheToSet != null) + { + // Setting a new cache server + CacheServerInfo cacheServer = cacheServerResolver.ParseUrlOrFriendlyName(this.CacheToSet); + + // For set operation, allow fallback if config endpoint fails but cache server URL is valid + serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServer); + + cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); + + if (!cacheServerResolver.TrySaveUrlToLocalConfig(cacheServer, out error)) + { + this.ReportErrorAndExit("Failed to save cache to config: " + error); + } + + this.Output.WriteLine("You must remount GVFS for this to take effect."); + } else { - string cacheServerUrl = CacheServerResolver.GetUrlFromConfig(enlistment); - CacheServerInfo cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerUrl, serverGVFSConfig); + // Default operation: get current cache server info + CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); + + // For get operation, allow fallback if config endpoint fails but cache server URL is valid + serverGVFSConfig =this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServer); + + CacheServerInfo resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverGVFSConfig); - this.Output.WriteLine("Using cache server: " + cacheServer); + this.Output.WriteLine("Using cache server: " + resolvedCacheServer); } } } diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index e810308377..f9985f7e08 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -162,12 +162,12 @@ public override void Execute() string resolvedLocalCacheRoot; if (string.IsNullOrWhiteSpace(this.LocalCacheRoot)) { - string localCacheRootError; - if (!LocalCacheResolver.TryGetDefaultLocalCacheRoot(enlistment, out resolvedLocalCacheRoot, out localCacheRootError)) + string localCacheRootError; + if (!LocalCacheResolver.TryGetDefaultLocalCacheRoot(enlistment, out resolvedLocalCacheRoot, out localCacheRootError)) { this.ReportErrorAndExit( tracer, - $"Failed to determine the default location for the local GVFS cache: `{localCacheRootError}`"); + $"Failed to determine the default location for the local GVFS cache: `{localCacheRootError}`"); } } else @@ -189,7 +189,12 @@ public override void Execute() } RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); - serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); + + serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServer); cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); @@ -237,7 +242,6 @@ public override void Execute() exitCode = (int)result; } } - else { Process.Start(new ProcessStartInfo( diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index a9fcb95876..70248bf66d 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -493,6 +493,48 @@ protected RetryConfig GetRetryConfig(ITracer tracer, GVFSEnlistment enlistment, return retryConfig; } + /// + /// Attempts to query the GVFS config endpoint. If it fails, but a valid cache server URL is available, returns a null object. + /// If neither is available, . If config endpoint fails, skips minimum version check and warns (unless skip-version-check is set). + /// + protected ServerGVFSConfig QueryGVFSConfigWithFallbackCacheServer( + ITracer tracer, + GVFSEnlistment enlistment, + RetryConfig retryConfig, + CacheServerInfo fallbackCacheServer) + { + ServerGVFSConfig serverGVFSConfig = null; + string errorMessage = null; + bool configSuccess = this.ShowStatusWhileRunning( + () => + { + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) + { + const bool LogErrors = true; + return configRequestor.TryQueryGVFSConfig(LogErrors, out serverGVFSConfig, out _, out errorMessage); + } + }, + "Querying remote for config", + suppressGvfsLogMessage: true); + + if (!configSuccess) + { + // If a valid cache server URL is available, warn and continue + if (fallbackCacheServer != null && !string.IsNullOrWhiteSpace(fallbackCacheServer.Url)) + { + // Continue without config + // Warning will be logged/displayed when version check is run + return null; + } + else + { + this.ReportErrorAndExit(tracer, "Unable to query /gvfs/config" + Environment.NewLine + errorMessage); + } + } + return serverGVFSConfig; + } + + // Restore original QueryGVFSConfig for other callers protected ServerGVFSConfig QueryGVFSConfig(ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig) { ServerGVFSConfig serverGVFSConfig = null; diff --git a/GVFS/GVFS/CommandLine/MountVerb.cs b/GVFS/GVFS/CommandLine/MountVerb.cs index 90db430cf2..5183ec4342 100644 --- a/GVFS/GVFS/CommandLine/MountVerb.cs +++ b/GVFS/GVFS/CommandLine/MountVerb.cs @@ -95,7 +95,8 @@ protected override void Execute(GVFSEnlistment enlistment) this.ReportErrorAndExit("Error installing hooks: " + errorMessage); } - CacheServerInfo cacheServer = this.ResolvedCacheServer ?? CacheServerResolver.GetCacheServerFromConfig(enlistment); + var resolvedCacheServer = this.ResolvedCacheServer; + var cacheServerFromConfig = resolvedCacheServer ?? CacheServerResolver.GetCacheServerFromConfig(enlistment); tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.MountVerb), @@ -104,7 +105,7 @@ protected override void Execute(GVFSEnlistment enlistment) tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, - cacheServer.Url, + cacheServerFromConfig.Url, new EventMetadata { { "Unattended", this.Unattended }, @@ -122,7 +123,7 @@ protected override void Execute(GVFSEnlistment enlistment) { { "KernelDriver.IsReady_Error", errorMessage }, { TracingConstants.MessageKey.InfoMessage, "Service will retry" } - }); + }); if (!this.ShowStatusWhileRunning( () => { return this.TryEnableAndAttachPrjFltThroughService(enlistment.EnlistmentRoot, out errorMessage); }, @@ -134,7 +135,8 @@ protected override void Execute(GVFSEnlistment enlistment) RetryConfig retryConfig = null; ServerGVFSConfig serverGVFSConfig = this.DownloadedGVFSConfig; - if (!this.SkipVersionCheck) + /* If resolved cache server was passed in, we've already checked server config and version check in previous operation. */ + if (resolvedCacheServer == null) { string authErrorMessage; if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) @@ -150,17 +152,21 @@ protected override void Execute(GVFSEnlistment enlistment) retryConfig = this.GetRetryConfig(tracer, enlistment); } - serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); + serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServerFromConfig); } this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: true); CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverGVFSConfig); - this.Output.WriteLine("Configured cache server: " + cacheServer); + resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerFromConfig.Url, serverGVFSConfig); + this.Output.WriteLine("Configured cache server: " + cacheServerFromConfig); } - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, cacheServer); + this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, resolvedCacheServer); if (!this.ShowStatusWhileRunning( () => { return this.PerformPreMountValidation(tracer, enlistment, out mountExecutableLocation, out errorMessage); }, @@ -193,23 +199,23 @@ protected override void Execute(GVFSEnlistment enlistment) "Mounting")) { this.ReportErrorAndExit(tracer, errorMessage); - } - - if (!this.Unattended) + } + + if (!this.Unattended) { tracer.RelatedInfo($"{nameof(this.Execute)}: Registering for automount"); - - if (this.ShowStatusWhileRunning( - () => { return this.RegisterMount(enlistment, out errorMessage); }, - "Registering for automount")) + + if (this.ShowStatusWhileRunning( + () => { return this.RegisterMount(enlistment, out errorMessage); }, + "Registering for automount")) { - tracer.RelatedInfo($"{nameof(this.Execute)}: Registered for automount"); + tracer.RelatedInfo($"{nameof(this.Execute)}: Registered for automount"); } else { this.Output.WriteLine(" WARNING: " + errorMessage); tracer.RelatedInfo($"{nameof(this.Execute)}: Failed to register for automount"); - } + } } } } diff --git a/GVFS/GVFS/CommandLine/PrefetchVerb.cs b/GVFS/GVFS/CommandLine/PrefetchVerb.cs index 5d6373f8e6..ab72b5e9f1 100644 --- a/GVFS/GVFS/CommandLine/PrefetchVerb.cs +++ b/GVFS/GVFS/CommandLine/PrefetchVerb.cs @@ -53,12 +53,12 @@ public class PrefetchVerb : GVFSVerb.ForExistingEnlistment Required = false, Default = false, HelpText = "Specify this flag to load file list from stdin. Same format as when loading from file.")] - public bool FilesFromStdIn { get; set; } - - [Option( - "stdin-folders-list", - Required = false, - Default = false, + public bool FilesFromStdIn { get; set; } + + [Option( + "stdin-folders-list", + Required = false, + Default = false, HelpText = "Specify this flag to load folder list from stdin. Same format as when loading from file.")] public bool FoldersFromStdIn { get; set; } @@ -109,7 +109,7 @@ protected override void Execute(GVFSEnlistment enlistment) tracer.AddDiagnosticConsoleEventListener(EventLevel.Informational, Keywords.Any); } - string cacheServerUrl = CacheServerResolver.GetUrlFromConfig(enlistment); + var cacheServerFromConfig = CacheServerResolver.GetCacheServerFromConfig(enlistment); tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.Prefetch), @@ -118,7 +118,7 @@ protected override void Execute(GVFSEnlistment enlistment) tracer.WriteStartEvent( enlistment.EnlistmentRoot, enlistment.RepoUrl, - cacheServerUrl); + cacheServerFromConfig.Url); try { @@ -127,8 +127,8 @@ protected override void Execute(GVFSEnlistment enlistment) metadata.Add("Files", this.Files); metadata.Add("Folders", this.Folders); metadata.Add("FileListFile", this.FilesListFile); - metadata.Add("FoldersListFile", this.FoldersListFile); - metadata.Add("FilesFromStdIn", this.FilesFromStdIn); + metadata.Add("FoldersListFile", this.FoldersListFile); + metadata.Add("FilesFromStdIn", this.FilesFromStdIn); metadata.Add("FoldersFromStdIn", this.FoldersFromStdIn); metadata.Add("HydrateFiles", this.HydrateFiles); tracer.RelatedEvent(EventLevel.Informational, "PerformPrefetch", metadata); @@ -151,14 +151,14 @@ protected override void Execute(GVFSEnlistment enlistment) } GitObjectsHttpRequestor objectRequestor; - CacheServerInfo cacheServer; + CacheServerInfo resolvedCacheServer; this.InitializeServerConnection( tracer, enlistment, - cacheServerUrl, + cacheServerFromConfig, out objectRequestor, - out cacheServer); - this.PrefetchCommits(tracer, enlistment, objectRequestor, cacheServer); + out resolvedCacheServer); + this.PrefetchCommits(tracer, enlistment, objectRequestor, resolvedCacheServer); } else { @@ -167,8 +167,8 @@ protected override void Execute(GVFSEnlistment enlistment) List foldersList; FileBasedDictionary lastPrefetchArgs; - this.LoadBlobPrefetchArgs(tracer, enlistment, out headCommitId, out filesList, out foldersList, out lastPrefetchArgs); - + this.LoadBlobPrefetchArgs(tracer, enlistment, out headCommitId, out filesList, out foldersList, out lastPrefetchArgs); + if (BlobPrefetcher.IsNoopPrefetch(tracer, lastPrefetchArgs, headCommitId, filesList, foldersList, this.HydrateFiles)) { Console.WriteLine("All requested files are already available. Nothing new to prefetch."); @@ -176,14 +176,14 @@ protected override void Execute(GVFSEnlistment enlistment) else { GitObjectsHttpRequestor objectRequestor; - CacheServerInfo cacheServer; + CacheServerInfo resolvedCacheServer; this.InitializeServerConnection( tracer, enlistment, - cacheServerUrl, + cacheServerFromConfig, out objectRequestor, - out cacheServer); - this.PrefetchBlobs(tracer, enlistment, headCommitId, filesList, foldersList, lastPrefetchArgs, objectRequestor, cacheServer); + out resolvedCacheServer); + this.PrefetchBlobs(tracer, enlistment, headCommitId, filesList, foldersList, lastPrefetchArgs, objectRequestor, resolvedCacheServer); } } } @@ -230,15 +230,18 @@ protected override void Execute(GVFSEnlistment enlistment) private void InitializeServerConnection( ITracer tracer, GVFSEnlistment enlistment, - string cacheServerUrl, + CacheServerInfo cacheServerFromConfig, out GitObjectsHttpRequestor objectRequestor, - out CacheServerInfo cacheServer) + out CacheServerInfo resolvedCacheServer) { RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); - cacheServer = this.ResolvedCacheServer; + // These this.* arguments are set if this is a follow-on operation from clone or mount. + resolvedCacheServer = this.ResolvedCacheServer; ServerGVFSConfig serverGVFSConfig = this.ServerGVFSConfig; - if (!this.SkipVersionCheck) + + // If ResolvedCacheServer is set, then we have already tried querying the server config and checking versions. + if (resolvedCacheServer == null) { string authErrorMessage; if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) @@ -246,24 +249,29 @@ private void InitializeServerConnection( this.ReportErrorAndExit(tracer, "Unable to prefetch because authentication failed: " + authErrorMessage); } + CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); + if (serverGVFSConfig == null) { - serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); + serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + tracer, + enlistment, + retryConfig, + cacheServerFromConfig); } - if (cacheServer == null) + resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerFromConfig.Url, serverGVFSConfig); + + if (!this.SkipVersionCheck) { - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerUrl, serverGVFSConfig); + this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: false); } - this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: false); - - this.Output.WriteLine("Configured cache server: " + cacheServer); + this.Output.WriteLine("Configured cache server: " + resolvedCacheServer); } - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, cacheServer); - objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, cacheServer, retryConfig); + this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, resolvedCacheServer); + objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, resolvedCacheServer, retryConfig); } private void PrefetchCommits(ITracer tracer, GVFSEnlistment enlistment, GitObjectsHttpRequestor objectRequestor, CacheServerInfo cacheServer) @@ -300,8 +308,8 @@ private void LoadBlobPrefetchArgs( out List foldersList, out FileBasedDictionary lastPrefetchArgs) { - string error; - + string error; + if (!FileBasedDictionary.TryCreate( tracer, Path.Combine(enlistment.DotGVFSRoot, "LastBlobPrefetch.dat"), From d69080c97c67ccb8adef2bad8ba7f7182036bbeb Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Fri, 3 Oct 2025 09:16:40 -0700 Subject: [PATCH 2/2] Fix summary of QueryGVFSConfigWithFallbackCacheServer --- GVFS/GVFS/CommandLine/GVFSVerb.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index 70248bf66d..f0c7d984da 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -494,8 +494,10 @@ protected RetryConfig GetRetryConfig(ITracer tracer, GVFSEnlistment enlistment, } /// - /// Attempts to query the GVFS config endpoint. If it fails, but a valid cache server URL is available, returns a null object. - /// If neither is available, . If config endpoint fails, skips minimum version check and warns (unless skip-version-check is set). + /// Attempts to query the GVFS config endpoint. If successful, returns the config. + /// If the query fails but a valid fallback cache server URL is available, returns null and continues. + /// (A warning will be logged later.) + /// If the query fails and no valid fallback is available, reports an error and exits. /// protected ServerGVFSConfig QueryGVFSConfigWithFallbackCacheServer( ITracer tracer,