File: BackEnd\Node\OutOfProcServerNode.cs
Web Access
Project: ..\..\..\src\Build\Microsoft.Build.csproj (Microsoft.Build)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.BackEnd;
using Microsoft.Build.BackEnd.Logging;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Microsoft.Build.Framework.Telemetry;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Experimental
{
    /// <summary>
    /// This class represents an implementation of INode for out-of-proc server nodes aka MSBuild server
    /// </summary>
    public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacketHandler
    {
        /// <summary>
        /// A callback used to execute command line build.
        /// </summary>
        public delegate (int exitCode, string exitType) BuildCallback(
#if FEATURE_GET_COMMANDLINE
            string commandLine);
#else
            string[] commandLine);
#endif
 
        private readonly BuildCallback _buildFunction;
 
        /// <summary>
        /// The endpoint used to talk to the host.
        /// </summary>
        private INodeEndpoint _nodeEndpoint = default!;
 
        /// <summary>
        /// The packet factory.
        /// </summary>
        private readonly NodePacketFactory _packetFactory;
 
        /// <summary>
        /// The queue of packets we have received but which have not yet been processed.
        /// </summary>
        private readonly ConcurrentQueue<INodePacket> _receivedPackets;
 
        /// <summary>
        /// The event which is set when we receive packets.
        /// </summary>
        private readonly AutoResetEvent _packetReceivedEvent;
 
        /// <summary>
        /// The event which is set when we should shut down.
        /// </summary>
        private readonly ManualResetEvent _shutdownEvent;
 
        /// <summary>
        /// The reason we are shutting down.
        /// </summary>
        private NodeEngineShutdownReason _shutdownReason;
 
        /// <summary>
        /// The exception, if any, which caused shutdown.
        /// </summary>
        private Exception? _shutdownException = null;
 
        /// <summary>
        /// Indicate that cancel has been requested and initiated.
        /// </summary>
        private bool _cancelRequested = false;
        private string _serverBusyMutexName = default!;
 
        public OutOfProcServerNode(BuildCallback buildFunction)
        {
            _buildFunction = buildFunction;
 
            _receivedPackets = new ConcurrentQueue<INodePacket>();
            _packetReceivedEvent = new AutoResetEvent(false);
            _shutdownEvent = new ManualResetEvent(false);
            _packetFactory = new NodePacketFactory();
 
            (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.ServerNodeBuildCommand, ServerNodeBuildCommand.FactoryForDeserialization, this);
            (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this);
            (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.ServerNodeBuildCancel, ServerNodeBuildCancel.FactoryForDeserialization, this);
        }
 
        #region INode Members
 
        /// <summary>
        /// Starts up the server node and processes all build requests until the server is requested to shut down.
        /// </summary>
        /// <param name="shutdownException">The exception which caused shutdown, if any.</param>
        /// <returns>The reason for shutting down.</returns>
        public NodeEngineShutdownReason Run(out Exception? shutdownException)
        {
            ServerNodeHandshake handshake = new(
                CommunicationsUtilities.GetHandshakeOptions(taskHost: false, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture()));
 
            _serverBusyMutexName = GetBusyServerMutexName(handshake);
 
            // Handled race condition. If two processes spawn to start build Server one will die while
            // one Server client connects to the other one and run build on it.
            CommunicationsUtilities.Trace("Starting new server node with handshake {0}", handshake);
            using var serverRunningMutex = ServerNamedMutex.OpenOrCreateMutex(GetRunningServerMutexName(handshake), out bool mutexCreatedNew);
            if (!mutexCreatedNew)
            {
                shutdownException = new InvalidOperationException("MSBuild server is already running!");
                return NodeEngineShutdownReason.Error;
            }
 
            while (true)
            {
                NodeEngineShutdownReason shutdownReason = RunInternal(out shutdownException, handshake);
                if (shutdownReason != NodeEngineShutdownReason.BuildCompleteReuse)
                {
                    return shutdownReason;
                }
 
                // We need to clear cache for two reasons:
                // - cache file names can collide cross build requests, which would cause stale caching
                // - we might need to avoid cache builds-up in files system during lifetime of server
                FileUtilities.ClearCacheDirectory();
                _shutdownEvent.Reset();
            }
 
            // UNREACHABLE
        }
 
        private NodeEngineShutdownReason RunInternal(out Exception? shutdownException, ServerNodeHandshake handshake)
        {
            _nodeEndpoint = new ServerNodeEndpointOutOfProc(GetPipeName(handshake), handshake);
            _nodeEndpoint.OnLinkStatusChanged += OnLinkStatusChanged;
            _nodeEndpoint.Listen(this);
 
            var waitHandles = new WaitHandle[] { _shutdownEvent, _packetReceivedEvent };
 
            // Get the current directory before doing any work. We need this so we can restore the directory when the node shutsdown.
            while (true)
            {
                int index = WaitHandle.WaitAny(waitHandles);
                switch (index)
                {
                    case 0:
                        NodeEngineShutdownReason shutdownReason = HandleShutdown(out shutdownException);
                        return shutdownReason;
 
                    case 1:
 
                        while (_receivedPackets.TryDequeue(out INodePacket? packet))
                        {
                            if (packet != null)
                            {
                                HandlePacket(packet);
                            }
                        }
 
                        break;
                }
            }
 
            // UNREACHABLE
        }
 
        #endregion
 
        internal static string GetPipeName(ServerNodeHandshake handshake)
            => NamedPipeUtil.GetPlatformSpecificPipeName($"MSBuildServer-{handshake.ComputeHash()}");
 
        internal static string GetRunningServerMutexName(ServerNodeHandshake handshake)
            => $@"Global\msbuild-server-running-{handshake.ComputeHash()}";
 
        internal static string GetBusyServerMutexName(ServerNodeHandshake handshake)
            => $@"Global\msbuild-server-busy-{handshake.ComputeHash()}";
 
        #region INodePacketFactory Members
 
        /// <summary>
        /// Registers a packet handler.
        /// </summary>
        /// <param name="packetType">The packet type for which the handler should be registered.</param>
        /// <param name="factory">The factory used to create packets.</param>
        /// <param name="handler">The handler for the packets.</param>
        void INodePacketFactory.RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler)
        {
            _packetFactory.RegisterPacketHandler(packetType, factory, handler);
        }
 
        /// <summary>
        /// Unregisters a packet handler.
        /// </summary>
        /// <param name="packetType">The type of packet for which the handler should be unregistered.</param>
        void INodePacketFactory.UnregisterPacketHandler(NodePacketType packetType)
        {
            _packetFactory.UnregisterPacketHandler(packetType);
        }
 
        /// <summary>
        /// Deserializes and routes a packer to the appropriate handler.
        /// </summary>
        /// <param name="nodeId">The node from which the packet was received.</param>
        /// <param name="packetType">The packet type.</param>
        /// <param name="translator">The translator to use as a source for packet data.</param>
        void INodePacketFactory.DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator)
        {
            _packetFactory.DeserializeAndRoutePacket(nodeId, packetType, translator);
        }
 
        /// <summary>
        /// Routes a packet to the appropriate handler.
        /// </summary>
        /// <param name="nodeId">The node id from which the packet was received.</param>
        /// <param name="packet">The packet to route.</param>
        void INodePacketFactory.RoutePacket(int nodeId, INodePacket packet)
        {
            _packetFactory.RoutePacket(nodeId, packet);
        }
 
        #endregion
 
        #region INodePacketHandler Members
 
        /// <summary>
        /// Called when a packet has been received.
        /// </summary>
        /// <param name="node">The node from which the packet was received.</param>
        /// <param name="packet">The packet.</param>
        void INodePacketHandler.PacketReceived(int node, INodePacket packet)
        {
            _receivedPackets.Enqueue(packet);
            _packetReceivedEvent.Set();
        }
 
        #endregion
 
        /// <summary>
        /// Perform necessary actions to shut down the node.
        /// </summary>
        // TODO: it is too complicated, for simple role of server node it needs to be simplified
        private NodeEngineShutdownReason HandleShutdown(out Exception? exception)
        {
            CommunicationsUtilities.Trace("Shutting down with reason: {0}, and exception: {1}.", _shutdownReason, _shutdownException);
 
            // On Windows, a process holds a handle to the current directory,
            // so reset it away from a user-requested folder that may get deleted.
            NativeMethodsShared.SetCurrentDirectory(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory);
 
            exception = _shutdownException;
 
            _nodeEndpoint.OnLinkStatusChanged -= OnLinkStatusChanged;
            _nodeEndpoint.Disconnect();
 
            CommunicationsUtilities.Trace("Shut down complete.");
 
            return _shutdownReason;
        }
 
        /// <summary>
        /// Event handler for the node endpoint's LinkStatusChanged event.
        /// </summary>
        private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status)
        {
            switch (status)
            {
                case LinkStatus.ConnectionFailed:
                case LinkStatus.Failed:
                    _shutdownReason = NodeEngineShutdownReason.ConnectionFailed;
                    _shutdownEvent.Set();
                    break;
 
                default:
                    break;
            }
        }
 
        /// <summary>
        /// Callback for logging packets to be sent.
        /// </summary>
        private void SendPacket(INodePacket packet)
        {
            if (_nodeEndpoint.LinkStatus == LinkStatus.Active)
            {
                _nodeEndpoint.SendData(packet);
            }
        }
 
        /// <summary>
        /// Dispatches the packet to the correct handler.
        /// </summary>
        private void HandlePacket(INodePacket packet)
        {
            switch (packet.Type)
            {
                case NodePacketType.ServerNodeBuildCommand:
                    HandleServerNodeBuildCommandAsync((ServerNodeBuildCommand)packet);
                    break;
                case NodePacketType.NodeBuildComplete:
                    HandleServerShutdownCommand((NodeBuildComplete)packet);
                    break;
                case NodePacketType.ServerNodeBuildCancel:
                    HandleBuildCancel();
                    break;
            }
        }
 
        /// <summary>
        /// NodeBuildComplete is used to signalize that node work is done (including server node)
        /// and shall recycle or shutdown if PrepareForReuse is false.
        /// </summary>
        /// <param name="buildComplete"></param>
        private void HandleServerShutdownCommand(NodeBuildComplete buildComplete)
        {
            _shutdownReason = buildComplete.PrepareForReuse ? NodeEngineShutdownReason.BuildCompleteReuse : NodeEngineShutdownReason.BuildComplete;
            _shutdownEvent.Set();
        }
 
        private void HandleBuildCancel()
        {
            CommunicationsUtilities.Trace("Received request to cancel build running on MSBuild Server. MSBuild server will shutdown.}");
            _cancelRequested = true;
            BuildManager.DefaultBuildManager.CancelAllSubmissions();
        }
 
        private void HandleServerNodeBuildCommandAsync(ServerNodeBuildCommand command)
        {
            Task.Run(() =>
            {
                try
                {
                    HandleServerNodeBuildCommand(command);
                }
                catch (Exception e)
                {
                    _shutdownException = e;
                    _shutdownReason = NodeEngineShutdownReason.Error;
                    _shutdownEvent.Set();
                }
            });
        }
 
        private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command)
        {
            CommunicationsUtilities.Trace("Building with MSBuild server with command line {0}", command.CommandLine);
            using var serverBusyMutex = ServerNamedMutex.OpenOrCreateMutex(name: _serverBusyMutexName, createdNew: out var holdsMutex);
            if (!holdsMutex)
            {
                // Client must have send request message to server even though serer is busy.
                // It is not a race condition, as client exclusivity is also guaranteed by name pipe which allows only one client to connect.
                _shutdownException = new InvalidOperationException("Client requested build while server is busy processing previous client build request.");
                _shutdownReason = NodeEngineShutdownReason.Error;
                _shutdownEvent.Set();
 
                return;
            }
 
            // Set build process context
            Directory.SetCurrentDirectory(command.StartupDirectory);
 
            CommunicationsUtilities.SetEnvironment(command.BuildProcessEnvironment);
            Traits.UpdateFromEnvironment();
 
            Thread.CurrentThread.CurrentCulture = command.Culture;
            Thread.CurrentThread.CurrentUICulture = command.UICulture;
 
            // Reconfigure static BuildParameters.StartupDirectory to have this value
            // same as startup directory of msbuild entry client or dotnet CLI.
            BuildParameters.StartupDirectory = command.StartupDirectory;
 
            // Configure console configuration so Loggers can change their behavior based on Target (client) Console properties.
            ConsoleConfiguration.Provider = command.ConsoleConfiguration;
 
            // Initiate build telemetry
            if (command.PartialBuildTelemetry != null)
            {
                BuildTelemetry buildTelemetry = KnownTelemetry.PartialBuildTelemetry ??= new BuildTelemetry();
 
                buildTelemetry.StartAt = command.PartialBuildTelemetry.StartedAt;
                buildTelemetry.InitialServerState = command.PartialBuildTelemetry.InitialServerState;
                buildTelemetry.ServerFallbackReason = command.PartialBuildTelemetry.ServerFallbackReason;
            }
 
            // Also try our best to increase chance custom Loggers which use Console static members will work as expected.
            try
            {
                if (NativeMethodsShared.IsWindows && command.ConsoleConfiguration.BufferWidth > 0)
                {
                    Console.BufferWidth = command.ConsoleConfiguration.BufferWidth;
                }
 
                if ((int)command.ConsoleConfiguration.BackgroundColor != -1)
                {
                    Console.BackgroundColor = command.ConsoleConfiguration.BackgroundColor;
                }
            }
            catch (Exception)
            {
                // Ignore exception, it is best effort only
            }
 
            // Configure console output redirection
            var oldOut = Console.Out;
            var oldErr = Console.Error;
            (int exitCode, string exitType) buildResult;
 
            // Dispose must be called before the server sends ServerNodeBuildResult packet
            using (var outWriter = RedirectConsoleWriter.Create(text => SendPacket(new ServerNodeConsoleWrite(text, ConsoleOutput.Standard))))
            using (var errWriter = RedirectConsoleWriter.Create(text => SendPacket(new ServerNodeConsoleWrite(text, ConsoleOutput.Error))))
            {
                Console.SetOut(outWriter);
                Console.SetError(errWriter);
 
                buildResult = _buildFunction(command.CommandLine);
 
                Console.SetOut(oldOut);
                Console.SetError(oldErr);
            }
 
            // On Windows, a process holds a handle to the current directory,
            // so reset it away from a user-requested folder that may get deleted.
            NativeMethodsShared.SetCurrentDirectory(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory);
 
            _nodeEndpoint.ClientWillDisconnect();
            var response = new ServerNodeBuildResult(buildResult.exitCode, buildResult.exitType);
            SendPacket(response);
 
            // Shutdown server if cancel was requested. This is consistent with nodes behavior.
            _shutdownReason = _cancelRequested ? NodeEngineShutdownReason.BuildComplete : NodeEngineShutdownReason.BuildCompleteReuse;
            _shutdownEvent.Set();
        }
 
        internal sealed class RedirectConsoleWriter : TextWriter
        {
            private readonly Action<string> _writeCallback;
            private readonly Timer _timer;
            private readonly TextWriter _syncWriter;
 
            private readonly StringWriter _internalWriter;
 
            private RedirectConsoleWriter(Action<string> writeCallback)
            {
                _writeCallback = writeCallback;
                _internalWriter = new StringWriter();
                _syncWriter = Synchronized(_internalWriter);
                _timer = new Timer(TimerCallback, null, 0, 40);
            }
 
            public override Encoding Encoding => _internalWriter.Encoding;
 
            public static TextWriter Create(Action<string> writeCallback)
            {
                RedirectConsoleWriter writer = new RedirectConsoleWriter(writeCallback);
 
                return writer;
            }
 
            public override void Flush()
            {
                var sb = _internalWriter.GetStringBuilder();
                string captured = sb.ToString();
                sb.Clear();
 
                _writeCallback(captured);
                _internalWriter.Flush();
            }
 
            public override void Write(char value) => _syncWriter.Write(value);
 
            public override void Write(char[]? buffer) => _syncWriter.Write(buffer);
 
            public override void Write(char[] buffer, int index, int count) => _syncWriter.Write(buffer, index, count);
 
            public override void Write(bool value) => _syncWriter.Write(value);
 
            public override void Write(int value) => _syncWriter.Write(value);
 
            public override void Write(uint value) => _syncWriter.Write(value);
 
            public override void Write(long value) => _syncWriter.Write(value);
 
            public override void Write(ulong value) => _syncWriter.Write(value);
 
            public override void Write(float value) => _syncWriter.Write(value);
 
            public override void Write(double value) => _syncWriter.Write(value);
 
            public override void Write(decimal value) => _syncWriter.Write(value);
 
            public override void Write(string? value) => _syncWriter.Write(value);
 
            public override void Write(object? value) => _syncWriter.Write(value);
 
            public override void Write(string format, object? arg0) => _syncWriter.Write(format, arg0);
 
            public override void Write(string format, object? arg0, object? arg1) => _syncWriter.Write(format, arg0, arg1);
 
            public override void Write(string format, object? arg0, object? arg1, object? arg2) => _syncWriter.Write(format, arg0, arg1, arg2);
 
            public override void Write(string format, params object?[] arg) => _syncWriter.WriteLine(format, arg);
 
            public override void WriteLine() => _syncWriter.WriteLine();
 
            public override void WriteLine(char value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(decimal value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(char[]? buffer) => _syncWriter.WriteLine(buffer);
 
            public override void WriteLine(char[] buffer, int index, int count) => _syncWriter.WriteLine(buffer, index, count);
 
            public override void WriteLine(bool value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(int value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(uint value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(long value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(ulong value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(float value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(double value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(string? value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(object? value) => _syncWriter.WriteLine(value);
 
            public override void WriteLine(string format, object? arg0) => _syncWriter.WriteLine(format, arg0);
 
            public override void WriteLine(string format, object? arg0, object? arg1) => _syncWriter.WriteLine(format, arg0, arg1);
 
            public override void WriteLine(string format, object? arg0, object? arg1, object? arg2) => _syncWriter.WriteLine(format, arg0, arg1, arg2);
 
            public override void WriteLine(string format, params object?[] arg) => _syncWriter.WriteLine(format, arg);
 
            private void TimerCallback(object? state)
            {
                if (_internalWriter.GetStringBuilder().Length > 0)
                {
                    _syncWriter.Flush();
                }
            }
 
            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {
                    _timer.Dispose();
                    Flush();
                    _internalWriter?.Dispose();
                }
 
                base.Dispose(disposing);
            }
        }
    }
}