Write process and pipe information to a pid file
This commit is contained in:
parent
2c6ae20e11
commit
d2fe76be21
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue