Write process and pipe information to a pid file

This commit is contained in:
Ajay Bhargav Baaskaran 2018-04-16 17:33:44 -07:00
parent 2c6ae20e11
commit d2fe76be21
10 changed files with 182 additions and 11 deletions

View File

@ -1,4 +1,5 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;

View File

@ -12,7 +12,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
public static string GetServerMutexName(string pipeName)
{
return $"{pipeName}.server";
// We want to prefix this with Global\ because we want this mutex to be visible
// across terminal sessions which is useful for cases like shutdown.
// https://msdn.microsoft.com/en-us/library/system.threading.mutex(v=vs.110).aspx#Remarks
// This still wouldn't allow other users to access the server because the pipe will fail to connect.
return $"Global\\{pipeName}.server";
}
}
}

View File

@ -2,6 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.CommandLineUtils;
@ -17,6 +21,18 @@ namespace Microsoft.AspNetCore.Razor.Tools
KeepAlive = Option("-k|--keep-alive", "sets the default idle timeout for the server in seconds", CommandOptionType.SingleValue);
}
// For testing purposes only.
internal ServerCommand(Application parent, string pipeName, int? keepAlive = null)
: this(parent)
{
Pipe.Values.Add(pipeName);
if (keepAlive.HasValue)
{
KeepAlive.Values.Add(keepAlive.Value.ToString());
}
}
public CommandOption Pipe { get; }
public CommandOption KeepAlive { get; }
@ -61,8 +77,20 @@ namespace Microsoft.AspNetCore.Razor.Tools
return Task.FromResult(1);
}
FileStream pidFileStream = null;
try
{
try
{
// Write the process and pipe information to a file in a well-known location.
pidFileStream = WritePidFile();
}
catch (Exception ex)
{
// Something happened when trying to write to the pid file. Log and move on.
ServerLogger.LogException(ex, "Failed to create PID file.");
}
TimeSpan? keepAlive = null;
if (KeepAlive.HasValue() && int.TryParse(KeepAlive.Value(), out var result))
{
@ -79,6 +107,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
{
serverMutex.ReleaseMutex();
serverMutex.Dispose();
pidFileStream?.Close();
}
return Task.FromResult(0);
@ -89,5 +118,49 @@ namespace Microsoft.AspNetCore.Razor.Tools
var dispatcher = RequestDispatcher.Create(host, compilerHost, cancellationToken, eventBus, keepAlive);
dispatcher.Run();
}
internal FileStream WritePidFile()
{
// To make all the running rzc servers more discoverable, We want to write the process Id and pipe name to a file.
// The file contents will be in the following format,
//
// <PID>
// rzc
// path/to/rzc.dll
// <pipename>
const int DefaultBufferSize = 4096;
var processId = Process.GetCurrentProcess().Id;
var fileName = $"rzc-{processId}";
var path = Environment.GetEnvironmentVariable("DOTNET_BUILD_PIDFILE_DIRECTORY");
if (string.IsNullOrEmpty(path))
{
var homeEnvVariable = PlatformInformation.IsWindows ? "USERPROFILE" : "HOME";
var homePath = Environment.GetEnvironmentVariable(homeEnvVariable);
if (string.IsNullOrEmpty(homePath))
{
// Couldn't locate the user profile directory. Bail.
return null;
}
path = Path.Combine(homePath, ".dotnet", "pids", "build");
}
// Make sure the directory exists.
Directory.CreateDirectory(path);
path = Path.Combine(path, fileName);
var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, DefaultBufferSize, FileOptions.DeleteOnClose);
using (var writer = new StreamWriter(fileStream, Encoding.UTF8, DefaultBufferSize, leaveOpen: true))
{
var rzcPath = Assembly.GetExecutingAssembly().Location;
var content = $"{processId}{Environment.NewLine}rzc{Environment.NewLine}{rzcPath}{Environment.NewLine}{Pipe.Value()}";
writer.Write(content);
}
return fileStream;
}
}
}

View File

@ -1,4 +1,5 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Runtime.InteropServices;

View File

