aspnetcore/src/Microsoft.AspNetCore.Razor..../ServerCommand.cs

189 lines
6.7 KiB
C#

// 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.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.AspNetCore.Razor.Tools
{
internal class ServerCommand : CommandBase
{
public ServerCommand(Application parent)
: base(parent, "server")
{
Pipe = Option("-p|--pipe", "name of named pipe", 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)
{
if (!string.IsNullOrEmpty(pipeName))
{
Pipe.Values.Add(pipeName);
}
if (keepAlive.HasValue)
{
KeepAlive.Values.Add(keepAlive.Value.ToString());
}
}
public CommandOption Pipe { get; }
public CommandOption KeepAlive { get; }
protected override bool ValidateArguments()
{
if (string.IsNullOrEmpty(Pipe.Value()))
{
Pipe.Values.Add(PipeName.ComputeDefault());
}
return true;
}
protected override Task<int> ExecuteCoreAsync()
{
// Make sure there's only one server with the same identity at a time.
var serverMutexName = MutexName.GetServerMutexName(Pipe.Value());
Mutex serverMutex = null;
var holdsMutex = false;
try
{
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);
}
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))
{
// 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);
}
finally
{
serverMutex.ReleaseMutex();
serverMutex.Dispose();
pidFileStream?.Close();
}
return Task.FromResult(0);
}
protected virtual void ExecuteServerCore(ConnectionHost host, CompilerHost compilerHost, CancellationToken cancellationToken, EventBus eventBus, TimeSpan? keepAlive)
{
var dispatcher = RequestDispatcher.Create(host, compilerHost, cancellationToken, eventBus, keepAlive);
dispatcher.Run();
}
protected virtual FileStream WritePidFile()
{
var path = GetPidFilePath(env => Environment.GetEnvironmentVariable(env));
return WritePidFile(path);
}
// Internal for testing.
internal virtual FileStream WritePidFile(string directoryPath)
{
if (string.IsNullOrEmpty(directoryPath))
{
// Invalid path. Bail.
return null;
}
// 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}";
// Make sure the directory exists.
Directory.CreateDirectory(directoryPath);
var path = Path.Combine(directoryPath, 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;
}
// Internal for testing.
internal virtual string GetPidFilePath(Func<string, string> getEnvironmentVariable)
{
var path = getEnvironmentVariable("DOTNET_BUILD_PIDFILE_DIRECTORY");
if (string.IsNullOrEmpty(path))
{
var homeEnvVariable = PlatformInformation.IsWindows ? "USERPROFILE" : "HOME";
var homePath = getEnvironmentVariable(homeEnvVariable);
if (string.IsNullOrEmpty(homePath))
{
// Couldn't locate the user profile directory. Bail.
return null;
}
path = Path.Combine(homePath, ".dotnet", "pids", "build");
}
return path;
}
}
}