diff --git a/GVFS/GVFS.Common/ConsoleHelper.cs b/GVFS/GVFS.Common/ConsoleHelper.cs
index d40785334..dd1e0127f 100644
--- a/GVFS/GVFS.Common/ConsoleHelper.cs
+++ b/GVFS/GVFS.Common/ConsoleHelper.cs
@@ -21,6 +21,32 @@ public static bool ShowStatusWhileRunning(
bool showSpinner,
string gvfsLogEnlistmentRoot,
int initialDelayMs = 0)
+ {
+ return ShowStatusWhileRunning(
+ action,
+ getMessage: null,
+ message: message,
+ output,
+ showSpinner,
+ gvfsLogEnlistmentRoot,
+ initialDelayMs);
+ }
+
+ ///
+ /// Runs an action while displaying a dynamic status message with a spinner.
+ /// The delegate is called on each spinner tick
+ /// and may return a sub-status string (e.g. "Authenticating") that is appended
+ /// to in parentheses. When null or returning null,
+ /// only the base message is shown.
+ ///
+ public static bool ShowStatusWhileRunning(
+ Func action,
+ Func getMessage,
+ string message,
+ TextWriter output,
+ bool showSpinner,
+ string gvfsLogEnlistmentRoot,
+ int initialDelayMs = 0)
{
Func actionResultAction =
() =>
@@ -30,6 +56,7 @@ public static bool ShowStatusWhileRunning(
ActionResult result = ShowStatusWhileRunning(
actionResultAction,
+ getMessage,
message,
output,
showSpinner,
@@ -46,6 +73,18 @@ public static ActionResult ShowStatusWhileRunning(
bool showSpinner,
string gvfsLogEnlistmentRoot,
int initialDelayMs)
+ {
+ return ShowStatusWhileRunning(action, getMessage: null, message, output, showSpinner, gvfsLogEnlistmentRoot, initialDelayMs);
+ }
+
+ public static ActionResult ShowStatusWhileRunning(
+ Func action,
+ Func getMessage,
+ string message,
+ TextWriter output,
+ bool showSpinner,
+ string gvfsLogEnlistmentRoot,
+ int initialDelayMs)
{
ActionResult result = ActionResult.Failure;
bool initialMessageWritten = false;
@@ -67,6 +106,7 @@ public static ActionResult ShowStatusWhileRunning(
{
int retries = 0;
char[] waiting = { '\u2014', '\\', '|', '/' };
+ string lastProgress = null;
while (!isComplete)
{
@@ -76,7 +116,23 @@ public static ActionResult ShowStatusWhileRunning(
}
else
{
- output.Write("\r{0}...{1}", message, waiting[(retries / 2) % waiting.Length]);
+ string progress = getMessage?.Invoke();
+ string displayMessage = !string.IsNullOrEmpty(progress)
+ ? $"{message} ({progress})"
+ : message;
+
+ // Clear previous line content when message shrinks
+ string line = $"\r{displayMessage}...{waiting[(retries / 2) % waiting.Length]}";
+ if (lastProgress != null && lastProgress.Length > line.Length)
+ {
+ output.Write(line + new string(' ', lastProgress.Length - line.Length));
+ }
+ else
+ {
+ output.Write(line);
+ }
+
+ lastProgress = line;
initialMessageWritten = true;
actionIsDone.WaitOne(100);
}
@@ -86,8 +142,16 @@ public static ActionResult ShowStatusWhileRunning(
if (initialMessageWritten)
{
- // Clear out any trailing waiting character
- output.Write("\r{0}...", message);
+ // Clear out any trailing waiting character and sub-status
+ string finalLine = $"\r{message}...";
+ if (lastProgress != null && lastProgress.Length > finalLine.Length)
+ {
+ output.Write(finalLine + new string(' ', lastProgress.Length - finalLine.Length) + $"\r{message}...");
+ }
+ else
+ {
+ output.Write(finalLine);
+ }
}
});
spinnerThread.Start();
diff --git a/GVFS/GVFS.Common/GVFSEnlistment.cs b/GVFS/GVFS.Common/GVFSEnlistment.cs
index 286b32037..9f395f6b6 100644
--- a/GVFS/GVFS.Common/GVFSEnlistment.cs
+++ b/GVFS/GVFS.Common/GVFSEnlistment.cs
@@ -212,13 +212,13 @@ public static string GetNewGVFSLogFileName(
fileSystem: fileSystem);
}
- public static bool WaitUntilMounted(ITracer tracer, string enlistmentRoot, bool unattended, out string errorMessage)
+ public static bool WaitUntilMounted(ITracer tracer, string enlistmentRoot, bool unattended, out string errorMessage, Action onProgress = null)
{
string pipeName = GVFSPlatform.Instance.GetNamedPipeName(enlistmentRoot);
- return WaitUntilMounted(tracer, pipeName, enlistmentRoot, unattended, out errorMessage);
+ return WaitUntilMounted(tracer, pipeName, enlistmentRoot, unattended, out errorMessage, onProgress);
}
- public static bool WaitUntilMounted(ITracer tracer, string pipeName, string enlistmentRoot, bool unattended, out string errorMessage)
+ public static bool WaitUntilMounted(ITracer tracer, string pipeName, string enlistmentRoot, bool unattended, out string errorMessage, Action onProgress = null)
{
tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Creating NamedPipeClient for pipe '{pipeName}'");
@@ -260,6 +260,11 @@ public static bool WaitUntilMounted(ITracer tracer, string pipeName, string enli
}
else
{
+ if (onProgress != null && !string.IsNullOrEmpty(getStatusResponse.MountProgress))
+ {
+ onProgress(getStatusResponse.MountProgress);
+ }
+
tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Waiting 500ms for mount process to be ready");
Thread.Sleep(100);
}
diff --git a/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs b/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs
index d42c84873..7b2159605 100644
--- a/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs
+++ b/GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs
@@ -35,6 +35,7 @@ public static class GetStatus
public class Response
{
public string MountStatus { get; set; }
+ public string MountProgress { get; set; }
public string EnlistmentRoot { get; set; }
public string LocalCacheRoot { get; set; }
public string RepoUrl { get; set; }
diff --git a/GVFS/GVFS.Mount/InProcessMount.cs b/GVFS/GVFS.Mount/InProcessMount.cs
index 1272eb876..68c3f32bc 100644
--- a/GVFS/GVFS.Mount/InProcessMount.cs
+++ b/GVFS/GVFS.Mount/InProcessMount.cs
@@ -55,7 +55,8 @@ public class InProcessMount
private GVFSContext context;
private GVFSGitObjects gitObjects;
- private MountState currentState;
+ private volatile MountState currentState;
+ private volatile string mountProgressMessage;
private HeartbeatThread heartbeat;
private ManualResetEvent unmountEvent;
@@ -194,65 +195,71 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords)
this.enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot);
- // Local validations and git config run while we wait for the network
- var localTask = Task.Run(() =>
+ // Start the pipe server early so MountVerb can connect and poll progress
+ // during the parallel validation phase. Only GetStatus requests are
+ // handled while currentState == Mounting (see HandleRequest guard).
+ this.mountProgressMessage = "Authenticating and validating";
+ using (NamedPipeServer pipeServer = this.StartNamedPipe())
{
- Stopwatch sw = Stopwatch.StartNew();
+ this.tracer.RelatedEvent(
+ EventLevel.Informational,
+ $"{nameof(this.Mount)}_StartedNamedPipe",
+ new EventMetadata { { "NamedPipeName", this.enlistment.NamedPipeName } });
- this.ValidateGitVersion();
- this.tracer.RelatedInfo("ParallelMount: ValidateGitVersion completed in {0}ms", sw.ElapsedMilliseconds);
+ // Local validations and git config run while we wait for the network
+ Task localTask = Task.Run(() =>
+ {
+ Stopwatch sw = Stopwatch.StartNew();
- this.ValidateHooksVersion();
- this.ValidateFileSystemSupportsRequiredFeatures();
+ this.ValidateGitVersion();
+ this.tracer.RelatedInfo("ParallelMount: ValidateGitVersion completed in {0}ms", sw.ElapsedMilliseconds);
- GitProcess git = new GitProcess(this.enlistment);
- if (!git.IsValidRepo())
- {
- this.FailMountAndExit("The .git folder is missing or has invalid contents");
- }
+ this.ValidateHooksVersion();
+ this.ValidateFileSystemSupportsRequiredFeatures();
- if (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.WorkingDirectoryRoot, 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 (!GVFSPlatform.Instance.FileSystem.IsFileSystemSupported(this.enlistment.WorkingDirectoryRoot, out string fsError))
+ {
+ this.FailMountAndExit("FileSystem unsupported: " + fsError);
+ }
- if (!this.TrySetRequiredGitConfigSettings())
- {
- this.FailMountAndExit("Unable to configure git repo");
- }
+ this.tracer.RelatedInfo("ParallelMount: Local validations completed in {0}ms", sw.ElapsedMilliseconds);
- this.LogEnlistmentInfoAndSetConfigValues();
- this.tracer.RelatedInfo("ParallelMount: Local validations + git config completed in {0}ms", sw.ElapsedMilliseconds);
- });
+ if (!this.TrySetRequiredGitConfigSettings())
+ {
+ this.FailMountAndExit("Unable to configure git repo");
+ }
- try
- {
- Task.WaitAll(networkTask, localTask);
- }
- catch (AggregateException ae)
- {
- this.FailMountAndExit(ae.Flatten().InnerExceptions[0].Message);
- }
+ this.LogEnlistmentInfoAndSetConfigValues();
+ this.tracer.RelatedInfo("ParallelMount: Local validations + git config completed in {0}ms", sw.ElapsedMilliseconds);
+ });
- parallelTimer.Stop();
- this.tracer.RelatedInfo("ParallelMount: All parallel tasks completed in {0}ms", parallelTimer.ElapsedMilliseconds);
+ try
+ {
+ Task.WaitAll(networkTask, localTask);
+ }
+ catch (AggregateException ae)
+ {
+ this.FailMountAndExit(ae.Flatten().InnerExceptions[0].Message);
+ }
- ServerGVFSConfig serverGVFSConfig = networkTask.Result;
+ parallelTimer.Stop();
+ this.tracer.RelatedInfo("ParallelMount: All parallel tasks completed in {0}ms", parallelTimer.ElapsedMilliseconds);
- CacheServerResolver cacheServerResolver = new CacheServerResolver(this.tracer, this.enlistment);
- this.cacheServer = cacheServerResolver.ResolveNameFromRemote(this.cacheServer.Url, serverGVFSConfig);
+ ServerGVFSConfig serverGVFSConfig = networkTask.Result;
- this.EnsureLocalCacheIsHealthy(serverGVFSConfig);
+ this.mountProgressMessage = "Resolving cache server";
+ CacheServerResolver cacheServerResolver = new CacheServerResolver(this.tracer, this.enlistment);
+ this.cacheServer = cacheServerResolver.ResolveNameFromRemote(this.cacheServer.Url, serverGVFSConfig);
- using (NamedPipeServer pipeServer = this.StartNamedPipe())
- {
- this.tracer.RelatedEvent(
- EventLevel.Informational,
- $"{nameof(this.Mount)}_StartedNamedPipe",
- new EventMetadata { { "NamedPipeName", this.enlistment.NamedPipeName } });
+ this.EnsureLocalCacheIsHealthy(serverGVFSConfig);
+ this.mountProgressMessage = "Preparing mount";
this.context = this.CreateContext();
if (this.context.Unattended)
@@ -273,6 +280,7 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords)
GVFSPlatform.Instance.ConfigureVisualStudio(this.enlistment.GitBinPath, this.tracer);
+ this.mountProgressMessage = "Starting virtualization";
this.MountAndStartWorkingDirectoryCallbacks(this.cacheServer);
try
@@ -295,6 +303,7 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords)
},
Keywords.Telemetry);
+ this.mountProgressMessage = null;
this.currentState = MountState.Ready;
this.unmountEvent.WaitOne();
@@ -474,6 +483,16 @@ private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Conne
{
NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request);
+ // While mounting, only GetStatus requests are safe — other handlers depend
+ // on context, fileSystemCallbacks, etc. that aren't initialized yet.
+ if (message.Header != NamedPipeMessages.GetStatus.Request &&
+ this.currentState != MountState.Ready &&
+ this.currentState != MountState.Unmounting)
+ {
+ connection.TrySendResponse(NamedPipeMessages.MountNotReadyResult);
+ return;
+ }
+
switch (message.Header)
{
case NamedPipeMessages.GetStatus.Request:
@@ -999,14 +1018,15 @@ private void HandleGetStatusRequest(NamedPipeServer.Connection connection)
response.EnlistmentRoot = this.enlistment.WorkingDirectoryRoot;
response.LocalCacheRoot = !string.IsNullOrWhiteSpace(this.enlistment.LocalCacheRoot) ? this.enlistment.LocalCacheRoot : this.enlistment.GitObjectsRoot;
response.RepoUrl = this.enlistment.RepoUrl;
- response.CacheServer = this.cacheServer.ToString();
- response.LockStatus = this.context?.Repository.GVFSLock != null ? this.context.Repository.GVFSLock.GetStatus() : "Unavailable";
+ response.CacheServer = this.cacheServer?.ToString() ?? string.Empty;
+ response.LockStatus = this.context?.Repository?.GVFSLock != null ? this.context.Repository.GVFSLock.GetStatus() : "Unavailable";
response.DiskLayoutVersion = $"{GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion}.{GVFSPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion}";
switch (this.currentState)
{
case MountState.Mounting:
response.MountStatus = NamedPipeMessages.GetStatus.Mounting;
+ response.MountProgress = this.mountProgressMessage;
break;
case MountState.Ready:
diff --git a/GVFS/GVFS/CommandLine/GVFSVerb.cs b/GVFS/GVFS/CommandLine/GVFSVerb.cs
index 069ac1661..427302321 100644
--- a/GVFS/GVFS/CommandLine/GVFSVerb.cs
+++ b/GVFS/GVFS/CommandLine/GVFSVerb.cs
@@ -198,6 +198,22 @@ protected bool ShowStatusWhileRunning(
initialDelayMs: 0);
}
+ protected bool ShowStatusWhileRunning(
+ Func action,
+ Func getMessage,
+ string message,
+ string gvfsLogEnlistmentRoot)
+ {
+ return ConsoleHelper.ShowStatusWhileRunning(
+ action,
+ getMessage,
+ message,
+ this.Output,
+ showSpinner: !this.Unattended && this.Output == Console.Out && !GVFSPlatform.Instance.IsConsoleOutputRedirectedToFile(),
+ gvfsLogEnlistmentRoot: gvfsLogEnlistmentRoot,
+ initialDelayMs: 0);
+ }
+
protected bool ShowStatusWhileRunning(
Func action,
string message,
diff --git a/GVFS/GVFS/CommandLine/MountVerb.cs b/GVFS/GVFS/CommandLine/MountVerb.cs
index 37e5f1041..53077bba4 100644
--- a/GVFS/GVFS/CommandLine/MountVerb.cs
+++ b/GVFS/GVFS/CommandLine/MountVerb.cs
@@ -14,6 +14,7 @@ public class MountVerb : GVFSVerb.ForExistingEnlistment
{
private const string MountVerbName = "mount";
private Process mountProcess;
+ private volatile string currentMountProgress;
public string Verbosity { get; set; }
@@ -197,7 +198,9 @@ protected override void Execute(GVFSEnlistment enlistment)
if (!this.ShowStatusWhileRunning(
() => { return this.TryMount(tracer, enlistment, mountExecutableLocation, out errorMessage); },
- "Mounting"))
+ getMessage: () => this.currentMountProgress,
+ "Mounting",
+ enlistment.WorkingDirectoryRoot))
{
ReturnCode mountExitCode = ReturnCode.GenericError;
if (this.mountProcess != null)
@@ -277,7 +280,13 @@ private bool TryMount(ITracer tracer, GVFSEnlistment enlistment, string mountExe
tracer.RelatedInfo($"{nameof(this.TryMount)}: Waiting for repo to be mounted");
- return GVFSEnlistment.WaitUntilMounted(tracer, enlistment.NamedPipeName, enlistment.WorkingDirectoryRoot, this.Unattended, out errorMessage);
+ return GVFSEnlistment.WaitUntilMounted(
+ tracer,
+ enlistment.NamedPipeName,
+ enlistment.WorkingDirectoryRoot,
+ this.Unattended,
+ out errorMessage,
+ onProgress: progress => this.currentMountProgress = progress);
}
private bool RegisterMount(GVFSEnlistment enlistment, out string errorMessage)