Repository navigation
Report the real reason a mount fails instead of a broken pipe #2105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Tyrie Vella (tyrielv)
wants to merge
5
commits into
microsoft:master
Choose a base branch
from
tyrielv:tyrielv/fix-mount-failure-reporting
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
9ba27f1
Report the real reason a mount fails instead of a broken pipe
tyrielv d4ddf6b
Address self-review findings on the mount failure reporting fix
tyrielv 14cdfdf
Extract the mount-failure handshake into MountFailureReporter
tyrielv 974b651
Tighten the MountFailureReporter surface and its test coverage
tyrielv d84568b
Keep a failing mount from crashing, and make its tests deterministic
tyrielv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| using System; | ||
| using System.Threading; | ||
|
|
||
| namespace GVFS.Common.NamedPipes | ||
| { | ||
| /// <summary> | ||
| /// The process-level effects <see cref="MountFailureReporter"/> depends on: waiting, | ||
| /// sleeping, and ending the process. Injected so unit tests can observe what was | ||
| /// requested without real delays and without ending the test host. | ||
| /// </summary> | ||
| internal interface IMountFailureEnvironment | ||
| { | ||
| /// <returns>True if <paramref name="signal"/> was set before the timeout.</returns> | ||
| bool WaitForSignal(WaitHandle signal, int timeoutMs); | ||
|
|
||
| void Sleep(int durationMs); | ||
|
|
||
| void ExitProcess(int exitCode); | ||
|
|
||
| /// <summary> | ||
| /// Parks the calling thread until another thread ends the process. | ||
| /// </summary> | ||
| void BlockUntilProcessExits(); | ||
| } | ||
|
|
||
| internal sealed class ProcessMountFailureEnvironment : IMountFailureEnvironment | ||
| { | ||
| public bool WaitForSignal(WaitHandle signal, int timeoutMs) | ||
| { | ||
| return signal.WaitOne(timeoutMs); | ||
| } | ||
|
|
||
| public void Sleep(int durationMs) | ||
| { | ||
| Thread.Sleep(durationMs); | ||
| } | ||
|
|
||
| public void ExitProcess(int exitCode) | ||
| { | ||
| Environment.Exit(exitCode); | ||
| } | ||
|
|
||
| public void BlockUntilProcessExits() | ||
| { | ||
| Thread.Sleep(Timeout.Infinite); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| using GVFS.Common.Tracing; | ||
| using System; | ||
| using System.Threading; | ||
|
|
||
| namespace GVFS.Common.NamedPipes | ||
| { | ||
| /// <summary> | ||
| /// Server half of the mount-failure handshake. A failing mount process stays alive | ||
| /// until a client has read the reason over the named pipe, or until a short timeout | ||
| /// elapses, so the failure response reaches the client before the process exits. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Until the process exits it holds the mount lock and the named pipe, so the wait is | ||
| /// bounded and short. This type owns only the handshake state. The caller decides how | ||
| /// and when the process exits. The client half of the protocol is | ||
| /// <see cref="GVFSEnlistment.WaitUntilMounted(ITracer, string, string, bool, out string, Action{string})"/>. | ||
| /// </remarks> | ||
| public sealed class MountFailureReporter | ||
| { | ||
| /// <summary> | ||
| /// Cap on how long to wait once a client is known to be polling. | ||
| /// </summary> | ||
| internal const int DefaultReportTimeoutMs = 2000; | ||
|
|
||
| /// <summary> | ||
| /// Cap used when no client has received a status yet. It only has to cover the | ||
| /// gap between the pipe opening and the client's first poll. | ||
| /// </summary> | ||
| internal const int DefaultNoClientTimeoutMs = 500; | ||
|
|
||
| /// <summary> | ||
| /// Time to stay alive after a client reads the failure. Any client can satisfy the | ||
| /// wait, and it is not necessarily the mounting one: a concurrent "gvfs status" | ||
| /// polls the same pipe. The mounting client must get at least one more poll before | ||
| /// the process exits, so this is longer than the client's poll interval. | ||
| /// </summary> | ||
| internal const int DefaultDrainMs = GVFSEnlistment.MountStatusPollIntervalMs * 3 / 2; | ||
|
|
||
| private readonly ITracer tracer; | ||
| private readonly IMountFailureEnvironment environment; | ||
|
|
||
| // Held for the life of the process, which ends through Environment.Exit. | ||
| private readonly ManualResetEvent failureReported = new ManualResetEvent(false); | ||
|
|
||
| private volatile string failureMessage; | ||
| private volatile bool namedPipeReady; | ||
| private volatile bool clientReceivedStatus; | ||
|
|
||
| // 0 means no thread owns the failure path yet. | ||
| private int owningThreadId; | ||
|
|
||
| public MountFailureReporter(ITracer tracer) | ||
| : this(tracer, new ProcessMountFailureEnvironment()) | ||
| { | ||
| } | ||
|
|
||
| internal MountFailureReporter(ITracer tracer, IMountFailureEnvironment environment) | ||
| { | ||
| this.tracer = tracer; | ||
| this.environment = environment; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// The reason the mount failed, or null if the mount has not failed. Sent to the | ||
| /// client in the GetStatus response. | ||
| /// </summary> | ||
| public string FailureMessage | ||
| { | ||
| get { return this.failureMessage; } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Formats a failure message without letting the formatting itself throw. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Callers pass messages built from external text (exception strings, server | ||
| /// responses) that can contain unbalanced braces. A FormatException here would | ||
| /// mask the failure being reported, so fall back to the unformatted string. | ||
| /// </remarks> | ||
| public static string FormatFailure(string error, object[] args) | ||
| { | ||
| if (args == null || args.Length == 0) | ||
| { | ||
| return error; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| return string.Format(error, args); | ||
| } | ||
| catch (FormatException) | ||
| { | ||
| return error; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Records that the named pipe server is accepting requests. A failure before this | ||
| /// point cannot be reported over the pipe, so it does not wait for a client. | ||
| /// </summary> | ||
| public void OnNamedPipeReady() | ||
| { | ||
| this.namedPipeReady = true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Takes ownership of the mount-failure path. | ||
| /// </summary> | ||
| /// <returns> | ||
| /// True if the calling thread now owns reporting and must drive it to process exit. | ||
| /// False if it does not. Both false outcomes end in process exit, so the caller | ||
| /// does not continue in production: | ||
| /// <list type="bullet"> | ||
| /// <item>The calling thread already owns reporting, because something threw while it | ||
| /// reported and an outer catch block called back in. It exits now. Waiting again | ||
| /// would block on a signal that this same thread has to observe.</item> | ||
| /// <item>Another thread owns reporting. The calling thread blocks until that thread | ||
| /// ends the process, so it does not carry on with a half-initialized mount.</item> | ||
| /// </list> | ||
| /// </returns> | ||
| public bool TryBeginFailure(int exitCode, string message) | ||
| { | ||
| int currentThreadId = Environment.CurrentManagedThreadId; | ||
| int previousOwner = Interlocked.CompareExchange(ref this.owningThreadId, currentThreadId, 0); | ||
|
|
||
| if (previousOwner == currentThreadId) | ||
| { | ||
| this.tracer.RelatedWarning( | ||
| null, | ||
| $"{nameof(this.TryBeginFailure)}: Re-entered while reporting a mount failure: {message}", | ||
| Keywords.Telemetry); | ||
| this.environment.ExitProcess(exitCode); | ||
| return false; | ||
| } | ||
|
|
||
| if (previousOwner != 0) | ||
| { | ||
| this.environment.BlockUntilProcessExits(); | ||
| return false; | ||
| } | ||
|
|
||
| this.failureMessage = message; | ||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Sends a status response, and records the outcome only if the send succeeded. | ||
| /// </summary> | ||
| /// <param name="trySend">Sends the response. Returns true if it reached the client.</param> | ||
| /// <param name="carriesMountFailure">True if the response reports MountFailed.</param> | ||
| /// <remarks> | ||
| /// The send must come first. Recording a client that never received the response | ||
| /// would release the wait and let the process exit before any client read the | ||
| /// failure. | ||
| /// </remarks> | ||
| public bool TrySendStatus(Func<bool> trySend, bool carriesMountFailure) | ||
| { | ||
| if (!trySend()) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| this.clientReceivedStatus = true; | ||
|
|
||
| if (carriesMountFailure) | ||
| { | ||
| this.failureReported.Set(); | ||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Blocks until a client has read the failure, or until a short timeout elapses. | ||
| /// Returns immediately when no client could read it. | ||
| /// </summary> | ||
| /// <returns>True if a client read the failure.</returns> | ||
| public bool WaitForClientToReadFailure() | ||
| { | ||
| if (!this.namedPipeReady) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| int timeoutMs = this.clientReceivedStatus ? DefaultReportTimeoutMs : DefaultNoClientTimeoutMs; | ||
|
|
||
| if (this.environment.WaitForSignal(this.failureReported, timeoutMs)) | ||
| { | ||
| this.environment.Sleep(DefaultDrainMs); | ||
| return true; | ||
| } | ||
|
|
||
| this.tracer.RelatedWarning( | ||
| null, | ||
| $"{nameof(this.WaitForClientToReadFailure)}: No client read the mount failure within {timeoutMs}ms. Exiting anyway.", | ||
| Keywords.Telemetry); | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.