File: ServerCommand.cs
Web Access
Project: ..\..\..\src\RazorSdk\Tool\Microsoft.NET.Sdk.Razor.Tool.csproj (rzc)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#nullable disable
 
using System.Globalization;
using System.Reflection;
using Microsoft.DotNet.Configurer;
using Microsoft.NET.Sdk.Razor.Tool.CommandLineUtils;
 
namespace Microsoft.NET.Sdk.Razor.Tool
{
    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(CultureInfo.InvariantCulture));
            }
        }
 
        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();
            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 = Environment.ProcessId;
            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 static string GetPidFilePath()
        {
            var path = Environment.GetEnvironmentVariable("DOTNET_BUILD_PIDFILE_DIRECTORY");
            if (string.IsNullOrEmpty(path))
            {
                var homePath = CliFolderPathCalculatorCore.GetDotnetHomePath();
                if (homePath is null)
                {
                    // Couldn't locate the user profile directory. Bail.
                    return null;
                }
 
                path = Path.Combine(homePath, ".dotnet", "pids", "build");
            }
 
            return path;
        }
    }
}