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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
|
||||||
public static string GetServerMutexName(string pipeName)
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
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);
|
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 Pipe { get; }
|
||||||
|
|
||||||
public CommandOption KeepAlive { get; }
|
public CommandOption KeepAlive { get; }
|
||||||
|
|
@ -61,8 +77,20 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
return Task.FromResult(1);
|
return Task.FromResult(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FileStream pidFileStream = null;
|
||||||
try
|
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;
|
TimeSpan? keepAlive = null;
|
||||||
if (KeepAlive.HasValue() && int.TryParse(KeepAlive.Value(), out var result))
|
if (KeepAlive.HasValue() && int.TryParse(KeepAlive.Value(), out var result))
|
||||||
{
|
{
|
||||||
|
|
@ -79,6 +107,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
{
|
{
|
||||||
serverMutex.ReleaseMutex();
|
serverMutex.ReleaseMutex();
|
||||||
serverMutex.Dispose();
|
serverMutex.Dispose();
|
||||||
|
pidFileStream?.Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
|
|
@ -89,5 +118,49 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
var dispatcher = RequestDispatcher.Create(host, compilerHost, cancellationToken, eventBus, keepAlive);
|
var dispatcher = RequestDispatcher.Create(host, compilerHost, cancellationToken, eventBus, keepAlive);
|
||||||
dispatcher.Run();
|
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;
|
||||||
using System.Runtime.InteropServices;
|
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;
|
||||||
using System.Collections.Generic;
|
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.
|
// Spend up to 20s connecting to a new process, to allow time for it to start.
|
||||||
private const int TimeOutMsNewProcess = 20000;
|
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)
|
public static bool WasServerMutexOpen(string mutexName)
|
||||||
{
|
{
|
||||||
Mutex mutex = null;
|
Mutex mutex = null;
|
||||||
|
|
@ -118,7 +122,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
string pipeName,
|
string pipeName,
|
||||||
string keepAlive,
|
string keepAlive,
|
||||||
int? timeoutOverride,
|
int? timeoutOverride,
|
||||||
Func<string, string, bool, bool> tryCreateServerFunc,
|
TryCreateServerCoreDelegate<string, string, int?, bool, bool> tryCreateServerFunc,
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
bool debug)
|
bool debug)
|
||||||
{
|
{
|
||||||
|
|
@ -181,7 +185,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
var wasServerRunning = WasServerMutexOpen(serverMutexName);
|
var wasServerRunning = WasServerMutexOpen(serverMutexName);
|
||||||
var timeout = wasServerRunning ? timeoutExistingProcess : timeoutNewProcess;
|
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);
|
pipeTask = Client.ConnectAsync(pipeName, TimeSpan.FromMilliseconds(timeout), cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
@ -277,10 +281,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal for testing.
|
// 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 expectedPath;
|
||||||
string processArguments;
|
string processArguments;
|
||||||
|
processId = null;
|
||||||
|
|
||||||
// The server should be in the same directory as the client
|
// The server should be in the same directory as the client
|
||||||
var expectedCompilerPath = Path.Combine(clientDir, ServerName);
|
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);
|
ServerLogger.Log("Successfully created process with process id {0}", processInfo.dwProcessId);
|
||||||
NativeMethods.CloseHandle(processInfo.hProcess);
|
NativeMethods.CloseHandle(processInfo.hProcess);
|
||||||
NativeMethods.CloseHandle(processInfo.hThread);
|
NativeMethods.CloseHandle(processInfo.hThread);
|
||||||
|
processId = processInfo.dwProcessId;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -351,7 +357,9 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
CreateNoWindow = true
|
CreateNoWindow = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Process.Start(startInfo);
|
var process = Process.Start(startInfo);
|
||||||
|
processId = process.Id;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
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;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
|
||||||
try
|
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();
|
var request = ServerRequest.CreateShutdown();
|
||||||
await request.WriteAsync(client.Stream, Cancelled).ConfigureAwait(false);
|
await request.WriteAsync(client.Stream, Cancelled).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -15,9 +17,12 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
||||||
{
|
{
|
||||||
public class BuildServerIntegrationTest : MSBuildIntegrationTestBase, IClassFixture<BuildServerTestFixture>
|
public class BuildServerIntegrationTest : MSBuildIntegrationTestBase, IClassFixture<BuildServerTestFixture>
|
||||||
{
|
{
|
||||||
|
private BuildServerTestFixture _buildServer;
|
||||||
|
|
||||||
public BuildServerIntegrationTest(BuildServerTestFixture buildServer)
|
public BuildServerIntegrationTest(BuildServerTestFixture buildServer)
|
||||||
: base(buildServer)
|
: base(buildServer)
|
||||||
{
|
{
|
||||||
|
_buildServer = buildServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,18 @@ namespace Microsoft.AspNetCore.Razor.Design.IntegrationTests
|
||||||
{
|
{
|
||||||
PipeName = Guid.NewGuid().ToString();
|
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}.");
|
throw new InvalidOperationException($"Failed to start the build server at pipe {PipeName}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ProcessId = processId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string PipeName { get; }
|
public string PipeName { get; }
|
||||||
|
|
||||||
|
public int? ProcessId { get; }
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
// Shutdown the build server.
|
// 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