Handle Mutex creation exceptions

This commit is contained in:
Ajay Bhargav Baaskaran 2018-03-27 17:25:56 -07:00
parent bc72470c21
commit 03938dfd95
4 changed files with 134 additions and 62 deletions

View File

@ -34,37 +34,51 @@ namespace Microsoft.AspNetCore.Razor.Tools
protected override Task<int> ExecuteCoreAsync()
{
// Make sure there's only one server with the same identity at a time.
using (var mutex = new Mutex(initiallyOwned: true, name: MutexName.GetServerMutexName(Pipe.Value()), createdNew: out var holdsMutex))
var serverMutexName = MutexName.GetServerMutexName(Pipe.Value());
Mutex serverMutex = null;
var holdsMutex = false;
try
{
if (!holdsMutex)
serverMutex = new Mutex(initiallyOwned: true, name: serverMutexName, createdNew: out holdsMutex);
}
catch (Exception ex)
{
// The Mutex constructor can throw in certain cases. One specific example is docker containers
// where the /tmp directory is restricted. In those cases there is no reliable way to execute
// the server and we need to fall back to the command line.
// Example: https://github.com/dotnet/roslyn/issues/24124
Error.Write($"Server mutex creation failed. {ex.Message}");
return Task.FromResult(-1);
}
if (!holdsMutex)
{
// Another server is running, just exit.
Error.Write("Another server already running...");
return Task.FromResult(1);
}
try
{
TimeSpan? keepAlive = null;
if (KeepAlive.HasValue() && int.TryParse(KeepAlive.Value(), out var result))
{
// Another server is running, just exit.
Error.Write("Another server already running...");
return Task.FromResult(1);
// Keep alive times are specified in seconds
keepAlive = TimeSpan.FromSeconds(result);
}
try
{
TimeSpan? keepAlive = null;
if (KeepAlive.HasValue())
{
var value = KeepAlive.Value();
if (int.TryParse(value, out var result))
{
// Keep alive times are specified in seconds
keepAlive = TimeSpan.FromSeconds(result);
}
}
var host = ConnectionHost.Create(Pipe.Value());
var host = ConnectionHost.Create(Pipe.Value());
var compilerHost = CompilerHost.Create();
ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive);
}
finally
{
mutex.ReleaseMutex();
}
var compilerHost = CompilerHost.Create();
ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive);
}
finally
{
serverMutex.ReleaseMutex();
serverMutex.Dispose();
}
return Task.FromResult(0);

View File

@ -23,13 +23,21 @@ namespace Microsoft.AspNetCore.Razor.Tools
public static bool WasServerMutexOpen(string mutexName)
{
var open = Mutex.TryOpenExisting(mutexName, out var mutex);
if (open)
Mutex mutex = null;
var open = false;
try
{
mutex.Dispose();
return true;
open = Mutex.TryOpenExisting(mutexName, out mutex);
}
return false;
catch
{
// In the case an exception occured trying to open the Mutex then
// the assumption is that it's not open.
}
mutex?.Dispose();
return open;
}
/// <summary>
@ -129,44 +137,63 @@ namespace Microsoft.AspNetCore.Razor.Tools
var timeoutExistingProcess = timeoutOverride ?? TimeOutMsExistingProcess;
var clientMutexName = MutexName.GetClientMutexName(pipeName);
Task<Client> pipeTask = null;
using (var clientMutex = new Mutex(initiallyOwned: true, name: clientMutexName, createdNew: out var holdsMutex))
Mutex clientMutex = null;
var holdsMutex = false;
try
{
try
{
if (!holdsMutex)
{
try
{
holdsMutex = clientMutex.WaitOne(timeoutNewProcess);
if (!holdsMutex)
{
return new RejectedServerResponse();
}
}
catch (AbandonedMutexException)
{
holdsMutex = true;
}
}
// Check for an already running server
var serverMutexName = MutexName.GetServerMutexName(pipeName);
var wasServerRunning = WasServerMutexOpen(serverMutexName);
var timeout = wasServerRunning ? timeoutExistingProcess : timeoutNewProcess;
if (wasServerRunning || tryCreateServerFunc(clientDir, pipeName, debug))
{
pipeTask = Client.ConnectAsync(pipeName, TimeSpan.FromMilliseconds(timeout), cancellationToken);
}
clientMutex = new Mutex(initiallyOwned: true, name: clientMutexName, createdNew: out holdsMutex);
}
finally
catch (Exception ex)
{
if (holdsMutex)
// The Mutex constructor can throw in certain cases. One specific example is docker containers
// where the /tmp directory is restricted. In those cases there is no reliable way to execute
// the server and we need to fall back to the command line.
// Example: https://github.com/dotnet/roslyn/issues/24124
ServerLogger.LogException(ex, "Client mutex creation failed.");
return new RejectedServerResponse();
}
if (!holdsMutex)
{
try
{
clientMutex.ReleaseMutex();
holdsMutex = clientMutex.WaitOne(timeoutNewProcess);
if (!holdsMutex)
{
return new RejectedServerResponse();
}
}
catch (AbandonedMutexException)
{
holdsMutex = true;
}
}
// Check for an already running server
var serverMutexName = MutexName.GetServerMutexName(pipeName);
var wasServerRunning = WasServerMutexOpen(serverMutexName);
var timeout = wasServerRunning ? timeoutExistingProcess : timeoutNewProcess;
if (wasServerRunning || tryCreateServerFunc(clientDir, pipeName, debug))
{
pipeTask = Client.ConnectAsync(pipeName, TimeSpan.FromMilliseconds(timeout), cancellationToken);
}
}
finally
{
if (holdsMutex)
{
clientMutex?.ReleaseMutex();
}
clientMutex?.Dispose();
}
if (pipeTask != null)

View File

@ -103,5 +103,35 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll");
Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.dll");
}
[Fact]
[InitializeTestProject("SimpleMvc")]
public async Task Build_ServerConnectionMutexCreationFails_FallsBackToInProcessRzc()
{
// Use a pipe name longer that 260 characters to make the Mutex constructor throw.
var pipeName = new string('P', 261);
var result = await DotnetMSBuild(
"Build",
"/p:_RazorForceBuildServer=true",
buildServerPipeName: pipeName);
// We expect this to fail because we don't allow it to fallback to in process execution.
Assert.BuildFailed(result);
Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.dll");
// Try to build again without forcing it to run on build server.
result = await DotnetMSBuild(
"Build",
"/p:_RazorForceBuildServer=false",
buildServerPipeName: pipeName);
Assert.BuildPassed(result);
Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll");
Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.Views.dll");
// Note: We don't need to handle server clean up here because it will fail before
// it reaches server creation part.
}
}
}

View File

@ -58,6 +58,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
bool suppressRestore = false,
bool suppressTimeout = false,
bool suppressBuildServer = false,
string buildServerPipeName = null,
MSBuildProcessKind msBuildProcessKind = MSBuildProcessKind.Dotnet)
{
var timeout = suppressTimeout ? (TimeSpan?)Timeout.InfiniteTimeSpan : null;
@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
if (!suppressBuildServer)
{
buildArgumentList.Add($"/p:_RazorBuildServerPipeName={BuildServer.PipeName}");
buildArgumentList.Add($"/p:_RazorBuildServerPipeName={buildServerPipeName ?? BuildServer.PipeName}");
}
buildArgumentList.Add($"/t:{target} /p:Configuration={Configuration} {args}");