Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 67 additions & 3 deletions GVFS/GVFS.Common/ConsoleHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/// <summary>
/// Runs an action while displaying a dynamic status message with a spinner.
/// The <paramref name="getMessage"/> delegate is called on each spinner tick
/// and may return a sub-status string (e.g. "Authenticating") that is appended
/// to <paramref name="message"/> in parentheses. When null or returning null,
/// only the base message is shown.
/// </summary>
public static bool ShowStatusWhileRunning(
Func<bool> action,
Func<string> getMessage,
string message,
TextWriter output,
bool showSpinner,
string gvfsLogEnlistmentRoot,
int initialDelayMs = 0)
{
Func<ActionResult> actionResultAction =
() =>
Expand All @@ -30,6 +56,7 @@ public static bool ShowStatusWhileRunning(

ActionResult result = ShowStatusWhileRunning(
actionResultAction,
getMessage,
message,
output,
showSpinner,
Expand All @@ -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<ActionResult> action,
Func<string> getMessage,
string message,
TextWriter output,
bool showSpinner,
string gvfsLogEnlistmentRoot,
int initialDelayMs)
{
ActionResult result = ActionResult.Failure;
bool initialMessageWritten = false;
Expand All @@ -67,6 +106,7 @@ public static ActionResult ShowStatusWhileRunning(
{
int retries = 0;
char[] waiting = { '\u2014', '\\', '|', '/' };
string lastProgress = null;

while (!isComplete)
{
Expand All @@ -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);
}
Expand All @@ -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();
Expand Down
11 changes: 8 additions & 3 deletions GVFS/GVFS.Common/GVFSEnlistment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> 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<string> onProgress = null)
{
tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Creating NamedPipeClient for pipe '{pipeName}'");

Expand Down Expand Up @@ -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);
}
Expand Down
1 change: 1 addition & 0 deletions GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
114 changes: 67 additions & 47 deletions GVFS/GVFS.Mount/InProcessMount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -295,6 +303,7 @@ private void MountWithLockAcquired(EventLevel verbosity, Keywords keywords)
},
Keywords.Telemetry);

this.mountProgressMessage = null;
this.currentState = MountState.Ready;

this.unmountEvent.WaitOne();
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
16 changes: 16 additions & 0 deletions GVFS/GVFS/CommandLine/GVFSVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,22 @@ protected bool ShowStatusWhileRunning(
initialDelayMs: 0);
}

protected bool ShowStatusWhileRunning(
Func<bool> action,
Func<string> 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<bool> action,
string message,
Expand Down
13 changes: 11 additions & 2 deletions GVFS/GVFS/CommandLine/MountVerb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading