From 9d3a09b3f07168603c4ecb61df2fc6dac586d3f1 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Tue, 3 Mar 2026 10:56:17 -0800 Subject: [PATCH 1/4] Move pre-mount validation from gvfs.exe to gvfs.mount.exe Move authentication, server config query, version validation, cache health checks, git config settings, and enlistment logging from the mount verb (gvfs.exe) into the mount process (gvfs.mount.exe). This eliminates duplicate work (auth and index parsing were done in both processes) and reduces mount time from ~40s to ~22s. The verb now only does: disk layout upgrade check, ProjFS attach, enum arg validation, mount exe existence check, launch + wait. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- GVFS/GVFS.Mount/InProcessMount.cs | 508 ++++++++++++++++++++++++++++- GVFS/GVFS/CommandLine/MountVerb.cs | 140 +------- 2 files changed, 512 insertions(+), 136 deletions(-) diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index e6d43a842..b34aa804f 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -15,6 +15,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Security; using System.Text; using System.Threading; using static GVFS.Common.Git.LibGit2Repo; @@ -121,6 +122,40 @@ public void Mount(EventLevel verbosity, Keywords keywords) this.enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot); + if (!this.enlistment.Authentication.TryInitialize(this.tracer, this.enlistment, out error)) + { + this.tracer.RelatedWarning("Mount will proceed, but new files cannot be accessed until GVFS can authenticate: " + error); + } + + this.ValidateGitVersion(); + this.ValidateHooksVersion(); + this.ValidateFileSystemSupportsRequiredFeatures(); + + ServerGVFSConfig serverGVFSConfig = this.QueryAndValidateGVFSConfig(); + + CacheServerResolver cacheServerResolver = new CacheServerResolver(this.tracer, this.enlistment); + this.cacheServer = cacheServerResolver.ResolveNameFromRemote(this.cacheServer.Url, serverGVFSConfig); + + this.EnsureLocalCacheIsHealthy(serverGVFSConfig); + + GitProcess git = new GitProcess(this.enlistment); + if (!git.IsValidRepo()) + { + this.FailMountAndExit("The .git folder is missing or has invalid contents"); + } + + if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.EnlistmentRoot, out error)) + { + this.FailMountAndExit("FileSystem unsupported: " + error); + } + + if (!this.TrySetRequiredGitConfigSettings()) + { + this.FailMountAndExit("Unable to configure git repo"); + } + + this.LogEnlistmentInfoAndSetConfigValues(); + using (NamedPipeServer pipeServer = this.StartNamedPipe()) { this.tracer.RelatedEvent( @@ -772,13 +807,6 @@ private void HandleUnmountRequest(NamedPipeServer.Connection connection) private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache, bool alreadyInitialized = false) { string error; - if (!alreadyInitialized) - { - if (!this.context.Enlistment.Authentication.TryInitialize(this.context.Tracer, this.context.Enlistment, out error)) - { - this.FailMountAndExit("Failed to obtain git credentials: " + error); - } - } GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(this.context.Tracer, this.context.Enlistment, cache, this.retryConfig); this.gitObjects = new GVFSGitObjects(this.context, objectRequestor); @@ -846,6 +874,472 @@ private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache, bool this.heartbeat.Start(); } + private void ValidateGitVersion() + { + GitVersion gitVersion = null; + if (string.IsNullOrEmpty(this.enlistment.GitBinPath) || !GitProcess.TryGetVersion(this.enlistment.GitBinPath, out gitVersion, out string _)) + { + this.FailMountAndExit("Error: Unable to retrieve the Git version"); + } + + this.enlistment.SetGitVersion(gitVersion.ToString()); + + if (gitVersion.Platform != GVFSConstants.SupportedGitVersion.Platform) + { + this.FailMountAndExit("Error: Invalid version of Git {0}. Must use vfs version.", gitVersion); + } + + if (gitVersion.IsLessThan(GVFSConstants.SupportedGitVersion)) + { + this.FailMountAndExit( + "Error: Installed Git version {0} is less than the minimum supported version of {1}.", + gitVersion, + GVFSConstants.SupportedGitVersion); + } + else if (gitVersion.Revision != GVFSConstants.SupportedGitVersion.Revision) + { + this.FailMountAndExit( + "Error: Installed Git version {0} has revision number {1} instead of {2}." + + " This Git version is too new, so either downgrade Git or upgrade VFS for Git." + + " The minimum supported version of Git is {3}.", + gitVersion, + gitVersion.Revision, + GVFSConstants.SupportedGitVersion.Revision, + GVFSConstants.SupportedGitVersion); + } + } + + private void ValidateHooksVersion() + { + string hooksVersion; + string error; + if (!GVFSPlatform.Instance.TryGetGVFSHooksVersion(out hooksVersion, out error)) + { + this.FailMountAndExit(error); + } + + string gvfsVersion = ProcessHelper.GetCurrentProcessVersion(); + if (hooksVersion != gvfsVersion) + { + this.FailMountAndExit("GVFS.Hooks version ({0}) does not match GVFS version ({1}).", hooksVersion, gvfsVersion); + } + + this.enlistment.SetGVFSHooksVersion(hooksVersion); + } + + private void ValidateFileSystemSupportsRequiredFeatures() + { + try + { + string warning; + string error; + if (!GVFSPlatform.Instance.KernelDriver.IsSupported(this.enlistment.EnlistmentRoot, out warning, out error)) + { + this.FailMountAndExit("Error: {0}", error); + } + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", e.ToString()); + this.tracer.RelatedError(metadata, "Failed to determine if file system supports features required by GVFS"); + this.FailMountAndExit("Error: Failed to determine if file system supports features required by GVFS."); + } + } + + private ServerGVFSConfig QueryAndValidateGVFSConfig() + { + ServerGVFSConfig serverGVFSConfig = null; + string errorMessage = null; + + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(this.tracer, this.enlistment, this.retryConfig)) + { + const bool LogErrors = true; + if (!configRequestor.TryQueryGVFSConfig(LogErrors, out serverGVFSConfig, out _, out errorMessage)) + { + // If we have a valid cache server, continue without config (matches verb fallback behavior) + if (this.cacheServer != null && !string.IsNullOrWhiteSpace(this.cacheServer.Url)) + { + this.tracer.RelatedWarning("Unable to query /gvfs/config: " + errorMessage); + serverGVFSConfig = null; + } + else + { + this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + errorMessage); + } + } + } + + this.ValidateGVFSVersion(serverGVFSConfig); + + return serverGVFSConfig; + } + + private void ValidateGVFSVersion(ServerGVFSConfig config) + { + using (ITracer activity = this.tracer.StartActivity("ValidateGVFSVersion", EventLevel.Informational)) + { + if (ProcessHelper.IsDevelopmentVersion()) + { + return; + } + + string recordedVersion = ProcessHelper.GetCurrentProcessVersion(); + int plus = recordedVersion.IndexOf('+'); + Version currentVersion = new Version(plus < 0 ? recordedVersion : recordedVersion.Substring(0, plus)); + IEnumerable allowedGvfsClientVersions = + config != null + ? config.AllowedGVFSClientVersions + : null; + + if (allowedGvfsClientVersions == null || !allowedGvfsClientVersions.Any()) + { + string warningMessage = "WARNING: Unable to validate your GVFS version" + Environment.NewLine; + if (config == null) + { + warningMessage += "Could not query valid GVFS versions from: " + Uri.EscapeUriString(this.enlistment.RepoUrl); + } + else + { + warningMessage += "Server not configured to provide supported GVFS versions"; + } + + this.tracer.RelatedWarning(warningMessage); + return; + } + + foreach (ServerGVFSConfig.VersionRange versionRange in config.AllowedGVFSClientVersions) + { + if (currentVersion >= versionRange.Min && + (versionRange.Max == null || currentVersion <= versionRange.Max)) + { + activity.RelatedEvent( + EventLevel.Informational, + "GVFSVersionValidated", + new EventMetadata + { + { "SupportedVersionRange", versionRange }, + }); + + this.enlistment.SetGVFSVersion(currentVersion.ToString()); + return; + } + } + + activity.RelatedError("GVFS version {0} is not supported", currentVersion); + this.FailMountAndExit("ERROR: Your GVFS version is no longer supported. Install the latest and try again."); + } + } + + private void EnsureLocalCacheIsHealthy(ServerGVFSConfig serverGVFSConfig) + { + if (!Directory.Exists(this.enlistment.LocalCacheRoot)) + { + try + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Local cache root: {this.enlistment.LocalCacheRoot} missing, recreating it"); + Directory.CreateDirectory(this.enlistment.LocalCacheRoot); + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", e.ToString()); + metadata.Add("enlistment.LocalCacheRoot", this.enlistment.LocalCacheRoot); + this.tracer.RelatedError(metadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create local cache root"); + this.FailMountAndExit("Failed to create local cache: " + this.enlistment.LocalCacheRoot); + } + } + + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + if (Directory.Exists(this.enlistment.GitObjectsRoot)) + { + bool gitObjectsRootInAlternates = false; + string alternatesFilePath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Objects.Info.Alternates); + if (File.Exists(alternatesFilePath)) + { + try + { + using (Stream stream = fileSystem.OpenFileStream( + alternatesFilePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + callFlushFileBuffers: false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + while (!reader.EndOfStream) + { + string alternatesLine = reader.ReadLine(); + if (string.Equals(alternatesLine, this.enlistment.GitObjectsRoot, GVFSPlatform.Instance.Constants.PathComparison)) + { + gitObjectsRootInAlternates = true; + } + } + } + } + } + catch (Exception e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Exception", e.ToString()); + this.tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to validate alternates file"); + this.FailMountAndExit($"Failed to validate that alternates file includes git objects root: {e.Message}"); + } + } + else + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Alternates file not found"); + } + + if (!gitObjectsRootInAlternates) + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({this.enlistment.GitObjectsRoot}) missing from alternates files, recreating alternates"); + string error; + if (!this.TryCreateAlternatesFile(fileSystem, out error)) + { + this.FailMountAndExit($"Failed to update alternates file to include git objects root: {error}"); + } + } + } + else + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({this.enlistment.GitObjectsRoot}) missing, determining new root"); + + if (serverGVFSConfig == null) + { + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(this.tracer, this.enlistment, this.retryConfig)) + { + string configError; + if (!configRequestor.TryQueryGVFSConfig(true, out serverGVFSConfig, out _, out configError)) + { + this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + configError); + } + } + } + + string localCacheKey; + string error; + LocalCacheResolver localCacheResolver = new LocalCacheResolver(this.enlistment); + if (!localCacheResolver.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( + this.tracer, + serverGVFSConfig, + this.cacheServer, + this.enlistment.LocalCacheRoot, + localCacheKey: out localCacheKey, + errorMessage: out error)) + { + this.FailMountAndExit($"Previous git objects root ({this.enlistment.GitObjectsRoot}) not found, and failed to determine new local cache key: {error}"); + } + + EventMetadata keyMetadata = new EventMetadata(); + keyMetadata.Add("localCacheRoot", this.enlistment.LocalCacheRoot); + keyMetadata.Add("localCacheKey", localCacheKey); + keyMetadata.Add(TracingConstants.MessageKey.InfoMessage, "Initializing and persisting updated paths"); + this.tracer.RelatedEvent(EventLevel.Informational, "EnsureLocalCacheIsHealthy_InitializePathsFromKey", keyMetadata); + this.enlistment.InitializeCachePathsFromKey(this.enlistment.LocalCacheRoot, localCacheKey); + + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating GitObjectsRoot ({this.enlistment.GitObjectsRoot}), GitPackRoot ({this.enlistment.GitPackRoot}), and BlobSizesRoot ({this.enlistment.BlobSizesRoot})"); + try + { + Directory.CreateDirectory(this.enlistment.GitObjectsRoot); + Directory.CreateDirectory(this.enlistment.GitPackRoot); + } + catch (Exception e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Exception", e.ToString()); + exceptionMetadata.Add("enlistment.GitObjectsRoot", this.enlistment.GitObjectsRoot); + exceptionMetadata.Add("enlistment.GitPackRoot", this.enlistment.GitPackRoot); + this.tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create objects and pack folders"); + this.FailMountAndExit("Failed to create objects and pack folders"); + } + + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating new alternates file"); + if (!this.TryCreateAlternatesFile(fileSystem, out error)) + { + this.FailMountAndExit($"Failed to update alternates file with new objects path: {error}"); + } + + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving git objects root ({this.enlistment.GitObjectsRoot}) in repo metadata"); + RepoMetadata.Instance.SetGitObjectsRoot(this.enlistment.GitObjectsRoot); + + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving blob sizes root ({this.enlistment.BlobSizesRoot}) in repo metadata"); + RepoMetadata.Instance.SetBlobSizesRoot(this.enlistment.BlobSizesRoot); + } + + if (!Directory.Exists(this.enlistment.BlobSizesRoot)) + { + this.tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: BlobSizesRoot ({this.enlistment.BlobSizesRoot}) not found, re-creating"); + try + { + Directory.CreateDirectory(this.enlistment.BlobSizesRoot); + } + catch (Exception e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Exception", e.ToString()); + exceptionMetadata.Add("enlistment.BlobSizesRoot", this.enlistment.BlobSizesRoot); + this.tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create blob sizes folder"); + this.FailMountAndExit("Failed to create blob sizes folder"); + } + } + } + + private bool TryCreateAlternatesFile(PhysicalFileSystem fileSystem, out string errorMessage) + { + try + { + string alternatesFilePath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Objects.Info.Alternates); + string tempFilePath = alternatesFilePath + ".tmp"; + fileSystem.WriteAllText(tempFilePath, this.enlistment.GitObjectsRoot); + fileSystem.MoveAndOverwriteFile(tempFilePath, alternatesFilePath); + } + catch (SecurityException e) { errorMessage = e.Message; return false; } + catch (IOException e) { errorMessage = e.Message; return false; } + + errorMessage = null; + return true; + } + + [Flags] + private enum GitCoreGVFSFlags + { + SkipShaOnIndex = 1 << 0, + BlockCommands = 1 << 1, + MissingOk = 1 << 2, + NoDeleteOutsideSparseCheckout = 1 << 3, + FetchSkipReachabilityAndUploadPack = 1 << 4, + BlockFiltersAndEolConversions = 1 << 6, + } + + private bool TrySetRequiredGitConfigSettings() + { + string expectedHooksPath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Hooks.Root); + expectedHooksPath = Paths.ConvertPathToGitFormat(expectedHooksPath); + + string gitStatusCachePath = null; + if (!GVFSEnlistment.IsUnattended(tracer: null) && GVFSPlatform.Instance.IsGitStatusCacheSupported()) + { + gitStatusCachePath = Path.Combine( + this.enlistment.EnlistmentRoot, + GVFSPlatform.Instance.Constants.DotGVFSRoot, + GVFSConstants.DotGVFS.GitStatusCache.CachePath); + + gitStatusCachePath = Paths.ConvertPathToGitFormat(gitStatusCachePath); + } + + string coreGVFSFlags = Convert.ToInt32( + GitCoreGVFSFlags.SkipShaOnIndex | + GitCoreGVFSFlags.BlockCommands | + GitCoreGVFSFlags.MissingOk | + GitCoreGVFSFlags.NoDeleteOutsideSparseCheckout | + GitCoreGVFSFlags.FetchSkipReachabilityAndUploadPack | + GitCoreGVFSFlags.BlockFiltersAndEolConversions) + .ToString(); + + Dictionary requiredSettings = new Dictionary + { + { "am.keepcr", "true" }, + { "checkout.optimizenewbranch", "true" }, + { "core.autocrlf", "false" }, + { "core.commitGraph", "true" }, + { "core.fscache", "true" }, + { "core.gvfs", coreGVFSFlags }, + { "core.multiPackIndex", "true" }, + { "core.preloadIndex", "true" }, + { "core.safecrlf", "false" }, + { "core.untrackedCache", "false" }, + { "core.repositoryformatversion", "0" }, + { "core.filemode", GVFSPlatform.Instance.FileSystem.SupportsFileMode ? "true" : "false" }, + { "core.bare", "false" }, + { "core.logallrefupdates", "true" }, + { GitConfigSetting.CoreVirtualizeObjectsName, "true" }, + { GitConfigSetting.CoreVirtualFileSystemName, Paths.ConvertPathToGitFormat(GVFSConstants.DotGit.Hooks.VirtualFileSystemPath) }, + { "core.hookspath", expectedHooksPath }, + { GitConfigSetting.CredentialUseHttpPath, "true" }, + { "credential.validate", "false" }, + { "diff.autoRefreshIndex", "true" }, + { "feature.manyFiles", "false" }, + { "feature.experimental", "false" }, + { "fetch.writeCommitGraph", "false" }, + { "gc.auto", "0" }, + { "gui.gcwarning", "false" }, + { "index.threads", "true" }, + { "index.version", "4" }, + { "merge.stat", "false" }, + { "merge.renames", "false" }, + { "pack.useBitmaps", "false" }, + { "pack.useSparse", "true" }, + { "receive.autogc", "false" }, + { "reset.quiet", "true" }, + { "status.deserializePath", gitStatusCachePath }, + { "status.submoduleSummary", "false" }, + { "commitGraph.generationVersion", "1" }, + { "core.useBuiltinFSMonitor", "false" }, + }; + + GitProcess git = new GitProcess(this.enlistment); + + Dictionary existingConfigSettings; + if (!git.TryGetAllConfig(localOnly: true, configSettings: out existingConfigSettings)) + { + return false; + } + + foreach (KeyValuePair setting in requiredSettings) + { + GitConfigSetting existingSetting; + if (setting.Value != null) + { + if (!existingConfigSettings.TryGetValue(setting.Key, out existingSetting) || + !existingSetting.HasValue(setting.Value)) + { + GitProcess.Result setConfigResult = git.SetInLocalConfig(setting.Key, setting.Value); + if (setConfigResult.ExitCodeIsFailure) + { + return false; + } + } + } + else + { + if (existingConfigSettings.TryGetValue(setting.Key, out existingSetting)) + { + git.DeleteFromLocalConfig(setting.Key); + } + } + } + + return true; + } + + private void LogEnlistmentInfoAndSetConfigValues() + { + string mountId = Guid.NewGuid().ToString("N"); + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId); + metadata.Add(nameof(mountId), mountId); + metadata.Add("Enlistment", this.enlistment); + metadata.Add("PhysicalDiskInfo", GVFSPlatform.Instance.GetPhysicalDiskInfo(this.enlistment.WorkingDirectoryRoot, sizeStatsOnly: false)); + this.tracer.RelatedEvent(EventLevel.Informational, "EnlistmentInfo", metadata, Keywords.Telemetry); + + GitProcess git = new GitProcess(this.enlistment); + GitProcess.Result configResult = git.SetInLocalConfig(GVFSConstants.GitConfig.EnlistmentId, RepoMetadata.Instance.EnlistmentId, replaceAll: true); + if (configResult.ExitCodeIsFailure) + { + string error = "Could not update config with enlistment id, error: " + configResult.Errors; + this.tracer.RelatedWarning(error); + } + + configResult = git.SetInLocalConfig(GVFSConstants.GitConfig.MountId, mountId, replaceAll: true); + if (configResult.ExitCodeIsFailure) + { + string error = "Could not update config with mount id, error: " + configResult.Errors; + this.tracer.RelatedWarning(error); + } + } + private void UnmountAndStopWorkingDirectoryCallbacks(bool willRemountInSameProcess = false) { if (this.maintenanceScheduler != null) diff --git a/GVFS/GVFS/CommandLine/MountVerb.cs b/GVFS/GVFS/CommandLine/MountVerb.cs index 5183ec434..312a9f1c6 100644 --- a/GVFS/GVFS/CommandLine/MountVerb.cs +++ b/GVFS/GVFS/CommandLine/MountVerb.cs @@ -1,15 +1,11 @@ using CommandLine; using GVFS.Common; -using GVFS.Common.FileSystem; -using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.NamedPipes; using GVFS.Common.Tracing; using GVFS.DiskLayoutUpgrades; -using GVFS.Virtualization.Projection; using System; using System.IO; -using System.Security.Principal; namespace GVFS.CommandLine { @@ -86,17 +82,11 @@ protected override void Execute(GVFSEnlistment enlistment) string mountExecutableLocation = null; using (JsonTracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "ExecuteMount")) { - PhysicalFileSystem fileSystem = new PhysicalFileSystem(); - GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem); - GVFSContext context = new GVFSContext(tracer, fileSystem, gitRepo, enlistment); + // Validate these before handing them to the background process + // which cannot tell the user when they are bad + this.ValidateEnumArgs(); - if (!this.SkipInstallHooks && !HooksInstaller.InstallHooks(context, out errorMessage)) - { - this.ReportErrorAndExit("Error installing hooks: " + errorMessage); - } - - var resolvedCacheServer = this.ResolvedCacheServer; - var cacheServerFromConfig = resolvedCacheServer ?? CacheServerResolver.GetCacheServerFromConfig(enlistment); + CacheServerInfo cacheServerFromConfig = CacheServerResolver.GetCacheServerFromConfig(enlistment); tracer.AddLogFileEventListener( GVFSEnlistment.GetNewGVFSLogFileName(enlistment.GVFSLogsRoot, GVFSConstants.LogFileTypes.MountVerb), @@ -133,65 +123,11 @@ protected override void Execute(GVFSEnlistment enlistment) } } - RetryConfig retryConfig = null; - ServerGVFSConfig serverGVFSConfig = this.DownloadedGVFSConfig; - /* If resolved cache server was passed in, we've already checked server config and version check in previous operation. */ - if (resolvedCacheServer == null) + // Verify mount executable exists before launching + mountExecutableLocation = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), GVFSPlatform.Instance.Constants.MountExecutableName); + if (!File.Exists(mountExecutableLocation)) { - string authErrorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) - { - this.Output.WriteLine(" WARNING: " + authErrorMessage); - this.Output.WriteLine(" Mount will proceed, but new files cannot be accessed until GVFS can authenticate."); - } - - if (serverGVFSConfig == null) - { - if (retryConfig == null) - { - retryConfig = this.GetRetryConfig(tracer, enlistment); - } - - serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( - tracer, - enlistment, - retryConfig, - cacheServerFromConfig); - } - - this.ValidateClientVersions(tracer, enlistment, serverGVFSConfig, showWarnings: true); - - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerFromConfig.Url, serverGVFSConfig); - this.Output.WriteLine("Configured cache server: " + cacheServerFromConfig); - } - - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverGVFSConfig, resolvedCacheServer); - - if (!this.ShowStatusWhileRunning( - () => { return this.PerformPreMountValidation(tracer, enlistment, out mountExecutableLocation, out errorMessage); }, - "Validating repo")) - { - this.ReportErrorAndExit(tracer, errorMessage); - } - - if (!this.SkipVersionCheck) - { - string error; - if (!RepoMetadata.TryInitialize(tracer, enlistment.DotGVFSRoot, out error)) - { - this.ReportErrorAndExit(tracer, error); - } - - try - { - GitProcess git = new GitProcess(enlistment); - this.LogEnlistmentInfoAndSetConfigValues(tracer, git, enlistment); - } - finally - { - RepoMetadata.Shutdown(); - } + this.ReportErrorAndExit(tracer, $"Could not find {GVFSPlatform.Instance.Constants.MountExecutableName}. You may need to reinstall GVFS."); } if (!this.ShowStatusWhileRunning( @@ -220,62 +156,8 @@ protected override void Execute(GVFSEnlistment enlistment) } } - private bool PerformPreMountValidation(ITracer tracer, GVFSEnlistment enlistment, out string mountExecutableLocation, out string errorMessage) - { - errorMessage = string.Empty; - mountExecutableLocation = string.Empty; - - // We have to parse these parameters here to make sure they are valid before - // handing them to the background process which cannot tell the user when they are bad - EventLevel verbosity; - Keywords keywords; - this.ParseEnumArgs(out verbosity, out keywords); - - mountExecutableLocation = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), GVFSPlatform.Instance.Constants.MountExecutableName); - if (!File.Exists(mountExecutableLocation)) - { - errorMessage = $"Could not find {GVFSPlatform.Instance.Constants.MountExecutableName}. You may need to reinstall GVFS."; - return false; - } - - GitProcess git = new GitProcess(enlistment); - if (!git.IsValidRepo()) - { - errorMessage = "The .git folder is missing or has invalid contents"; - return false; - } - - try - { - GitIndexProjection.ReadIndex(tracer, Path.Combine(enlistment.WorkingDirectoryBackingRoot, GVFSConstants.DotGit.Index)); - } - catch (Exception e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Exception", e.ToString()); - tracer.RelatedError(metadata, "Index validation failed"); - errorMessage = "Index validation failed, run 'gvfs repair' to repair index."; - - return false; - } - - if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(enlistment.EnlistmentRoot, out string error)) - { - errorMessage = $"FileSystem unsupported: {error}"; - return false; - } - - return true; - } - private bool TryMount(ITracer tracer, GVFSEnlistment enlistment, string mountExecutableLocation, out string errorMessage) { - if (!GVFSVerb.TrySetRequiredGitConfigSettings(enlistment)) - { - errorMessage = "Unable to configure git repo"; - return false; - } - const string ParamPrefix = "--"; tracer.RelatedInfo($"{nameof(this.TryMount)}: Launching background process('{mountExecutableLocation}') for {enlistment.EnlistmentRoot}"); @@ -355,14 +237,14 @@ private bool RegisterMount(GVFSEnlistment enlistment, out string errorMessage) } } - private void ParseEnumArgs(out EventLevel verbosity, out Keywords keywords) + private void ValidateEnumArgs() { - if (!Enum.TryParse(this.KeywordsCsv, out keywords)) + if (!Enum.TryParse(this.KeywordsCsv, out Keywords _)) { this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv); } - if (!Enum.TryParse(this.Verbosity, out verbosity)) + if (!Enum.TryParse(this.Verbosity, out EventLevel _)) { this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity); } From a917f385a4edf27f2ff73b119122b77c46a95421 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Tue, 3 Mar 2026 14:01:19 -0800 Subject: [PATCH 2/4] Parallelize pre-mount validations with network operations Start auth + config query immediately on entry to Mount(), before repo metadata loading. This overlaps network latency with all local I/O: metadata, git version/hooks/filesystem checks, and git config writes. The network task (auth + config) and local task (validations + git config) run concurrently via Task.WhenAll. RepoMetadata loading runs on the main thread between task launch and local task start, overlapping with the initial anonymous auth probe. Measured improvement on os.2020 (2.4M index entries): Production: ~29s wall clock Sequential: ~22s (prior commit moved work to mount.exe) Parallel: ~19s (this commit) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- GVFS/GVFS.Mount/InProcessMount.cs | 82 +++++++++++++++++++++++-------- 1 file changed, 61 insertions(+), 21 deletions(-) diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index b34aa804f..6f6fcb12d 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -18,6 +18,7 @@ using System.Security; using System.Text; using System.Threading; +using System.Threading.Tasks; using static GVFS.Common.Git.LibGit2Repo; namespace GVFS.Mount @@ -84,6 +85,27 @@ public void Mount(EventLevel verbosity, Keywords keywords) { this.currentState = MountState.Mounting; + // Start auth + config query immediately — these are network-bound and don't + // depend on repo metadata or cache paths. Every millisecond of network latency + // we can overlap with local I/O is a win. + Stopwatch parallelTimer = Stopwatch.StartNew(); + + var networkTask = Task.Run(() => + { + Stopwatch sw = Stopwatch.StartNew(); + string authError; + if (!this.enlistment.Authentication.TryInitialize(this.tracer, this.enlistment, out authError)) + { + this.tracer.RelatedWarning("Mount will proceed, but new files cannot be accessed until GVFS can authenticate: " + authError); + } + + this.tracer.RelatedInfo("ParallelMount: Auth completed in {0}ms", sw.ElapsedMilliseconds); + + ServerGVFSConfig config = this.QueryAndValidateGVFSConfig(); + this.tracer.RelatedInfo("ParallelMount: Auth + config query completed in {0}ms", sw.ElapsedMilliseconds); + return config; + }); + // We must initialize repo metadata before starting the pipe server so it // can immediately handle status requests string error; @@ -122,39 +144,57 @@ public void Mount(EventLevel verbosity, Keywords keywords) this.enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot); - if (!this.enlistment.Authentication.TryInitialize(this.tracer, this.enlistment, out error)) + // Local validations and git config run while we wait for the network + var localTask = Task.Run(() => { - this.tracer.RelatedWarning("Mount will proceed, but new files cannot be accessed until GVFS can authenticate: " + error); - } + Stopwatch sw = Stopwatch.StartNew(); - this.ValidateGitVersion(); - this.ValidateHooksVersion(); - this.ValidateFileSystemSupportsRequiredFeatures(); + this.ValidateGitVersion(); + this.tracer.RelatedInfo("ParallelMount: ValidateGitVersion completed in {0}ms", sw.ElapsedMilliseconds); - ServerGVFSConfig serverGVFSConfig = this.QueryAndValidateGVFSConfig(); + this.ValidateHooksVersion(); + this.ValidateFileSystemSupportsRequiredFeatures(); - CacheServerResolver cacheServerResolver = new CacheServerResolver(this.tracer, this.enlistment); - this.cacheServer = cacheServerResolver.ResolveNameFromRemote(this.cacheServer.Url, serverGVFSConfig); + GitProcess git = new GitProcess(this.enlistment); + if (!git.IsValidRepo()) + { + this.FailMountAndExit("The .git folder is missing or has invalid contents"); + } - this.EnsureLocalCacheIsHealthy(serverGVFSConfig); + if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.EnlistmentRoot, out string fsError)) + { + this.FailMountAndExit("FileSystem unsupported: " + fsError); + } - GitProcess git = new GitProcess(this.enlistment); - if (!git.IsValidRepo()) - { - this.FailMountAndExit("The .git folder is missing or has invalid contents"); - } + this.tracer.RelatedInfo("ParallelMount: Local validations completed in {0}ms", sw.ElapsedMilliseconds); + + if (!this.TrySetRequiredGitConfigSettings()) + { + this.FailMountAndExit("Unable to configure git repo"); + } - if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.EnlistmentRoot, out error)) + this.LogEnlistmentInfoAndSetConfigValues(); + this.tracer.RelatedInfo("ParallelMount: Local validations + git config completed in {0}ms", sw.ElapsedMilliseconds); + }); + + try { - this.FailMountAndExit("FileSystem unsupported: " + error); + Task.WaitAll(networkTask, localTask); } - - if (!this.TrySetRequiredGitConfigSettings()) + catch (AggregateException ae) { - this.FailMountAndExit("Unable to configure git repo"); + this.FailMountAndExit(ae.Flatten().InnerExceptions[0].Message); } - this.LogEnlistmentInfoAndSetConfigValues(); + parallelTimer.Stop(); + this.tracer.RelatedInfo("ParallelMount: All parallel tasks completed in {0}ms", parallelTimer.ElapsedMilliseconds); + + ServerGVFSConfig serverGVFSConfig = networkTask.Result; + + CacheServerResolver cacheServerResolver = new CacheServerResolver(this.tracer, this.enlistment); + this.cacheServer = cacheServerResolver.ResolveNameFromRemote(this.cacheServer.Url, serverGVFSConfig); + + this.EnsureLocalCacheIsHealthy(serverGVFSConfig); using (NamedPipeServer pipeServer = this.StartNamedPipe()) { From 2d48281480e6a41f042f9c6938bcc2620ce2324a Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 4 Mar 2026 11:54:53 -0800 Subject: [PATCH 3/4] Combine auth initialization with config query Add TryInitializeAndQueryGVFSConfig to GitAuthentication that merges the anonymous probe, credential fetch, and config query into a single flow, making at most 2 HTTP requests (or 1 for anonymous repos) and reusing the same TCP/TLS connection. Refactor TryInitialize to delegate to the combined method, eliminating the duplicated TryAnonymousQuery logic. Add TryAuthenticateAndQueryGVFSConfig to GVFSVerb and update CloneVerb, PrefetchVerb, and CacheServerVerb to use it, replacing the two-step TryAuthenticate + QueryGVFSConfig pattern with a single call. Remove the now-unused QueryGVFSConfigWithFallbackCacheServer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- GVFS/GVFS.Common/Git/GitAuthentication.cs | 129 +++++++++++++--------- GVFS/GVFS.Mount/InProcessMount.cs | 27 +++-- GVFS/GVFS/CommandLine/CacheServerVerb.cs | 36 +++--- GVFS/GVFS/CommandLine/CloneVerb.cs | 16 +-- GVFS/GVFS/CommandLine/GVFSVerb.cs | 87 +++++++-------- GVFS/GVFS/CommandLine/PrefetchVerb.cs | 20 ++-- 6 files changed, 173 insertions(+), 142 deletions(-) diff --git a/GVFS/GVFS.Common/Git/GitAuthentication.cs b/GVFS/GVFS.Common/Git/GitAuthentication.cs index 27796f0e8..bb81a86c1 100644 --- a/GVFS/GVFS.Common/Git/GitAuthentication.cs +++ b/GVFS/GVFS.Common/Git/GitAuthentication.cs @@ -183,34 +183,98 @@ public bool TryGetCredentials(ITracer tracer, out string credentialString, out s return true; } + /// + /// Initialize authentication by probing the server. Determines whether + /// anonymous access is supported and, if not, fetches credentials. + /// Callers that also need the GVFS config should use + /// instead to avoid a + /// redundant HTTP round-trip. + /// public bool TryInitialize(ITracer tracer, Enlistment enlistment, out string errorMessage) + { + // Delegate to the combined method, discarding the config result. + // This avoids duplicating the anonymous-probe + credential-fetch logic. + return this.TryInitializeAndQueryGVFSConfig( + tracer, + enlistment, + new RetryConfig(), + out _, + out errorMessage); + } + + /// + /// Combines authentication initialization with the GVFS config query, + /// eliminating a redundant HTTP round-trip. The anonymous probe and + /// config query use the same request to /gvfs/config: + /// 1. Config query → /gvfs/config → 200 (anonymous) or 401 + /// 2. If 401: credential fetch, then retry → 200 + /// This saves one HTTP request compared to probing auth separately + /// and then querying config, and reuses the same TCP/TLS connection. + /// + public bool TryInitializeAndQueryGVFSConfig( + ITracer tracer, + Enlistment enlistment, + RetryConfig retryConfig, + out ServerGVFSConfig serverGVFSConfig, + out string errorMessage) { if (this.isInitialized) { throw new InvalidOperationException("Already initialized"); } + serverGVFSConfig = null; errorMessage = null; - bool isAnonymous; - if (!this.TryAnonymousQuery(tracer, enlistment, out isAnonymous)) + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) { - errorMessage = $"Unable to determine if authentication is required"; - return false; - } + HttpStatusCode? httpStatus; - if (!isAnonymous && - !this.TryCallGitCredential(tracer, out errorMessage)) - { + // First attempt without credentials. If anonymous access works, + // we get the config in a single request. + if (configRequestor.TryQueryGVFSConfig(false, out serverGVFSConfig, out httpStatus, out _)) + { + this.IsAnonymous = true; + this.isInitialized = true; + tracer.RelatedInfo("{0}: Anonymous access succeeded, config obtained in one request", nameof(this.TryInitializeAndQueryGVFSConfig)); + return true; + } + + if (httpStatus != HttpStatusCode.Unauthorized) + { + errorMessage = "Unable to query /gvfs/config"; + tracer.RelatedWarning("{0}: Config query failed with status {1}", nameof(this.TryInitializeAndQueryGVFSConfig), httpStatus?.ToString() ?? "None"); + return false; + } + + // Server requires authentication — fetch credentials + this.IsAnonymous = false; + + if (!this.TryCallGitCredential(tracer, out errorMessage)) + { + tracer.RelatedWarning("{0}: Credential fetch failed: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage); + return false; + } + + this.isInitialized = true; + + // Retry with credentials using the same ConfigHttpRequestor (reuses HttpClient/connection) + if (configRequestor.TryQueryGVFSConfig(true, out serverGVFSConfig, out _, out errorMessage)) + { + tracer.RelatedInfo("{0}: Config obtained with credentials", nameof(this.TryInitializeAndQueryGVFSConfig)); + return true; + } + + tracer.RelatedWarning("{0}: Config query failed with credentials: {1}", nameof(this.TryInitializeAndQueryGVFSConfig), errorMessage); return false; } - - this.IsAnonymous = isAnonymous; - this.isInitialized = true; - return true; } - public bool TryInitializeAndRequireAuth(ITracer tracer, out string errorMessage) + /// + /// Test-only initialization that skips the network probe and goes + /// straight to credential fetch. Not for production use. + /// + internal bool TryInitializeAndRequireAuth(ITracer tracer, out string errorMessage) { if (this.isInitialized) { @@ -267,45 +331,6 @@ private static bool TryParseCredentialString(string credentialString, out string return false; } - private bool TryAnonymousQuery(ITracer tracer, Enlistment enlistment, out bool isAnonymous) - { - bool querySucceeded; - using (ITracer anonymousTracer = tracer.StartActivity("AttemptAnonymousAuth", EventLevel.Informational)) - { - HttpStatusCode? httpStatus; - - using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(anonymousTracer, enlistment, new RetryConfig())) - { - ServerGVFSConfig gvfsConfig; - const bool LogErrors = false; - if (configRequestor.TryQueryGVFSConfig(LogErrors, out gvfsConfig, out httpStatus, out _)) - { - querySucceeded = true; - isAnonymous = true; - } - else if (httpStatus == HttpStatusCode.Unauthorized) - { - querySucceeded = true; - isAnonymous = false; - } - else - { - querySucceeded = false; - isAnonymous = false; - } - } - - anonymousTracer.Stop(new EventMetadata - { - { "HttpStatus", httpStatus.HasValue ? ((int)httpStatus).ToString() : "None" }, - { "QuerySucceeded", querySucceeded }, - { "IsAnonymous", isAnonymous }, - }); - } - - return querySucceeded; - } - private DateTime GetNextAuthAttemptTime() { if (this.numberOfAttempts <= 1) diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index 6f6fcb12d..f09300057 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -88,21 +88,34 @@ public void Mount(EventLevel verbosity, Keywords keywords) // Start auth + config query immediately — these are network-bound and don't // depend on repo metadata or cache paths. Every millisecond of network latency // we can overlap with local I/O is a win. + // TryInitializeAndQueryGVFSConfig combines the anonymous probe, credential fetch, + // and config query into at most 2 HTTP requests (1 for anonymous repos), reusing + // the same HttpClient/TCP connection. Stopwatch parallelTimer = Stopwatch.StartNew(); var networkTask = Task.Run(() => { Stopwatch sw = Stopwatch.StartNew(); - string authError; - if (!this.enlistment.Authentication.TryInitialize(this.tracer, this.enlistment, out authError)) + ServerGVFSConfig config; + string authConfigError; + + if (!this.enlistment.Authentication.TryInitializeAndQueryGVFSConfig( + this.tracer, this.enlistment, this.retryConfig, + out config, out authConfigError)) { - this.tracer.RelatedWarning("Mount will proceed, but new files cannot be accessed until GVFS can authenticate: " + authError); + if (this.cacheServer != null && !string.IsNullOrWhiteSpace(this.cacheServer.Url)) + { + this.tracer.RelatedWarning("Mount will proceed with fallback cache server: " + authConfigError); + config = null; + } + else + { + this.FailMountAndExit("Unable to query /gvfs/config" + Environment.NewLine + authConfigError); + } } - this.tracer.RelatedInfo("ParallelMount: Auth completed in {0}ms", sw.ElapsedMilliseconds); - - ServerGVFSConfig config = this.QueryAndValidateGVFSConfig(); - this.tracer.RelatedInfo("ParallelMount: Auth + config query completed in {0}ms", sw.ElapsedMilliseconds); + this.ValidateGVFSVersion(config); + this.tracer.RelatedInfo("ParallelMount: Auth + config completed in {0}ms", sw.ElapsedMilliseconds); return config; }); diff --git a/GVFS/GVFS/CommandLine/CacheServerVerb.cs b/GVFS/GVFS/CommandLine/CacheServerVerb.cs index 86754ae67..9fedad0b0 100644 --- a/GVFS/GVFS/CommandLine/CacheServerVerb.cs +++ b/GVFS/GVFS/CommandLine/CacheServerVerb.cs @@ -42,12 +42,6 @@ protected override void Execute(GVFSEnlistment enlistment) using (ITracer tracer = new JsonTracer(GVFSConstants.GVFSEtwProviderName, "CacheVerb")) { - string authErrorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) - { - this.ReportErrorAndExit(tracer, "Authentication failed: " + authErrorMessage); - } - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); ServerGVFSConfig serverGVFSConfig = null; string error = null; @@ -55,8 +49,12 @@ protected override void Execute(GVFSEnlistment enlistment) // Handle the three operation types: list, set, and get (default) if (this.ListCacheServers) { - // For listing, require config endpoint to succeed - serverGVFSConfig = this.QueryGVFSConfig(tracer, enlistment, retryConfig); + // For listing, require config endpoint to succeed (no fallback) + if (!this.TryAuthenticateAndQueryGVFSConfig( + tracer, enlistment, retryConfig, out serverGVFSConfig, out error)) + { + this.ReportErrorAndExit(tracer, "Unable to query /gvfs/config" + Environment.NewLine + error); + } List cacheServers = serverGVFSConfig.CacheServers.ToList(); @@ -80,11 +78,12 @@ protected override void Execute(GVFSEnlistment enlistment) 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); + if (!this.TryAuthenticateAndQueryGVFSConfig( + tracer, enlistment, retryConfig, out serverGVFSConfig, out error, + fallbackCacheServer: cacheServer)) + { + this.ReportErrorAndExit(tracer, "Authentication failed: " + error); + } cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); @@ -101,11 +100,12 @@ protected override void Execute(GVFSEnlistment enlistment) 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); + if (!this.TryAuthenticateAndQueryGVFSConfig( + tracer, enlistment, retryConfig, out serverGVFSConfig, out error, + fallbackCacheServer: cacheServer)) + { + this.ReportErrorAndExit(tracer, "Authentication failed: " + error); + } CacheServerInfo resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverGVFSConfig); diff --git a/GVFS/GVFS/CommandLine/CloneVerb.cs b/GVFS/GVFS/CommandLine/CloneVerb.cs index 8bbc5b9fb..bd37c7d4b 100644 --- a/GVFS/GVFS/CommandLine/CloneVerb.cs +++ b/GVFS/GVFS/CommandLine/CloneVerb.cs @@ -185,19 +185,19 @@ public override void Execute() this.Output.WriteLine(" Local Cache: " + resolvedLocalCacheRoot); this.Output.WriteLine(" Destination: " + enlistment.EnlistmentRoot); - string authErrorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) - { - this.ReportErrorAndExit(tracer, "Cannot clone because authentication failed: " + authErrorMessage); - } - RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); - serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + string authErrorMessage; + if (!this.TryAuthenticateAndQueryGVFSConfig( tracer, enlistment, retryConfig, - cacheServer); + out serverGVFSConfig, + out authErrorMessage, + fallbackCacheServer: cacheServer)) + { + this.ReportErrorAndExit(tracer, "Cannot clone because authentication failed: " + authErrorMessage); + } cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverGVFSConfig); diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index fe0731a00..ab97fbabd 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -429,14 +429,50 @@ protected bool ShowStatusWhileRunning( protected bool TryAuthenticate(ITracer tracer, GVFSEnlistment enlistment, out string authErrorMessage) { - string authError = null; + return this.TryAuthenticateAndQueryGVFSConfig(tracer, enlistment, null, out _, out authErrorMessage); + } + + /// + /// Combines authentication and GVFS config query into a single operation, + /// eliminating a redundant HTTP round-trip. If + /// is null, a default RetryConfig is used. + /// If the config query fails but a valid + /// URL is available, auth succeeds but + /// will be null (caller should handle this gracefully). + /// + protected bool TryAuthenticateAndQueryGVFSConfig( + ITracer tracer, + GVFSEnlistment enlistment, + RetryConfig retryConfig, + out ServerGVFSConfig serverGVFSConfig, + out string errorMessage, + CacheServerInfo fallbackCacheServer = null) + { + ServerGVFSConfig config = null; + string error = null; bool result = this.ShowStatusWhileRunning( - () => enlistment.Authentication.TryInitialize(tracer, enlistment, out authError), + () => enlistment.Authentication.TryInitializeAndQueryGVFSConfig( + tracer, + enlistment, + retryConfig ?? new RetryConfig(), + out config, + out error), "Authenticating", enlistment.EnlistmentRoot); - authErrorMessage = authError; + if (!result && fallbackCacheServer != null && !string.IsNullOrWhiteSpace(fallbackCacheServer.Url)) + { + // Auth/config query failed, but we have a fallback cache server. + // Allow auth to succeed so mount/clone can proceed; config will be null. + tracer.RelatedWarning("Config query failed but continuing with fallback cache server: " + error); + serverGVFSConfig = null; + errorMessage = null; + return true; + } + + serverGVFSConfig = config; + errorMessage = error; return result; } @@ -493,50 +529,7 @@ protected RetryConfig GetRetryConfig(ITracer tracer, GVFSEnlistment enlistment, return retryConfig; } - /// - /// 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, - 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 + // QueryGVFSConfig for callers that require config to succeed (no fallback) protected ServerGVFSConfig QueryGVFSConfig(ITracer tracer, GVFSEnlistment enlistment, RetryConfig retryConfig) { ServerGVFSConfig serverGVFSConfig = null; diff --git a/GVFS/GVFS/CommandLine/PrefetchVerb.cs b/GVFS/GVFS/CommandLine/PrefetchVerb.cs index ab72b5e9f..1dd31b3b1 100644 --- a/GVFS/GVFS/CommandLine/PrefetchVerb.cs +++ b/GVFS/GVFS/CommandLine/PrefetchVerb.cs @@ -243,23 +243,23 @@ private void InitializeServerConnection( // 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)) - { - this.ReportErrorAndExit(tracer, "Unable to prefetch because authentication failed: " + authErrorMessage); - } - - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - if (serverGVFSConfig == null) { - serverGVFSConfig = this.QueryGVFSConfigWithFallbackCacheServer( + string authErrorMessage; + if (!this.TryAuthenticateAndQueryGVFSConfig( tracer, enlistment, retryConfig, - cacheServerFromConfig); + out serverGVFSConfig, + out authErrorMessage, + fallbackCacheServer: cacheServerFromConfig)) + { + this.ReportErrorAndExit(tracer, "Unable to prefetch because authentication failed: " + authErrorMessage); + } } + CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); + resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerFromConfig.Url, serverGVFSConfig); if (!this.SkipVersionCheck) From 129adcda9f05b00b3d3e52bae3cb8d79a2165c26 Mon Sep 17 00:00:00 2001 From: Tyrie Vella Date: Wed, 4 Mar 2026 16:03:26 -0800 Subject: [PATCH 4/4] Fix duplication of GitCoreGVFSFlags --- GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs | 58 ++++++++++++++++++++++++ GVFS/GVFS.Mount/InProcessMount.cs | 10 ---- GVFS/GVFS/CommandLine/GVFSVerb.cs | 53 ---------------------- 3 files changed, 58 insertions(+), 63 deletions(-) create mode 100644 GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs diff --git a/GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs b/GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs new file mode 100644 index 000000000..411c5bc3c --- /dev/null +++ b/GVFS/GVFS.Common/Git/GitCoreGVFSFlags.cs @@ -0,0 +1,58 @@ +using System; + +namespace GVFS.Common.Git +{ + [Flags] + public enum GitCoreGVFSFlags + { + // GVFS_SKIP_SHA_ON_INDEX + // Disables the calculation of the sha when writing the index + SkipShaOnIndex = 1 << 0, + + // GVFS_BLOCK_COMMANDS + // Blocks git commands that are not allowed in a GVFS/Scalar repo + BlockCommands = 1 << 1, + + // GVFS_MISSING_OK + // Normally git write-tree ensures that the objects referenced by the + // directory exist in the object database.This option disables this check. + MissingOk = 1 << 2, + + // GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT + // When marking entries to remove from the index and the working + // directory this option will take into account what the + // skip-worktree bit was set to so that if the entry has the + // skip-worktree bit set it will not be removed from the working + // directory. This will allow virtualized working directories to + // detect the change to HEAD and use the new commit tree to show + // the files that are in the working directory. + NoDeleteOutsideSparseCheckout = 1 << 3, + + // GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK + // While performing a fetch with a virtual file system we know + // that there will be missing objects and we don't want to download + // them just because of the reachability of the commits. We also + // don't want to download a pack file with commits, trees, and blobs + // since these will be downloaded on demand. This flag will skip the + // checks on the reachability of objects during a fetch as well as + // the upload pack so that extraneous objects don't get downloaded. + FetchSkipReachabilityAndUploadPack = 1 << 4, + + // 1 << 5 has been deprecated + + // GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS + // With a virtual file system we only know the file size before any + // CRLF or smudge/clean filters processing is done on the client. + // To prevent file corruption due to truncation or expansion with + // garbage at the end, these filters must not run when the file + // is first accessed and brought down to the client. Git.exe can't + // currently tell the first access vs subsequent accesses so this + // flag just blocks them from occurring at all. + BlockFiltersAndEolConversions = 1 << 6, + + // GVFS_PREFETCH_DURING_FETCH + // While performing a `git fetch` command, use the gvfs-helper to + // perform a "prefetch" of commits and trees. + PrefetchDuringFetch = 1 << 7, + } +} diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index f09300057..c56627703 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -1255,16 +1255,6 @@ private bool TryCreateAlternatesFile(PhysicalFileSystem fileSystem, out string e return true; } - [Flags] - private enum GitCoreGVFSFlags - { - SkipShaOnIndex = 1 << 0, - BlockCommands = 1 << 1, - MissingOk = 1 << 2, - NoDeleteOutsideSparseCheckout = 1 << 3, - FetchSkipReachabilityAndUploadPack = 1 << 4, - BlockFiltersAndEolConversions = 1 << 6, - } private bool TrySetRequiredGitConfigSettings() { diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs index ab97fbabd..c2a4060d1 100644 --- a/GVFS/GVFS/CommandLine/GVFSVerb.cs +++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs @@ -36,59 +36,6 @@ public GVFSVerb(bool validateOrigin = true) this.InitializeDefaultParameterValues(); } - [Flags] - private enum GitCoreGVFSFlags - { - // GVFS_SKIP_SHA_ON_INDEX - // Disables the calculation of the sha when writing the index - SkipShaOnIndex = 1 << 0, - - // GVFS_BLOCK_COMMANDS - // Blocks git commands that are not allowed in a GVFS/Scalar repo - BlockCommands = 1 << 1, - - // GVFS_MISSING_OK - // Normally git write-tree ensures that the objects referenced by the - // directory exist in the object database.This option disables this check. - MissingOk = 1 << 2, - - // GVFS_NO_DELETE_OUTSIDE_SPARSECHECKOUT - // When marking entries to remove from the index and the working - // directory this option will take into account what the - // skip-worktree bit was set to so that if the entry has the - // skip-worktree bit set it will not be removed from the working - // directory. This will allow virtualized working directories to - // detect the change to HEAD and use the new commit tree to show - // the files that are in the working directory. - NoDeleteOutsideSparseCheckout = 1 << 3, - - // GVFS_FETCH_SKIP_REACHABILITY_AND_UPLOADPACK - // While performing a fetch with a virtual file system we know - // that there will be missing objects and we don't want to download - // them just because of the reachability of the commits. We also - // don't want to download a pack file with commits, trees, and blobs - // since these will be downloaded on demand. This flag will skip the - // checks on the reachability of objects during a fetch as well as - // the upload pack so that extraneous objects don't get downloaded. - FetchSkipReachabilityAndUploadPack = 1 << 4, - - // 1 << 5 has been deprecated - - // GVFS_BLOCK_FILTERS_AND_EOL_CONVERSIONS - // With a virtual file system we only know the file size before any - // CRLF or smudge/clean filters processing is done on the client. - // To prevent file corruption due to truncation or expansion with - // garbage at the end, these filters must not run when the file - // is first accessed and brought down to the client. Git.exe can't - // currently tell the first access vs subsequent accesses so this - // flag just blocks them from occurring at all. - BlockFiltersAndEolConversions = 1 << 6, - - // GVFS_PREFETCH_DURING_FETCH - // While performing a `git fetch` command, use the gvfs-helper to - // perform a "prefetch" of commits and trees. - PrefetchDuringFetch = 1 << 7, - } public abstract string EnlistmentRootPathParameter { get; set; }