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() protected override Task<int> ExecuteCoreAsync()
{ {
// Make sure there's only one server with the same identity at a time. // 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. // Keep alive times are specified in seconds
Error.Write("Another server already running..."); keepAlive = TimeSpan.FromSeconds(result);
return Task.FromResult(1);
} }
try var host = ConnectionHost.Create(Pipe.Value());
{
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 compilerHost = CompilerHost.Create();
ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive);
var compilerHost = CompilerHost.Create(); }
ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive); finally
} {
finally serverMutex.ReleaseMutex();
{ serverMutex.Dispose();
mutex.ReleaseMutex();
}
} }
return Task.FromResult(0); return Task.FromResult(0);

View File

@ -23,13 +23,21 @@ namespace Microsoft.AspNetCore.Razor.Tools
public static bool WasServerMutexOpen(string mutexName) public static bool WasServerMutexOpen(string mutexName)
{ {
var open = Mutex.TryOpenExisting(mutexName, out var mutex); Mutex mutex = null;
if (open) var open = false;
try
{ {
mutex.Dispose(); open = Mutex.TryOpenExisting(mutexName, out mutex);
return true;
} }
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> /// <summary>
@ -129,44 +137,63 @@ namespace Microsoft.AspNetCore.Razor.Tools
var timeoutExistingProcess = timeoutOverride ?? TimeOutMsExistingProcess; var timeoutExistingProcess = timeoutOverride ?? TimeOutMsExistingProcess;
var clientMutexName = MutexName.GetClientMutexName(pipeName); var clientMutexName = MutexName.GetClientMutexName(pipeName);
Task<Client> pipeTask = null; 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 try
{ {
if (!holdsMutex) clientMutex = new Mutex(initiallyOwned: true, name: clientMutexName, createdNew: out 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);
}
} }
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) if (pipeTask != null)

View File

@ -103,5 +103,35 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll"); Assert.FileExists(result, IntermediateOutputPath, "SimpleMvc.dll");
Assert.FileDoesNotExist(result, IntermediateOutputPath, "SimpleMvc.Views.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 suppressRestore = false,
bool suppressTimeout = false, bool suppressTimeout = false,
bool suppressBuildServer = false, bool suppressBuildServer = false,
string buildServerPipeName = null,
MSBuildProcessKind msBuildProcessKind = MSBuildProcessKind.Dotnet) MSBuildProcessKind msBuildProcessKind = MSBuildProcessKind.Dotnet)
{ {
var timeout = suppressTimeout ? (TimeSpan?)Timeout.InfiniteTimeSpan : null; var timeout = suppressTimeout ? (TimeSpan?)Timeout.InfiniteTimeSpan : null;
@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
if (!suppressBuildServer) if (!suppressBuildServer)
{ {
buildArgumentList.Add($"/p:_RazorBuildServerPipeName={BuildServer.PipeName}"); buildArgumentList.Add($"/p:_RazorBuildServerPipeName={buildServerPipeName ?? BuildServer.PipeName}");
} }
buildArgumentList.Add($"/t:{target} /p:Configuration={Configuration} {args}"); buildArgumentList.Add($"/t:{target} /p:Configuration={Configuration} {args}");