@ -1,4 +1,5 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
@ -21,6 +22,9 @@ namespace Microsoft.AspNetCore.Razor.Tools
// Spend up to 20s connecting to a new process, to allow time for it to start.
private const int TimeOutMsNewProcess = 20000;
// Custom delegate that contains an out param to use with TryCreateServerCore method.
private delegate TResult TryCreateServerCoreDelegate<T1, T2, T3, T4, out TResult>(T1 arg1, T2 arg2, out T3 arg3, T4 arg4);
public static bool WasServerMutexOpen(string mutexName)
{
Mutex mutex = null;
@ -118,7 +122,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
string pipeName,
string keepAlive,
int? timeoutOverride,
Func<string, string, bool, bool> tryCreateServerFunc,
TryCreateServerCoreDelegate<string, string, int?, bool, bool> tryCreateServerFunc,
CancellationToken cancellationToken,
bool debug)
{
@ -181,7 +185,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
var wasServerRunning = WasServerMutexOpen(serverMutexName);
var timeout = wasServerRunning ? timeoutExistingProcess : timeoutNewProcess;
if (wasServerRunning || tryCreateServerFunc(clientDir, pipeName, debug))
if (wasServerRunning || tryCreateServerFunc(clientDir, pipeName, out var _, debug))
{
pipeTask = Client.ConnectAsync(pipeName, TimeSpan.FromMilliseconds(timeout), cancellationToken);
}
@ -277,10 +281,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
}
// Internal for testing.
internal static bool TryCreateServerCore(string clientDir, string pipeName, bool debug = false)
internal static bool TryCreateServerCore(string clientDir, string pipeName, out int? processId, bool debug = false)
{
string expectedPath;
string processArguments;
processId = null;
// The server should be in the same directory as the client
var expectedCompilerPath = Path.Combine(clientDir, ServerName);
@ -328,6 +333,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
ServerLogger.Log("Successfully created process with process id {0}", processInfo.dwProcessId);
NativeMethods.CloseHandle(processInfo.hProcess);
NativeMethods.CloseHandle(processInfo.hThread);
processId = processInfo.dwProcessId;
}
else
{
@ -351,7 +357,9 @@ namespace Microsoft.AspNetCore.Razor.Tools
CreateNoWindow = true
};
Process.Start(startInfo);
var process = Process.Start(startInfo);
processId = process.Id;
return true;
}
catch

View File

@ -1,4 +1,5 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;

View File

@ -44,8 +44,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
try
{
using (var client = await Client.ConnectAsync(Pipe.Value(), timeout: null, cancellationToken: Cancelled))
using (var client = await Client.ConnectAsync(Pipe.Value(), timeout: TimeSpan.FromSeconds(5), cancellationToken: Cancelled))
{
if (client == null)
{
throw new InvalidOperationException("Couldn't connect to the server.");
}
var request = ServerRequest.CreateShutdown();
await request.WriteAsync(client.Stream, Cancelled).ConfigureAwait(false);

View File

@ -1,6 +1,8 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
@ -15,9 +17,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
{
public class BuildServerIntegrationTest : MSBuildIntegrationTestBase, IClassFixture<BuildServerTestFixture>
{
private BuildServerTestFixture _buildServer;
public BuildServerIntegrationTest(BuildServerTestFixture buildServer)
: base(buildServer)
{
_buildServer = buildServer;
}
[Fact]

View File

@ -18,14 +18,18 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
{
PipeName = Guid.NewGuid().ToString();
if (!ServerConnection.TryCreateServerCore(Environment.CurrentDirectory, PipeName))
if (!ServerConnection.TryCreateServerCore(Environment.CurrentDirectory, PipeName, out var processId))
{
throw new InvalidOperationException($"Failed to start the build server at pipe {PipeName}.");
}
ProcessId = processId;
}
public string PipeName { get; }
public int? ProcessId { get; }
public void Dispose()
{
// Shutdown the build server.

View File

@ -0,0 +1,69 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Microsoft.CodeAnalysis;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Razor.Tools
{
public class ServerCommandTest
{
[Fact]
public void WritePidFile_WorksAsExpected()
{
// Arrange
var expectedProcessId = Process.GetCurrentProcess().Id;
var expectedRzcPath = typeof(ServerCommand).Assembly.Location;
var expectedFileName = $"rzc-{expectedProcessId}";
var homeEnvVariable = PlatformInformation.IsWindows ? "USERPROFILE" : "HOME";
var path = Path.Combine(Environment.GetEnvironmentVariable(homeEnvVariable), ".dotnet", "pids", "build", expectedFileName);
var pipeName = Guid.NewGuid().ToString();
var server = GetServerCommand(pipeName);
// Act & Assert
try
{
using (var _ = server.WritePidFile())
{
Assert.True(File.Exists(path));
// Make sure another stream can be opened while the write stream is still open.
using (var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Write | FileShare.Delete))
using (var reader = new StreamReader(fileStream))
{
var lines = reader.ReadToEnd().Split(Environment.NewLine);
Assert.Equal(new[] { expectedProcessId.ToString(), "rzc", expectedRzcPath, pipeName }, lines);
}
}
// Make sure the file is deleted on dispose.
Assert.False(File.Exists(path));
}
finally
{
// Delete the file in case the test fails.
if (File.Exists(path))
{
File.Delete(path);
}
}
}
private ServerCommand GetServerCommand(string pipeName)
{
var application = new Application(
CancellationToken.None,
Mock.Of<ExtensionAssemblyLoader>(),
Mock.Of<ExtensionDependencyChecker>(),
(path, properties) => MetadataReference.CreateFromFile(path, properties));
return new ServerCommand(application, pipeName);
}
}
}