diff --git a/GVFS/GVFS.Common/Maintenance/PrefetchStep.cs b/GVFS/GVFS.Common/Maintenance/PrefetchStep.cs index 163089afb..4f7ee83d3 100644 --- a/GVFS/GVFS.Common/Maintenance/PrefetchStep.cs +++ b/GVFS/GVFS.Common/Maintenance/PrefetchStep.cs @@ -19,10 +19,18 @@ public class PrefetchStep : GitMaintenanceStep private const int NoExistingPrefetchPacks = -1; private readonly TimeSpan timeBetweenPrefetches = TimeSpan.FromMinutes(70); + private readonly Action> postFetchCallback; + public PrefetchStep(GVFSContext context, GitObjects gitObjects, bool requireCacheLock = true) + : this(context, gitObjects, requireCacheLock, postFetchCallback: null) + { + } + + public PrefetchStep(GVFSContext context, GitObjects gitObjects, bool requireCacheLock, Action> postFetchCallback) : base(context, requireCacheLock) { this.GitObjects = gitObjects; + this.postFetchCallback = postFetchCallback; } public override string Area => "PrefetchStep"; @@ -283,6 +291,14 @@ private void SchedulePostFetchJob(List packIndexes) return; } + // When running inside the mount process, use the injected callback to + // enqueue the post-fetch step directly (avoids re-entrant named pipe IPC). + if (this.postFetchCallback != null) + { + this.postFetchCallback(packIndexes); + return; + } + // We make a best-effort request to run MIDX and commit-graph writes using (NamedPipeClient pipeClient = new NamedPipeClient(this.Context.Enlistment.NamedPipeName)) { diff --git a/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs b/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs index d42c84873..121ddf75c 100644 --- a/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs +++ b/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs @@ -313,6 +313,73 @@ public Message CreateMessage() } } + public static class PrefetchCommits + { + public const string Request = "PrefetchCommits"; + public const string CompleteResult = "PrefetchCommitsComplete"; + public const string MountNotReadyResult = "MountNotReady"; + + public class Response + { + public bool Success { get; set; } + public string Error { get; set; } + + public static Response FromMessage(Message message) + { + return GVFSJsonOptions.Deserialize(message.Body); + } + + public Message CreateMessage() + { + return new Message(CompleteResult, GVFSJsonOptions.Serialize(this)); + } + } + } + + public static class PrefetchBlobs + { + public const string RequestHeader = "PrefetchBlobs"; + public const string CompleteResult = "PrefetchBlobsComplete"; + public const string MountNotReadyResult = "MountNotReady"; + + public class Request + { + public List Files { get; set; } + public List Folders { get; set; } + public string HeadCommitId { get; set; } + public bool HydrateFiles { get; set; } + + public static Request FromMessage(Message message) + { + return GVFSJsonOptions.Deserialize(message.Body); + } + + public Message CreateMessage() + { + return new Message(RequestHeader, GVFSJsonOptions.Serialize(this)); + } + } + + public class Response + { + public bool Success { get; set; } + public string Error { get; set; } + public int MatchedBlobCount { get; set; } + public int DownloadedBlobCount { get; set; } + public int HydratedFileCount { get; set; } + + public static Response FromMessage(Message message) + { + return GVFSJsonOptions.Deserialize(message.Body); + } + + public Message CreateMessage() + { + return new Message(CompleteResult, GVFSJsonOptions.Serialize(this)); + } + } + } + public static class Notification { public class Request diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchBlobsOffloadTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchBlobsOffloadTests.cs new file mode 100644 index 000000000..c3fcf977b --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchBlobsOffloadTests.cs @@ -0,0 +1,80 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + public class PrefetchBlobsOffloadTests : TestsWithEnlistmentPerFixture + { + private FileSystemRunner fileSystem; + + public PrefetchBlobsOffloadTests() + { + this.fileSystem = new SystemIORunner(); + } + + [TestCase, Order(1)] + public void PrefetchBlobsMountedUsesOffload() + { + // With the enlistment mounted, blob prefetch should succeed + // by offloading to the mount process (using its warm auth). + string output = this.Enlistment.Prefetch($"--files {Path.Combine("GVFS", "GVFS", "Program.cs")}"); + output.ShouldContain("Matched blobs:"); + output.ShouldContain("Downloaded:"); + } + + [TestCase, Order(2)] + public void PrefetchBlobsMountedReportsStats() + { + // Prefetch multiple files and verify stats are reported + string output = this.Enlistment.Prefetch( + $"--files {Path.Combine("GVFS", "GVFS", "Program.cs")};{Path.Combine("GVFS", "GVFS.FunctionalTests", "GVFS.FunctionalTests.csproj")}"); + output.ShouldContain("Matched blobs:"); + output.ShouldContain("Already cached:"); + output.ShouldContain("Downloaded:"); + } + + [TestCase, Order(3)] + public void PrefetchBlobsUnmountedFallsBackToDirectAuth() + { + // Unmount, then blob prefetch should fall back to direct auth + // and still succeed. + this.Enlistment.UnmountGVFS(); + + try + { + string output = this.Enlistment.Prefetch($"--files {Path.Combine("GVFS", "GVFS", "Program.cs")}"); + output.ShouldContain("Matched blobs:"); + output.ShouldContain("Downloaded:"); + } + finally + { + this.Enlistment.MountGVFS(); + } + } + + [TestCase, Order(4)] + public void PrefetchBlobsMountedWithFolders() + { + // Prefetch a folder while mounted + string output = this.Enlistment.Prefetch("--folders GVFS/GVFS"); + output.ShouldContain("Matched blobs:"); + } + + [TestCase, Order(5)] + public void PrefetchBlobsMountedAfterRemount() + { + // After unmount + remount, blob prefetch should work via + // the mount process again. + this.Enlistment.UnmountGVFS(); + this.Enlistment.MountGVFS(); + + string output = this.Enlistment.Prefetch($"--files {Path.Combine("GVFS", "GVFS", "Program.cs")}"); + output.ShouldContain("Matched blobs:"); + } + } +} diff --git a/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchCommitsOffloadTests.cs b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchCommitsOffloadTests.cs new file mode 100644 index 000000000..e763761fd --- /dev/null +++ b/GVFS/GVFS.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchCommitsOffloadTests.cs @@ -0,0 +1,128 @@ +using GVFS.FunctionalTests.FileSystemRunners; +using GVFS.FunctionalTests.Should; +using GVFS.FunctionalTests.Tools; +using GVFS.Tests.Should; +using NUnit.Framework; +using System.IO; + +namespace GVFS.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + public class PrefetchCommitsOffloadTests : TestsWithEnlistmentPerFixture + { + private const string PrefetchPackPrefix = "prefetch"; + + private FileSystemRunner fileSystem; + + public PrefetchCommitsOffloadTests() + : base(forcePerRepoObjectCache: true, skipPrefetchDuringClone: true) + { + this.fileSystem = new SystemIORunner(); + } + + private string PackRoot + { + get + { + return this.Enlistment.GetPackRoot(this.fileSystem); + } + } + + [TestCase, Order(1)] + public void PrefetchCommitsMountedUsesOffload() + { + // With the enlistment mounted, prefetch --commits should succeed + // by offloading to the mount process (using its warm auth). + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack after mounted prefetch"); + this.AllPrefetchPacksShouldHaveIdx(prefetchPacks); + } + + [TestCase, Order(2)] + public void PrefetchCommitsMountedIsIdempotent() + { + // Running prefetch --commits again while mounted should succeed + // (may be a no-op if packs are already up to date). + string[] packsBefore = this.ReadPrefetchPackFileNames(); + + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + string[] packsAfter = this.ReadPrefetchPackFileNames(); + packsAfter.Length.ShouldBeAtLeast(packsBefore.Length, "Pack count should not decrease after idempotent prefetch"); + this.AllPrefetchPacksShouldHaveIdx(packsAfter); + } + + [TestCase, Order(3)] + public void PrefetchCommitsUnmountedFallsBackToDirectAuth() + { + // Unmount, then prefetch --commits should fall back to direct auth + // and still succeed. + this.Enlistment.UnmountGVFS(); + + try + { + this.Enlistment.Prefetch("--commits"); + + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack after unmounted prefetch"); + this.AllPrefetchPacksShouldHaveIdx(prefetchPacks); + } + finally + { + this.Enlistment.MountGVFS(); + } + } + + [TestCase, Order(4)] + public void PrefetchCommitsMountedAfterRemount() + { + // After unmount + remount, prefetch --commits should work via + // the mount process again. + this.Enlistment.UnmountGVFS(); + this.Enlistment.MountGVFS(); + + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack after remount prefetch"); + this.AllPrefetchPacksShouldHaveIdx(prefetchPacks); + } + + private string[] ReadPrefetchPackFileNames() + { + return Directory.GetFiles(this.PackRoot, $"{PrefetchPackPrefix}*.pack"); + } + + private void AllPrefetchPacksShouldHaveIdx(string[] prefetchPacks) + { + foreach (string prefetchPack in prefetchPacks) + { + string idxPath = Path.ChangeExtension(prefetchPack, ".idx"); + idxPath.ShouldBeAFile(this.fileSystem); + } + } + + private void PostFetchJobShouldComplete() + { + string objectDir = this.Enlistment.GetObjectRoot(this.fileSystem); + string postFetchLock = Path.Combine(objectDir, "git-maintenance-step.lock"); + + System.Diagnostics.Stopwatch timeout = System.Diagnostics.Stopwatch.StartNew(); + while (this.fileSystem.FileExists(postFetchLock)) + { + timeout.Elapsed.TotalSeconds.ShouldBeAtMost(60, "Post-fetch lock file was not released within 60 seconds"); + System.Threading.Thread.Sleep(500); + } + + ProcessResult graphResult = GitProcess.InvokeProcess( + this.Enlistment.RepoRoot, + "commit-graph verify --shallow --object-dir=\"" + objectDir + "\""); + graphResult.ExitCode.ShouldEqual(0); + } + } +} diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs index 1272eb876..273c31df7 100644 --- a/GVFS/GVFS.Mount/InProcessMount.cs +++ b/GVFS/GVFS.Mount/InProcessMount.cs @@ -5,6 +5,7 @@ using GVFS.Common.Http; using GVFS.Common.Maintenance; using GVFS.Common.NamedPipes; +using GVFS.Common.Prefetch; using GVFS.Common.Tracing; using GVFS.PlatformLoader; using GVFS.Virtualization; @@ -516,6 +517,14 @@ private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Conne this.HandleDehydrateFolders(message, connection); break; + case NamedPipeMessages.PrefetchCommits.Request: + this.HandlePrefetchCommitsRequest(connection); + break; + + case NamedPipeMessages.PrefetchBlobs.RequestHeader: + this.HandlePrefetchBlobsRequest(message, connection); + break; + case NamedPipeMessages.HydrationStatus.Request: this.HandleGetHydrationStatusRequest(connection); break; @@ -993,6 +1002,177 @@ private void HandlePostFetchJobRequest(NamedPipeMessages.Message message, NamedP connection.TrySendResponse(response.CreateMessage()); } + private void HandlePrefetchCommitsRequest(NamedPipeServer.Connection connection) + { + this.tracer.RelatedInfo("Received prefetch commits request"); + + if (this.currentState != MountState.Ready) + { + connection.TrySendResponse( + new NamedPipeMessages.Message(NamedPipeMessages.PrefetchCommits.MountNotReadyResult, null)); + return; + } + + NamedPipeMessages.PrefetchCommits.Response response; + try + { + // Use a callback to enqueue the post-fetch step directly on the + // maintenance scheduler, avoiding a re-entrant named pipe call. + PrefetchStep prefetchStep = new PrefetchStep( + this.context, + this.gitObjects, + requireCacheLock: false, + postFetchCallback: packIndexes => + { + this.maintenanceScheduler.EnqueueOneTimeStep(new PostFetchStep(this.context, packIndexes)); + }); + + string error; + bool success = prefetchStep.TryPrefetchCommitsAndTrees(out error); + + response = new NamedPipeMessages.PrefetchCommits.Response + { + Success = success, + Error = error, + }; + } + catch (Exception e) + { + this.tracer.RelatedError("HandlePrefetchCommitsRequest: Exception: {0}", e.ToString()); + response = new NamedPipeMessages.PrefetchCommits.Response + { + Success = false, + Error = e.Message, + }; + } + + connection.TrySendResponse(response.CreateMessage()); + } + + private void HandlePrefetchBlobsRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection) + { + this.tracer.RelatedInfo("Received prefetch blobs request"); + + if (this.currentState != MountState.Ready) + { + connection.TrySendResponse( + new NamedPipeMessages.Message(NamedPipeMessages.PrefetchBlobs.MountNotReadyResult, null)); + return; + } + + NamedPipeMessages.PrefetchBlobs.Request request = NamedPipeMessages.PrefetchBlobs.Request.FromMessage(message); + + // Validate inputs — do not trust IPC requests blindly + if (request.Files == null || request.Folders == null) + { + connection.TrySendResponse(new NamedPipeMessages.PrefetchBlobs.Response + { + Success = false, + Error = "Files and Folders must not be null", + }.CreateMessage()); + return; + } + + if (request.Files.Count == 0 && request.Folders.Count == 0) + { + connection.TrySendResponse(new NamedPipeMessages.PrefetchBlobs.Response + { + Success = false, + Error = "Files and Folders must not both be empty", + }.CreateMessage()); + return; + } + + if (string.IsNullOrWhiteSpace(request.HeadCommitId)) + { + connection.TrySendResponse(new NamedPipeMessages.PrefetchBlobs.Response + { + Success = false, + Error = "HeadCommitId must be specified", + }.CreateMessage()); + return; + } + + NamedPipeMessages.PrefetchBlobs.Response response; + try + { + // Create a fresh GitObjectsHttpRequestor using the mount's warm auth. + // BlobPrefetcher constructs its own PrefetchGitObjects internally. + using (GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor( + this.tracer, this.enlistment, this.cacheServer, this.retryConfig)) + { + // Open LastBlobPrefetch.dat so BlobPrefetcher can update noop state + string lastPrefetchPath = Path.Combine(this.enlistment.DotGVFSRoot, "LastBlobPrefetch.dat"); + FileBasedDictionary lastPrefetchArgs; + string dictError; + if (!FileBasedDictionary.TryCreate( + this.tracer, lastPrefetchPath, new PhysicalFileSystem(), + out lastPrefetchArgs, out dictError)) + { + this.tracer.RelatedWarning("HandlePrefetchBlobsRequest: Unable to load last prefetch args: " + dictError); + lastPrefetchArgs = null; + } + + // Cap thread counts to avoid starving virtualization callbacks + int maxThreads = Math.Max(1, Environment.ProcessorCount / 2); + int downloadThreads = Math.Min(maxThreads, 16); + + BlobPrefetcher blobPrefetcher = new BlobPrefetcher( + this.tracer, + this.enlistment, + objectRequestor, + request.Files, + request.Folders, + lastPrefetchArgs, + chunkSize: 4000, + searchThreadCount: maxThreads, + downloadThreadCount: downloadThreads, + indexThreadCount: maxThreads); + + int matchedBlobCount; + int downloadedBlobCount; + int hydratedFileCount; + + blobPrefetcher.PrefetchWithStats( + request.HeadCommitId, + isBranch: false, + hydrateFilesAfterDownload: request.HydrateFiles, + matchedBlobCount: out matchedBlobCount, + downloadedBlobCount: out downloadedBlobCount, + hydratedFileCount: out hydratedFileCount); + + response = new NamedPipeMessages.PrefetchBlobs.Response + { + Success = !blobPrefetcher.HasFailures, + Error = blobPrefetcher.HasFailures ? "Blob prefetch encountered failures" : null, + MatchedBlobCount = matchedBlobCount, + DownloadedBlobCount = downloadedBlobCount, + HydratedFileCount = hydratedFileCount, + }; + } + } + catch (BlobPrefetcher.FetchException e) + { + this.tracer.RelatedError("HandlePrefetchBlobsRequest: FetchException: {0}", e.Message); + response = new NamedPipeMessages.PrefetchBlobs.Response + { + Success = false, + Error = e.Message, + }; + } + catch (Exception e) + { + this.tracer.RelatedError("HandlePrefetchBlobsRequest: Exception: {0}", e.ToString()); + response = new NamedPipeMessages.PrefetchBlobs.Response + { + Success = false, + Error = e.Message, + }; + } + + connection.TrySendResponse(response.CreateMessage()); + } + private void HandleGetStatusRequest(NamedPipeServer.Connection connection) { NamedPipeMessages.GetStatus.Response response = new NamedPipeMessages.GetStatus.Response(); diff --git a/GVFS/GVFS/CommandLine/PrefetchVerb.cs b/GVFS/GVFS/CommandLine/PrefetchVerb.cs index 17483c34c..ec1d42813 100644 --- a/GVFS/GVFS/CommandLine/PrefetchVerb.cs +++ b/GVFS/GVFS/CommandLine/PrefetchVerb.cs @@ -3,6 +3,7 @@ using GVFS.Common.Git; using GVFS.Common.Http; using GVFS.Common.Maintenance; +using GVFS.Common.NamedPipes; using GVFS.Common.Prefetch; using GVFS.Common.Tracing; using System; @@ -172,15 +173,25 @@ protected override void Execute(GVFSEnlistment enlistment) this.ReportErrorAndExit(tracer, "You can only specify --hydrate with --files or --folders"); } - GitObjectsHttpRequestor objectRequestor; - CacheServerInfo resolvedCacheServer; - this.InitializeServerConnection( - tracer, - enlistment, - cacheServerFromConfig, - out objectRequestor, - out resolvedCacheServer); - this.PrefetchCommits(tracer, enlistment, objectRequestor, resolvedCacheServer); + // Try offload silently — if mount isn't available this returns + // false quickly and we fall through to the direct-auth path which + // has its own spinner. We don't wrap this in ShowStatusWhileRunning + // because a false return (mount unavailable) would print "Failed" + // to the console, which is misleading for an expected fallback. + bool offloadSucceeded = this.TryPrefetchCommitsViaMountProcess(tracer, enlistment); + + if (!offloadSucceeded) + { + GitObjectsHttpRequestor objectRequestor; + CacheServerInfo resolvedCacheServer; + this.InitializeServerConnection( + tracer, + enlistment, + cacheServerFromConfig, + out objectRequestor, + out resolvedCacheServer); + this.PrefetchCommits(tracer, enlistment, objectRequestor, resolvedCacheServer); + } } else { @@ -195,7 +206,7 @@ protected override void Execute(GVFSEnlistment enlistment) { Console.WriteLine("All requested files are already available. Nothing new to prefetch."); } - else + else if (this.HydrateFiles || !this.TryPrefetchBlobsViaMountProcess(tracer, enlistment, filesList, foldersList, headCommitId)) { GitObjectsHttpRequestor objectRequestor; CacheServerInfo resolvedCacheServer; @@ -296,6 +307,142 @@ private void InitializeServerConnection( objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, resolvedCacheServer, retryConfig); } + /// + /// Attempts to offload the commit prefetch to a running mount process, + /// which already has warm authentication. Returns true if the mount + /// handled the request (success or failure); returns false if offload + /// is unavailable and the caller should fall back to direct auth. + /// + private bool TryPrefetchCommitsViaMountProcess(ITracer tracer, GVFSEnlistment enlistment) + { + using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) + { + if (!pipeClient.Connect()) + { + tracer.RelatedInfo("TryPrefetchCommitsViaMountProcess: Mount not running, falling back to direct prefetch"); + return false; + } + + NamedPipeMessages.Message request = new NamedPipeMessages.Message(NamedPipeMessages.PrefetchCommits.Request, null); + if (!pipeClient.TrySendRequest(request)) + { + tracer.RelatedWarning("TryPrefetchCommitsViaMountProcess: Failed to send request, falling back to direct prefetch"); + return false; + } + + NamedPipeMessages.Message response; + if (!pipeClient.TryReadResponse(out response)) + { + tracer.RelatedWarning("TryPrefetchCommitsViaMountProcess: Failed to read response, falling back to direct prefetch"); + return false; + } + + switch (response.Header) + { + case NamedPipeMessages.PrefetchCommits.CompleteResult: + NamedPipeMessages.PrefetchCommits.Response prefetchResponse = + NamedPipeMessages.PrefetchCommits.Response.FromMessage(response); + + if (prefetchResponse.Success) + { + tracer.RelatedInfo("TryPrefetchCommitsViaMountProcess: Mount completed prefetch successfully"); + return true; + } + + this.ReportErrorAndExit(tracer, "Prefetching commits and trees failed (via mount): " + prefetchResponse.Error); + return true; + + case NamedPipeMessages.PrefetchCommits.MountNotReadyResult: + tracer.RelatedInfo("TryPrefetchCommitsViaMountProcess: Mount not ready, falling back to direct prefetch"); + return false; + + default: + // Older mount that doesn't recognize PrefetchCommits + tracer.RelatedInfo("TryPrefetchCommitsViaMountProcess: Unexpected response '{0}', falling back to direct prefetch", response.Header); + return false; + } + } + } + + /// + /// Attempts to offload the blob prefetch to a running mount process, + /// which already has warm authentication. Returns true if the mount + /// handled the request (success or failure); returns false if offload + /// is unavailable and the caller should fall back to direct auth. + /// + private bool TryPrefetchBlobsViaMountProcess( + ITracer tracer, + GVFSEnlistment enlistment, + List filesList, + List foldersList, + string headCommitId) + { + using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) + { + if (!pipeClient.Connect()) + { + tracer.RelatedInfo("TryPrefetchBlobsViaMountProcess: Mount not running, falling back to direct prefetch"); + return false; + } + + NamedPipeMessages.PrefetchBlobs.Request request = new NamedPipeMessages.PrefetchBlobs.Request + { + Files = filesList, + Folders = foldersList, + HeadCommitId = headCommitId, + HydrateFiles = this.HydrateFiles, + }; + + if (!pipeClient.TrySendRequest(request.CreateMessage())) + { + tracer.RelatedWarning("TryPrefetchBlobsViaMountProcess: Failed to send request, falling back to direct prefetch"); + return false; + } + + NamedPipeMessages.Message response; + if (!pipeClient.TryReadResponse(out response)) + { + tracer.RelatedWarning("TryPrefetchBlobsViaMountProcess: Failed to read response, falling back to direct prefetch"); + return false; + } + + switch (response.Header) + { + case NamedPipeMessages.PrefetchBlobs.CompleteResult: + NamedPipeMessages.PrefetchBlobs.Response blobResponse = + NamedPipeMessages.PrefetchBlobs.Response.FromMessage(response); + + if (blobResponse.Success) + { + tracer.RelatedInfo("TryPrefetchBlobsViaMountProcess: Mount completed blob prefetch successfully"); + + Console.WriteLine(); + Console.WriteLine("Stats:"); + Console.WriteLine(" Matched blobs: " + blobResponse.MatchedBlobCount); + Console.WriteLine(" Already cached: " + (blobResponse.MatchedBlobCount - blobResponse.DownloadedBlobCount)); + Console.WriteLine(" Downloaded: " + blobResponse.DownloadedBlobCount); + if (this.HydrateFiles) + { + Console.WriteLine(" Hydrated files: " + blobResponse.HydratedFileCount); + } + + return true; + } + + this.ReportErrorAndExit(tracer, "Prefetching blobs failed (via mount): " + blobResponse.Error); + return true; + + case NamedPipeMessages.PrefetchBlobs.MountNotReadyResult: + tracer.RelatedInfo("TryPrefetchBlobsViaMountProcess: Mount not ready, falling back to direct prefetch"); + return false; + + default: + tracer.RelatedInfo("TryPrefetchBlobsViaMountProcess: Unexpected response '{0}', falling back to direct prefetch", response.Header); + return false; + } + } + } + private void PrefetchCommits(ITracer tracer, GVFSEnlistment enlistment, GitObjectsHttpRequestor objectRequestor, CacheServerInfo cacheServer) { bool success;