diff --git a/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs new file mode 100644 index 00000000000..9e58d151b66 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs @@ -0,0 +1,29 @@ +// 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.Text; +using System.Threading.Tasks; +using Microsoft.Build.Execution; +using Xunit; + +namespace Microsoft.Build.Engine.UnitTests.BackEnd +{ + public class RedirectConsoleWriter_Tests + { + [Fact] + public async Task EmitConsoleMessages() + { + StringBuilder sb = new StringBuilder(); + var writer = OutOfProcServerNode.RedirectConsoleWriter.Create(text => sb.Append(text)); + + writer.WriteLine("Line 1"); + await Task.Delay(300); + writer.Write("Line 2"); + writer.Dispose(); + + Assert.Equal($"Line 1{Environment.NewLine}Line 2", sb.ToString()); + } + } +} diff --git a/src/Build/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs b/src/Build/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs index dce14e3d422..6881debdae2 100644 --- a/src/Build/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs @@ -54,7 +54,7 @@ internal NodeEndpointOutOfProc( /// /// Returns the host handshake for this node endpoint /// - protected override Handshake GetHandshake() + protected override IHandshake GetHandshake() { return new Handshake(CommunicationsUtilities.GetHandshakeOptions(taskHost: false, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture(), nodeReuse: _enableReuse, lowPriority: _lowPriority)); } diff --git a/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs b/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs new file mode 100644 index 00000000000..528d27056da --- /dev/null +++ b/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +using Microsoft.Build.Internal; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This is an implementation of out-of-proc server node endpoint. + /// + internal sealed class ServerNodeEndpointOutOfProc : NodeEndpointOutOfProcBase + { + private readonly IHandshake _handshake; + + /// + /// Instantiates an endpoint to act as a client + /// + /// The name of the pipe to which we should connect. + /// + internal ServerNodeEndpointOutOfProc( + string pipeName, + IHandshake handshake) + { + _handshake = handshake; + + InternalConstruct(pipeName); + } + + /// + /// Returns the host handshake for this node endpoint + /// + protected override IHandshake GetHandshake() + { + return _handshake; + } + } +} diff --git a/src/Build/BackEnd/Node/ConsoleOutput.cs b/src/Build/BackEnd/Node/ConsoleOutput.cs new file mode 100644 index 00000000000..8cf4092bc84 --- /dev/null +++ b/src/Build/BackEnd/Node/ConsoleOutput.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +namespace Microsoft.Build.BackEnd +{ + internal enum ConsoleOutput + { + Standard = 1, + Error + } +} diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs new file mode 100644 index 00000000000..3d532f3506c --- /dev/null +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -0,0 +1,368 @@ +// 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.Generic; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; + +namespace Microsoft.Build.Execution +{ + /// + /// This class represents an implementation of INode for out-of-proc server nodes aka MSBuild server + /// + public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacketHandler + { + private readonly Func _buildFunction; + + /// + /// The endpoint used to talk to the host. + /// + private INodeEndpoint _nodeEndpoint = default!; + + /// + /// The packet factory. + /// + private readonly NodePacketFactory _packetFactory; + + /// + /// The queue of packets we have received but which have not yet been processed. + /// + private readonly ConcurrentQueue _receivedPackets; + + /// + /// The event which is set when we receive packets. + /// + private readonly AutoResetEvent _packetReceivedEvent; + + /// + /// The event which is set when we should shut down. + /// + private readonly ManualResetEvent _shutdownEvent; + + /// + /// The reason we are shutting down. + /// + private NodeEngineShutdownReason _shutdownReason; + + /// + /// The exception, if any, which caused shutdown. + /// + private Exception? _shutdownException = null; + + /// + /// Flag indicating if we should debug communications or not. + /// + private readonly bool _debugCommunications; + + private string _serverBusyMutexName = default!; + + public OutOfProcServerNode(Func buildFunction) + { + _buildFunction = buildFunction; + new Dictionary(); + _debugCommunications = (Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM") == "1"); + + _receivedPackets = new ConcurrentQueue(); + _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); + } + + #region INode Members + + /// + /// Starts up the node and processes messages until the node is requested to shut down. + /// + /// The exception which caused shutdown, if any. + /// The reason for shutting down. + public NodeEngineShutdownReason Run(out Exception? shutdownException) + { + string msBuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; + var handshake = new ServerNodeHandshake( + CommunicationsUtilities.GetHandshakeOptions(taskHost: false, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture()), + msBuildLocation); + + string pipeName = NamedPipeUtil.GetPipeNameOrPath("MSBuildServer-" + handshake.ComputeHash()); + + string serverRunningMutexName = $@"{ServerNamedMutex.RunningServerMutexNamePrefix}{pipeName}"; + _serverBusyMutexName = $@"{ServerNamedMutex.BusyServerMutexNamePrefix}{pipeName}"; + + // TODO: shall we address possible race condition. It is harmless as it, with acceptable probability, just cause unnecessary process spawning + // and of two processes will become victim and fails, build will not be affected + using var serverRunningMutex = ServerNamedMutex.OpenOrCreateMutex(serverRunningMutexName, out bool mutexCreatedNew); + if (!mutexCreatedNew) + { + shutdownException = new InvalidOperationException("MSBuild server is already running!"); + return NodeEngineShutdownReason.Error; + } + + _nodeEndpoint = new ServerNodeEndpointOutOfProc(pipeName, 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 + + #region INodePacketFactory Members + + /// + /// Registers a packet handler. + /// + /// The packet type for which the handler should be registered. + /// The factory used to create packets. + /// The handler for the packets. + void INodePacketFactory.RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) + { + _packetFactory.RegisterPacketHandler(packetType, factory, handler); + } + + /// + /// Unregisters a packet handler. + /// + /// The type of packet for which the handler should be unregistered. + void INodePacketFactory.UnregisterPacketHandler(NodePacketType packetType) + { + _packetFactory.UnregisterPacketHandler(packetType); + } + + /// + /// Deserializes and routes a packer to the appropriate handler. + /// + /// The node from which the packet was received. + /// The packet type. + /// The translator to use as a source for packet data. + void INodePacketFactory.DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator) + { + _packetFactory.DeserializeAndRoutePacket(nodeId, packetType, translator); + } + + /// + /// Routes a packet to the appropriate handler. + /// + /// The node id from which the packet was received. + /// The packet to route. + void INodePacketFactory.RoutePacket(int nodeId, INodePacket packet) + { + _packetFactory.RoutePacket(nodeId, packet); + } + + #endregion + + #region INodePacketHandler Members + + /// + /// Called when a packet has been received. + /// + /// The node from which the packet was received. + /// The packet. + void INodePacketHandler.PacketReceived(int node, INodePacket packet) + { + _receivedPackets.Enqueue(packet); + _packetReceivedEvent.Set(); + } + + #endregion + + /// + /// Perform necessary actions to shut down the node. + /// + // 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); + + exception = _shutdownException; + + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.OnLinkStatusChanged -= OnLinkStatusChanged; + } + + _nodeEndpoint.Disconnect(); + + CommunicationsUtilities.Trace("Shut down complete."); + + return _shutdownReason; + } + + /// + /// Event handler for the node endpoint's LinkStatusChanged event. + /// + private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) + { + switch (status) + { + case LinkStatus.ConnectionFailed: + case LinkStatus.Failed: + _shutdownReason = NodeEngineShutdownReason.ConnectionFailed; + _shutdownEvent.Set(); + break; + + case LinkStatus.Inactive: + break; + + case LinkStatus.Active: + break; + + default: + break; + } + } + + /// + /// Callback for logging packets to be sent. + /// + private void SendPacket(INodePacket packet) + { + if (_nodeEndpoint.LinkStatus == LinkStatus.Active) + { + _nodeEndpoint.SendData(packet); + } + } + + /// + /// Dispatches the packet to the correct handler. + /// + private void HandlePacket(INodePacket packet) + { + switch (packet.Type) + { + case NodePacketType.ServerNodeBuildCommand: + HandleServerNodeBuildCommand((ServerNodeBuildCommand)packet); + break; + } + } + + private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) + { + 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(); + } + + // set build process context + Directory.SetCurrentDirectory(command.StartupDirectory); + CommunicationsUtilities.SetEnvironment(command.BuildProcessEnvironment); + Thread.CurrentThread.CurrentCulture = command.Culture; + Thread.CurrentThread.CurrentUICulture = command.UICulture; + + // 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); + + var response = new ServerNodeBuildResult(buildResult.exitCode, buildResult.exitType); + SendPacket(response); + + _shutdownReason = NodeEngineShutdownReason.BuildCompleteReuse; + _shutdownEvent.Set(); + } + + internal sealed class RedirectConsoleWriter : StringWriter + { + private readonly Action _writeCallback; + private readonly Timer _timer; + private readonly TextWriter _syncWriter; + + private RedirectConsoleWriter(Action writeCallback) + { + _writeCallback = writeCallback; + _syncWriter = Synchronized(this); + _timer = new Timer(TimerCallback, null, 0, 200); + } + + public static TextWriter Create(Action writeCallback) + { + RedirectConsoleWriter writer = new(writeCallback); + return writer._syncWriter; + } + + private void TimerCallback(object? state) + { + if (GetStringBuilder().Length > 0) + { + _syncWriter.Flush(); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _timer.Dispose(); + Flush(); + } + + base.Dispose(disposing); + } + + public override void Flush() + { + var sb = GetStringBuilder(); + var captured = sb.ToString(); + sb.Clear(); + _writeCallback(captured); + + base.Flush(); + } + } + } +} diff --git a/src/Build/BackEnd/Node/ServerNamedMutex.cs b/src/Build/BackEnd/Node/ServerNamedMutex.cs new file mode 100644 index 00000000000..e149cda704b --- /dev/null +++ b/src/Build/BackEnd/Node/ServerNamedMutex.cs @@ -0,0 +1,91 @@ +// 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.Threading; + +namespace Microsoft.Build.Execution +{ + internal sealed class ServerNamedMutex : IDisposable + { + public const string RunningServerMutexNamePrefix = @"Global\server-running-"; + public const string BusyServerMutexNamePrefix = @"Global\server-busy-"; + + private readonly Mutex _serverMutex; + + public bool IsDisposed { get; private set; } + + public bool IsLocked { get; private set; } + + public ServerNamedMutex(string mutexName, out bool createdNew) + { + _serverMutex = new Mutex( + initiallyOwned: true, + name: mutexName, + createdNew: out createdNew); + + if (createdNew) + { + IsLocked = true; + } + } + + internal static ServerNamedMutex OpenOrCreateMutex(string name, out bool createdNew) + { + // TODO: verify it is not needed anymore + // if (PlatformInformation.IsRunningOnMono) + // { + // return new ServerFileMutexPair(name, initiallyOwned: true, out createdNew); + // } + // else + + return new ServerNamedMutex(name, out createdNew); + } + + public static bool WasOpen(string mutexName) + { + bool result = Mutex.TryOpenExisting(mutexName, out Mutex? mutex); + mutex?.Dispose(); + + return result; + } + + public bool TryLock(int timeoutMs) + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(ServerNamedMutex)); + } + + if (IsLocked) + { + throw new InvalidOperationException("Lock already held"); + } + + return IsLocked = _serverMutex.WaitOne(timeoutMs); + } + + public void Dispose() + { + if (IsDisposed) + { + return; + } + + IsDisposed = true; + + try + { + if (IsLocked) + { + _serverMutex.ReleaseMutex(); + } + } + finally + { + _serverMutex.Dispose(); + IsLocked = false; + } + } + } +} diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs new file mode 100644 index 00000000000..48ab050cf1e --- /dev/null +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -0,0 +1,92 @@ +// 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.Generic; +using System.Globalization; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Contains all of the information necessary for a entry node to run a command line. + /// + internal sealed class ServerNodeBuildCommand : INodePacket + { + private string _commandLine = default!; + private string _startupDirectory = default!; + private Dictionary _buildProcessEnvironment = default!; + private CultureInfo _culture = default!; + private CultureInfo _uiCulture = default!; + + /// + /// Retrieves the packet type. + /// + public NodePacketType Type => NodePacketType.ServerNodeBuildCommand; + + /// + /// The startup directory + /// + public string CommandLine => _commandLine; + + /// + /// The startup directory + /// + public string StartupDirectory => _startupDirectory; + + /// + /// The process environment. + /// + public Dictionary BuildProcessEnvironment => _buildProcessEnvironment; + + /// + /// The culture + /// + public CultureInfo Culture => _culture; + + /// + /// The UI culture. + /// + public CultureInfo UICulture => _uiCulture; + + /// + /// Private constructor for deserialization + /// + private ServerNodeBuildCommand() + { + } + + public ServerNodeBuildCommand(string commandLine, string startupDirectory, Dictionary buildProcessEnvironment, CultureInfo culture, CultureInfo uiCulture) + { + _commandLine = commandLine; + _startupDirectory = startupDirectory; + _buildProcessEnvironment = buildProcessEnvironment; + _culture = culture; + _uiCulture = uiCulture; + } + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(ITranslator translator) + { + translator.Translate(ref _commandLine); + translator.Translate(ref _startupDirectory); + translator.TranslateDictionary(ref _buildProcessEnvironment, StringComparer.OrdinalIgnoreCase); + translator.TranslateCulture(ref _culture); + translator.TranslateCulture(ref _uiCulture); + } + + /// + /// Factory for deserialization. + /// + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + ServerNodeBuildCommand command = new(); + command.Translate(translator); + + return command; + } + } +} diff --git a/src/Build/BackEnd/Node/ServerNodeBuildResult.cs b/src/Build/BackEnd/Node/ServerNodeBuildResult.cs new file mode 100644 index 00000000000..b7b9b3e7a2c --- /dev/null +++ b/src/Build/BackEnd/Node/ServerNodeBuildResult.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// + +namespace Microsoft.Build.BackEnd +{ + internal sealed class ServerNodeBuildResult : INodePacket + { + private int _exitCode = default!; + private string _exitType = default!; + + /// + /// Packet type. + /// This has to be in sync with + /// + public NodePacketType Type => NodePacketType.ServerNodeBuildResult; + + public int ExitCode => _exitCode; + + public string ExitType => _exitType; + + /// + /// Private constructor for deserialization + /// + private ServerNodeBuildResult() { } + + public ServerNodeBuildResult(int exitCode, string exitType) + { + _exitCode = exitCode; + _exitType = exitType; + } + + public void Translate(ITranslator translator) + { + translator.Translate(ref _exitCode); + translator.Translate(ref _exitType); + } + + /// + /// Factory for deserialization. + /// + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + ServerNodeBuildResult command = new(); + command.Translate(translator); + + return command; + } + } +} diff --git a/src/Build/BackEnd/Node/ServerNodeConsoleWrite.cs b/src/Build/BackEnd/Node/ServerNodeConsoleWrite.cs new file mode 100644 index 00000000000..da3f8473905 --- /dev/null +++ b/src/Build/BackEnd/Node/ServerNodeConsoleWrite.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Build.BackEnd +{ + internal sealed class ServerNodeConsoleWrite : INodePacket + { + private string _text = default!; + private ConsoleOutput _outputType = default!; + + /// + /// Packet type. + /// + public NodePacketType Type => NodePacketType.ServerNodeConsoleWrite; + + public string Text => _text; + + /// + /// Console output for the message + /// + public ConsoleOutput OutputType => _outputType; + + /// + /// Private constructor for deserialization + /// + private ServerNodeConsoleWrite() { } + + public ServerNodeConsoleWrite(string text, ConsoleOutput outputType) + { + _text = text; + _outputType = outputType; + } + + public void Translate(ITranslator translator) + { + translator.Translate(ref _text); + translator.TranslateEnum(ref _outputType, (int)_outputType); + } + + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + ServerNodeConsoleWrite command = new(); + command.Translate(translator); + + return command; + } + } +} diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index db86e8cbc7a..0bdc0df6561 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -145,9 +145,16 @@ + + + + + + + diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index e69de29bb2d..6a1467598ac 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -0,0 +1,3 @@ +Microsoft.Build.Execution.OutOfProcServerNode +Microsoft.Build.Execution.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction) -> void +Microsoft.Build.Execution.OutOfProcServerNode.Run(out System.Exception shutdownException) -> Microsoft.Build.Execution.NodeEngineShutdownReason diff --git a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt index e69de29bb2d..6cdbdc2bcc1 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -0,0 +1,4 @@ + +Microsoft.Build.Execution.OutOfProcServerNode +Microsoft.Build.Execution.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction) -> void +Microsoft.Build.Execution.OutOfProcServerNode.Run(out System.Exception shutdownException) -> Microsoft.Build.Execution.NodeEngineShutdownReason \ No newline at end of file diff --git a/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs b/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs index 36ea494b383..0f1e1eacb05 100644 --- a/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs +++ b/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs @@ -28,7 +28,7 @@ internal NodeEndpointOutOfProcTaskHost() /// /// Returns the host handshake for this node endpoint /// - protected override Handshake GetHandshake() + protected override IHandshake GetHandshake() { return new Handshake(CommunicationsUtilities.GetHandshakeOptions(taskHost: true)); } diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 42251449d2a..83c62ace30a 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -1959,6 +1959,11 @@ private static bool IsEnvironmentVariable(string envVar) /// internal static bool usingSwitchesFromAutoResponseFile = false; + /// + /// Indicates that this process is working as a server. + /// + private static bool s_isServerNode; + /// /// Parses the auto-response file (assumes the "/noautoresponse" switch is not specified on the command line), and combines the /// switches from the auto-response file with the switches passed in. @@ -2629,6 +2634,42 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool OutOfProcTaskHostNode node = new OutOfProcTaskHostNode(); shutdownReason = node.Run(out nodeException); } + else if (nodeModeNumber == 8) + { + // Since build function has to reuse code from *this* class and OutOfProcServerNode is in different assembly + // we have to pass down xmake build invocation to avoid circular dependency + Func buildFunction = (commandLine) => + { + int exitCode; + ExitType exitType; + + if (!s_initialized) + { + exitType = ExitType.InitializationError; + } + else + { + exitType = Execute( +#if FEATURE_GET_COMMANDLINE + commandLine +#else + QuotingUtilities.SplitUnquoted(commandLine).ToArray() +#endif + ); + exitCode = exitType == ExitType.Success ? 0 : 1; + } + exitCode = exitType == ExitType.Success ? 0 : 1; + + return (exitCode, exitType.ToString()); + }; + + OutOfProcServerNode node = new(buildFunction); + + s_isServerNode = true; + shutdownReason = node.Run(out nodeException); + + FileUtilities.ClearCacheDirectory(); + } else { CommandLineSwitchException.Throw("InvalidNodeNumberValue", nodeModeNumber.ToString()); @@ -3124,6 +3165,12 @@ List loggers consoleParameters = AggregateParameters(consoleParameters, consoleLoggerParameters); } + // Always use ANSI escape codes when the build is initiated by server + if (s_isServerNode) + { + consoleParameters = AggregateParameters(consoleParameters, new[] { "FORCECONSOLECOLOR" }); + } + // Check to see if there is a possibility we will be logging from an out-of-proc node. // If so (we're multi-proc or the in-proc node is disabled), we register a distributed logger. if (cpuCount == 1 && Environment.GetEnvironmentVariable("MSBUILDNOINPROCNODE") != "1") diff --git a/src/Shared/BinaryTranslator.cs b/src/Shared/BinaryTranslator.cs index b1540445884..1a999ac682d 100644 --- a/src/Shared/BinaryTranslator.cs +++ b/src/Shared/BinaryTranslator.cs @@ -435,6 +435,7 @@ private static bool TryLoadCulture(string cultureName, out CultureInfo cultureIn /// Finally, converting the enum to an int assumes that we always want to transport enums as ints. This /// works in all of our current cases, but certainly isn't perfectly generic. public void TranslateEnum(ref T value, int numericValue) + where T : struct, Enum { numericValue = _reader.ReadInt32(); Type enumType = value.GetType(); @@ -1039,10 +1040,8 @@ public void TranslateCulture(ref CultureInfo value) /// Finally, converting the enum to an int assumes that we always want to transport enums as ints. This /// works in all of our current cases, but certainly isn't perfectly generic. public void TranslateEnum(ref T value, int numericValue) + where T : struct, Enum { - Type enumType = value.GetType(); - ErrorUtilities.VerifyThrow(enumType.GetTypeInfo().IsEnum, "Must pass an enum type."); - _writer.Write(numericValue); } diff --git a/src/Shared/CommunicationsUtilities.cs b/src/Shared/CommunicationsUtilities.cs index 9fdf1e22306..7e86ff4ee03 100644 --- a/src/Shared/CommunicationsUtilities.cs +++ b/src/Shared/CommunicationsUtilities.cs @@ -14,6 +14,8 @@ using Microsoft.Build.Framework; using Microsoft.Build.Shared; using System.Reflection; +using System.Security.Cryptography; +using System.Text; #if !CLR2COMPATIBILITY using Microsoft.Build.Shared.Debugging; @@ -75,7 +77,24 @@ internal enum HandshakeOptions Arm64 = 128, } - internal readonly struct Handshake + internal interface IHandshake + { + int[] RetrieveHandshakeComponents(); + + /// + /// Get string key representing all handshake values. It does not need to be human readable. + /// + string GetKey(); + + /// + /// Some handshakes uses very 1st byte to encode version of handshake in it, + /// so if it does not match it can reject it early based on very first byte. + /// Null means that no such encoding is used + /// + byte? ExpectedVersionInFirstByte { get; } + } + + internal readonly struct Handshake : IHandshake { readonly int options; readonly int salt; @@ -113,7 +132,7 @@ public override string ToString() return String.Format("{0} {1} {2} {3} {4} {5} {6}", options, salt, fileVersionMajor, fileVersionMinor, fileVersionBuild, fileVersionPrivate, sessionId); } - internal int[] RetrieveHandshakeComponents() + public int[] RetrieveHandshakeComponents() { return new int[] { @@ -126,6 +145,88 @@ internal int[] RetrieveHandshakeComponents() CommunicationsUtilities.AvoidEndOfHandshakeSignal(sessionId) }; } + + public string GetKey() => $"{options} {salt} {fileVersionMajor} {fileVersionMinor} {fileVersionBuild} {fileVersionPrivate} {sessionId}".ToString(CultureInfo.InvariantCulture); + + public byte? ExpectedVersionInFirstByte => CommunicationsUtilities.handshakeVersion; + } + + internal sealed class ServerNodeHandshake : IHandshake + { + readonly int _options; + readonly int _salt; + readonly int _fileVersionMajor; + readonly int _fileVersionMinor; + readonly int _fileVersionBuild; + readonly int _fileVersionRevision; + + internal ServerNodeHandshake(HandshakeOptions nodeType, string msBuildLocation) + { + // We currently use 6 bits of this 32-bit integer. Very old builds will instantly reject any handshake that does not start with F5 or 06; slightly old builds always lead with 00. + // This indicates in the first byte that we are a modern build. + _options = (int)nodeType | (CommunicationsUtilities.handshakeVersion << 24); + string handshakeSalt = Environment.GetEnvironmentVariable("MSBUILDNODEHANDSHAKESALT"); + var msBuildFile = new FileInfo(msBuildLocation); + var msBuildDirectory = msBuildFile.DirectoryName; + _salt = ComputeHandshakeHash(handshakeSalt + msBuildDirectory); + Version fileVersion = new Version(FileVersionInfo.GetVersionInfo(msBuildLocation).FileVersion ?? string.Empty); + _fileVersionMajor = fileVersion.Major; + _fileVersionMinor = fileVersion.Minor; + _fileVersionBuild = fileVersion.Build; + _fileVersionRevision = fileVersion.Revision; + } + + internal const int EndOfHandshakeSignal = -0x2a2a2a2a; + + /// + /// Compute stable hash as integer + /// + private static int ComputeHandshakeHash(string fromString) + { + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(fromString)); + + return BitConverter.ToInt32(bytes, 0); + } + + internal static int AvoidEndOfHandshakeSignal(int x) + { + return x == EndOfHandshakeSignal ? ~x : x; + } + + public int[] RetrieveHandshakeComponents() + { + return new int[] + { + AvoidEndOfHandshakeSignal(_options), + AvoidEndOfHandshakeSignal(_salt), + AvoidEndOfHandshakeSignal(_fileVersionMajor), + AvoidEndOfHandshakeSignal(_fileVersionMinor), + AvoidEndOfHandshakeSignal(_fileVersionBuild), + AvoidEndOfHandshakeSignal(_fileVersionRevision), + }; + } + + public string GetKey() + { + return $"{_options} {_salt} {_fileVersionMajor} {_fileVersionMinor} {_fileVersionBuild} {_fileVersionRevision}" + .ToString(CultureInfo.InvariantCulture); + } + + public byte? ExpectedVersionInFirstByte => null; + + /// + /// Computes Handshake stable hash string representing whole state of handshake. + /// + public string ComputeHash() + { + var input = GetKey(); + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input)); + return Convert.ToBase64String(bytes) + .Replace("/", "_") + .Replace("=", string.Empty); + } } /// diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs index 481a99bfce9..b0ec1f1f6c5 100644 --- a/src/Shared/INodePacket.cs +++ b/src/Shared/INodePacket.cs @@ -189,6 +189,24 @@ internal enum NodePacketType : byte /// Message sent back to a node informing it about the resource that were granted by the scheduler. /// ResourceResponse, + + /// + /// Command in form of MSBuild command line for server node - MSBuild Server. + /// Keep this enum value constant intact as this is part of contract with dotnet CLI + /// + ServerNodeBuildCommand = 0xF0, + + /// + /// Response from server node command + /// Keep this enum value constant intact as this is part of contract with dotnet CLI + /// + ServerNodeBuildResult = 0xF1, + + /// + /// Info about server console activity. + /// Keep this enum value constant intact as this is part of contract with dotnet CLI + /// + ServerNodeConsoleWrite = 0xF2, } #endregion diff --git a/src/Shared/ITranslator.cs b/src/Shared/ITranslator.cs index 61dc02cc3a0..56f47d0c5f4 100644 --- a/src/Shared/ITranslator.cs +++ b/src/Shared/ITranslator.cs @@ -235,7 +235,8 @@ BinaryWriter Writer /// can you simply pass as ref Enum, because an enum instance doesn't match that function signature. /// Finally, converting the enum to an int assumes that we always want to transport enums as ints. This /// works in all of our current cases, but certainly isn't perfectly generic. - void TranslateEnum(ref T value, int numericValue); + void TranslateEnum(ref T value, int numericValue) + where T : struct, Enum; /// /// Translates a value using the .Net binary formatter. diff --git a/src/Shared/NamedPipeUtil.cs b/src/Shared/NamedPipeUtil.cs index 4fbe37002a4..9db07e16722 100644 --- a/src/Shared/NamedPipeUtil.cs +++ b/src/Shared/NamedPipeUtil.cs @@ -17,6 +17,11 @@ internal static string GetPipeNameOrPath(int? processId = null) string pipeName = $"MSBuild{processId}"; + return GetPipeNameOrPath(pipeName); + } + + internal static string GetPipeNameOrPath(string pipeName) + { if (NativeMethodsShared.IsUnixLike) { // If we're on a Unix machine then named pipes are implemented using Unix Domain Sockets. diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index ea696a53ec3..f402ecac71a 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -14,6 +14,7 @@ using Microsoft.Build.Shared; #if FEATURE_SECURITY_PERMISSIONS || FEATURE_PIPE_SECURITY using System.Security.AccessControl; +using System.Linq; #endif #if FEATURE_PIPE_SECURITY && FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR using System.Security.Principal; @@ -185,7 +186,7 @@ public void SendData(INodePacket packet) /// /// Instantiates an endpoint to act as a client /// - internal void InternalConstruct() + internal void InternalConstruct(string pipeName = null) { _status = LinkStatus.Inactive; _asyncDataMonitor = new object(); @@ -194,7 +195,7 @@ internal void InternalConstruct() _packetStream = new MemoryStream(); _binaryWriter = new BinaryWriter(_packetStream); - string pipeName = NamedPipeUtil.GetPipeNameOrPath(); + pipeName ??= NamedPipeUtil.GetPipeNameOrPath(); #if FEATURE_PIPE_SECURITY && FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR if (!NativeMethodsShared.IsMono) @@ -245,7 +246,7 @@ internal void InternalConstruct() /// /// Returns the host handshake for this node endpoint /// - protected abstract Handshake GetHandshake(); + protected abstract IHandshake GetHandshake(); /// /// Updates the current link status if it has changed and notifies any registered delegates. @@ -373,7 +374,7 @@ private void PacketPumpProc() // The handshake protocol is a series of int exchanges. The host sends us a each component, and we // verify it. Afterwards, the host sends an "End of Handshake" signal, to which we respond in kind. // Once the handshake is complete, both sides can be assured the other is ready to accept data. - Handshake handshake = GetHandshake(); + IHandshake handshake = GetHandshake(); try { int[] handshakeComponents = handshake.RetrieveHandshakeComponents();