From b0a788d8f8e487e7f2708c309f074ad28df8c09f Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Thu, 21 Apr 2022 14:24:17 +0200 Subject: [PATCH 01/43] [WIP-FEATURE] MSBuild server node (#7489) --- .../BackEnd/RedirectConsoleWriter_Tests.cs | 29 ++ .../Communications/NodeEndpointOutOfProc.cs | 2 +- .../ServerNodeEndpointOutOfProc.cs | 38 ++ src/Build/BackEnd/Node/ConsoleOutput.cs | 12 + src/Build/BackEnd/Node/OutOfProcServerNode.cs | 368 ++++++++++++++++++ src/Build/BackEnd/Node/ServerNamedMutex.cs | 91 +++++ .../BackEnd/Node/ServerNodeBuildCommand.cs | 92 +++++ .../BackEnd/Node/ServerNodeBuildResult.cs | 50 +++ .../BackEnd/Node/ServerNodeConsoleWrite.cs | 48 +++ src/Build/Microsoft.Build.csproj | 7 + .../PublicAPI/net/PublicAPI.Unshipped.txt | 3 + .../netstandard/PublicAPI.Unshipped.txt | 4 + src/MSBuild/NodeEndpointOutOfProcTaskHost.cs | 2 +- src/MSBuild/XMake.cs | 47 +++ src/Shared/BinaryTranslator.cs | 5 +- src/Shared/CommunicationsUtilities.cs | 105 ++++- src/Shared/INodePacket.cs | 18 + src/Shared/ITranslator.cs | 3 +- src/Shared/NamedPipeUtil.cs | 5 + src/Shared/NodeEndpointOutOfProcBase.cs | 9 +- 20 files changed, 926 insertions(+), 12 deletions(-) create mode 100644 src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs create mode 100644 src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs create mode 100644 src/Build/BackEnd/Node/ConsoleOutput.cs create mode 100644 src/Build/BackEnd/Node/OutOfProcServerNode.cs create mode 100644 src/Build/BackEnd/Node/ServerNamedMutex.cs create mode 100644 src/Build/BackEnd/Node/ServerNodeBuildCommand.cs create mode 100644 src/Build/BackEnd/Node/ServerNodeBuildResult.cs create mode 100644 src/Build/BackEnd/Node/ServerNodeConsoleWrite.cs 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(); From 3c24e9a54bbf75c5dea50a2a777f2e2dd489df01 Mon Sep 17 00:00:00 2001 From: Michal Pavlik Date: Mon, 25 Apr 2022 15:33:46 +0200 Subject: [PATCH 02/43] Removed IHanshake interface and removed duplicate code from ServerNodeHandshake. --- .../Communications/NodeEndpointOutOfProc.cs | 2 +- .../ServerNodeEndpointOutOfProc.cs | 6 +- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 4 +- src/MSBuild/NodeEndpointOutOfProcTaskHost.cs | 2 +- src/Shared/CommunicationsUtilities.cs | 96 ++++++------------- src/Shared/NodeEndpointOutOfProcBase.cs | 4 +- 6 files changed, 35 insertions(+), 79 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs b/src/Build/BackEnd/Components/Communications/NodeEndpointOutOfProc.cs index 6881debdae2..dce14e3d422 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 IHandshake GetHandshake() + protected override Handshake 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 index 528d27056da..0590a95c1ba 100644 --- a/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs +++ b/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs @@ -11,7 +11,7 @@ namespace Microsoft.Build.BackEnd /// internal sealed class ServerNodeEndpointOutOfProc : NodeEndpointOutOfProcBase { - private readonly IHandshake _handshake; + private readonly Handshake _handshake; /// /// Instantiates an endpoint to act as a client @@ -20,7 +20,7 @@ internal sealed class ServerNodeEndpointOutOfProc : NodeEndpointOutOfProcBase /// internal ServerNodeEndpointOutOfProc( string pipeName, - IHandshake handshake) + Handshake handshake) { _handshake = handshake; @@ -30,7 +30,7 @@ internal ServerNodeEndpointOutOfProc( /// /// Returns the host handshake for this node endpoint /// - protected override IHandshake GetHandshake() + protected override Handshake GetHandshake() { return _handshake; } diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 3d532f3506c..e6889579a10 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -85,10 +85,8 @@ public OutOfProcServerNode(Func buildFu /// 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); + CommunicationsUtilities.GetHandshakeOptions(taskHost: false, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture())); string pipeName = NamedPipeUtil.GetPipeNameOrPath("MSBuildServer-" + handshake.ComputeHash()); diff --git a/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs b/src/MSBuild/NodeEndpointOutOfProcTaskHost.cs index 0f1e1eacb05..36ea494b383 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 IHandshake GetHandshake() + protected override Handshake GetHandshake() { return new Handshake(CommunicationsUtilities.GetHandshakeOptions(taskHost: true)); } diff --git a/src/Shared/CommunicationsUtilities.cs b/src/Shared/CommunicationsUtilities.cs index 7e86ff4ee03..131a64a0786 100644 --- a/src/Shared/CommunicationsUtilities.cs +++ b/src/Shared/CommunicationsUtilities.cs @@ -77,34 +77,17 @@ internal enum HandshakeOptions Arm64 = 128, } - internal interface IHandshake + internal class Handshake { - 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; - readonly int fileVersionMajor; - readonly int fileVersionMinor; - readonly int fileVersionBuild; - readonly int fileVersionPrivate; - readonly int sessionId; - - internal Handshake(HandshakeOptions nodeType) + protected readonly int options; + protected readonly int salt; + protected readonly int fileVersionMajor; + protected readonly int fileVersionMinor; + protected readonly int fileVersionBuild; + protected readonly int fileVersionPrivate; + private readonly int sessionId; + + internal protected Handshake(HandshakeOptions nodeType) { const int handshakeVersion = (int)CommunicationsUtilities.handshakeVersion; @@ -132,7 +115,7 @@ public override string ToString() return String.Format("{0} {1} {2} {3} {4} {5} {6}", options, salt, fileVersionMajor, fileVersionMinor, fileVersionBuild, fileVersionPrivate, sessionId); } - public int[] RetrieveHandshakeComponents() + public virtual int[] RetrieveHandshakeComponents() { return new int[] { @@ -146,38 +129,20 @@ public int[] RetrieveHandshakeComponents() }; } - public string GetKey() => $"{options} {salt} {fileVersionMajor} {fileVersionMinor} {fileVersionBuild} {fileVersionPrivate} {sessionId}".ToString(CultureInfo.InvariantCulture); + public virtual string GetKey() => $"{options} {salt} {fileVersionMajor} {fileVersionMinor} {fileVersionBuild} {fileVersionPrivate} {sessionId}".ToString(CultureInfo.InvariantCulture); - public byte? ExpectedVersionInFirstByte => CommunicationsUtilities.handshakeVersion; + public virtual byte? ExpectedVersionInFirstByte => CommunicationsUtilities.handshakeVersion; } - internal sealed class ServerNodeHandshake : IHandshake + internal sealed class ServerNodeHandshake : Handshake { - readonly int _options; - readonly int _salt; - readonly int _fileVersionMajor; - readonly int _fileVersionMinor; - readonly int _fileVersionBuild; - readonly int _fileVersionRevision; - - internal ServerNodeHandshake(HandshakeOptions nodeType, string msBuildLocation) + public override byte? ExpectedVersionInFirstByte => null; + + internal ServerNodeHandshake(HandshakeOptions nodeType) + : base(nodeType) { - // 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 /// @@ -189,32 +154,25 @@ private static int ComputeHandshakeHash(string fromString) return BitConverter.ToInt32(bytes, 0); } - internal static int AvoidEndOfHandshakeSignal(int x) - { - return x == EndOfHandshakeSignal ? ~x : x; - } - - public int[] RetrieveHandshakeComponents() + public override int[] RetrieveHandshakeComponents() { return new int[] { - AvoidEndOfHandshakeSignal(_options), - AvoidEndOfHandshakeSignal(_salt), - AvoidEndOfHandshakeSignal(_fileVersionMajor), - AvoidEndOfHandshakeSignal(_fileVersionMinor), - AvoidEndOfHandshakeSignal(_fileVersionBuild), - AvoidEndOfHandshakeSignal(_fileVersionRevision), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(options), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(salt), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(fileVersionMajor), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(fileVersionMinor), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(fileVersionBuild), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(fileVersionPrivate), }; } - public string GetKey() + public override string GetKey() { - return $"{_options} {_salt} {_fileVersionMajor} {_fileVersionMinor} {_fileVersionBuild} {_fileVersionRevision}" + return $"{options} {salt} {fileVersionMajor} {fileVersionMinor} {fileVersionBuild} {fileVersionPrivate}" .ToString(CultureInfo.InvariantCulture); } - public byte? ExpectedVersionInFirstByte => null; - /// /// Computes Handshake stable hash string representing whole state of handshake. /// diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index f402ecac71a..9b15f5baf2f 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -246,7 +246,7 @@ internal void InternalConstruct(string pipeName = null) /// /// Returns the host handshake for this node endpoint /// - protected abstract IHandshake GetHandshake(); + protected abstract Handshake GetHandshake(); /// /// Updates the current link status if it has changed and notifies any registered delegates. @@ -374,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. - IHandshake handshake = GetHandshake(); + Handshake handshake = GetHandshake(); try { int[] handshakeComponents = handshake.RetrieveHandshakeComponents(); From 3be5f95d8a2915df2e03b6ebfda28e8b85621a9f Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Thu, 28 Apr 2022 13:19:14 +0200 Subject: [PATCH 03/43] [WIP-FEATURE] MSBuild client (#7540) Fixes #7374, #7373 Context MSBuild client is a new code path that is triggered with opt-in env variable. It sends the build request for execution to the MSBuild server node. This approach avoids to do execute targets and tasks into a short-living process from CLI tools like .NET SDK and MSBuild.exe. Changes Made This PR implements a new MSBuild client classes able to communicate with MSBuild server node via the named pipe. Testing Manually tested. Automatic tests will be added in another PR. Co-authored-by: Forgind Co-authored-by: Roman Konecny --- src/Build/BackEnd/Client/MSBuildClient.cs | 447 ++++++++++++++++++ .../BackEnd/Client/MSBuildClientExitResult.cs | 24 + .../BackEnd/Client/MSBuildClientExitType.cs | 33 ++ .../BackEnd/Client/MSBuildClientPacketPump.cs | 302 ++++++++++++ src/Build/BackEnd/Node/OutOfProcServerNode.cs | 4 +- src/Build/Microsoft.Build.csproj | 6 +- .../PublicAPI/net/PublicAPI.Unshipped.txt | 18 +- .../netstandard/PublicAPI.Unshipped.txt | 17 +- src/Framework/Traits.cs | 5 + src/MSBuild/MSBuild.csproj | 1 + src/MSBuild/MSBuildClientApp.cs | 153 ++++++ src/MSBuild/XMake.cs | 29 +- 12 files changed, 1031 insertions(+), 8 deletions(-) create mode 100644 src/Build/BackEnd/Client/MSBuildClient.cs create mode 100644 src/Build/BackEnd/Client/MSBuildClientExitResult.cs create mode 100644 src/Build/BackEnd/Client/MSBuildClientExitType.cs create mode 100644 src/Build/BackEnd/Client/MSBuildClientPacketPump.cs create mode 100644 src/MSBuild/MSBuildClientApp.cs diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs new file mode 100644 index 00000000000..8f2d33a8d56 --- /dev/null +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -0,0 +1,447 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using Microsoft.Build.BackEnd; +using Microsoft.Build.BackEnd.Client; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.Execution +{ + /// + /// This class is the public entry point for executing builds in msbuild server. + /// It processes command-line arguments and invokes the build engine. + /// + public sealed class MSBuildClient + { + /// + /// The build inherits all the environment variables from the client process. + /// This property allows to add extra environment variables or reset some of the existing ones. + /// + private readonly Dictionary _serverEnvironmentVariables; + + /// + /// Location of executable file to launch the server process. That should be either dotnet.exe or MSBuild.exe location. + /// + private readonly string _exeLocation; + + /// + /// Location of dll file to launch the server process if needed. Empty if executable is msbuild.exe and not empty if dotnet.exe. + /// + private readonly string _dllLocation; + + /// + /// The MSBuild client execution result. + /// + private readonly MSBuildClientExitResult _exitResult; + + /// + /// Whether MSBuild server finished the build. + /// + private bool _buildFinished = false; + + /// + /// Handshake between server and client. + /// + private readonly ServerNodeHandshake _handshake; + + /// + /// The named pipe name for client-server communication. + /// + private readonly string _pipeName; + + /// + /// The named pipe stream for client-server communication. + /// + private readonly NamedPipeClientStream _nodeStream; + + /// + /// A way to cache a byte array when writing out packets + /// + private readonly MemoryStream _packetMemoryStream; + + /// + /// A binary writer to help write into + /// + private readonly BinaryWriter _binaryWriter; + + /// + /// Public constructor with parameters. + /// + /// Location of executable file to launch the server process. + /// That should be either dotnet.exe or MSBuild.exe location. + /// Location of dll file to launch the server process if needed. + /// Empty if executable is msbuild.exe and not empty if dotnet.exe. + public MSBuildClient(string exeLocation, string dllLocation) + { + _serverEnvironmentVariables = new(); + _exitResult = new(); + + // dll & exe locations + _exeLocation = exeLocation; + _dllLocation = dllLocation; + + // Client <-> Server communication stream + _handshake = GetHandshake(); + _pipeName = OutOfProcServerNode.GetPipeName(_handshake); + _nodeStream = new NamedPipeClientStream(".", _pipeName, PipeDirection.InOut, PipeOptions.Asynchronous +#if FEATURE_PIPEOPTIONS_CURRENTUSERONLY + | PipeOptions.CurrentUserOnly +#endif + ); + + _packetMemoryStream = new MemoryStream(); + _binaryWriter = new BinaryWriter(_packetMemoryStream); + } + + /// + /// Orchestrates the execution of the build on the server, + /// responsible for client-server communication. + /// + /// The command line to process. The first argument + /// on the command line is assumed to be the name/path of the executable, and + /// is ignored. + /// Cancellation token. + /// A value of type that indicates whether the build succeeded, + /// or the manner in which it failed. + public MSBuildClientExitResult Execute(string commandLine, CancellationToken cancellationToken) + { + string serverRunningMutexName = $@"{ServerNamedMutex.RunningServerMutexNamePrefix}{_pipeName}"; + string serverBusyMutexName = $@"{ServerNamedMutex.BusyServerMutexNamePrefix}{_pipeName}"; + + // Start server it if is not running. + bool serverIsAlreadyRunning = ServerNamedMutex.WasOpen(serverRunningMutexName); + if (!serverIsAlreadyRunning && !TryLaunchServer()) + { + return _exitResult; + } + + // Check that server is not busy. + var serverWasBusy = ServerNamedMutex.WasOpen(serverBusyMutexName); + if (serverWasBusy) + { + CommunicationsUtilities.Trace("Server is busy, falling back to former behavior."); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.ServerBusy; + return _exitResult; + } + + // Connect to server. + if (!TryConnectToServer(serverIsAlreadyRunning ? 1_000 : 20_000)) + { + CommunicationsUtilities.Trace("Failure to connect to a server."); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; + return _exitResult; + } + + // Send build command. + // Let's send it outside the packet pump so that we easier and quicker deal with possible issues with connection to server. + if (!TrySendBuildCommand(commandLine)) + { + CommunicationsUtilities.Trace("Failure to connect to a server."); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; + return _exitResult; + } + + try + { + // Start packet pump + using MSBuildClientPacketPump packetPump = new(_nodeStream); + + packetPump.RegisterPacketHandler(NodePacketType.ServerNodeConsoleWrite, ServerNodeConsoleWrite.FactoryForDeserialization, packetPump); + packetPump.RegisterPacketHandler(NodePacketType.ServerNodeBuildResult, ServerNodeBuildResult.FactoryForDeserialization, packetPump); + packetPump.Start(); + + WaitHandle[] waitHandles = + { + cancellationToken.WaitHandle, + packetPump.PacketPumpErrorEvent, + packetPump.PacketReceivedEvent + }; + + while (!_buildFinished) + { + int index = WaitHandle.WaitAny(waitHandles); + switch (index) + { + case 0: + HandleCancellation(); + break; + + case 1: + HandlePacketPumpError(packetPump); + break; + + case 2: + while (packetPump.ReceivedPacketsQueue.TryDequeue(out INodePacket? packet) && + !_buildFinished && + !cancellationToken.IsCancellationRequested) + { + if (packet != null) + { + HandlePacket(packet); + } + } + + break; + } + } + } + catch (Exception ex) + { + CommunicationsUtilities.Trace("MSBuild client error: problem during packet handling occurred: {0}.", ex); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.Unexpected; + } + + CommunicationsUtilities.Trace("Build finished."); + return _exitResult; + } + + private void SendCancelCommand(NamedPipeClientStream nodeStream) => throw new NotImplementedException(); + + /// + /// Launches MSBuild server. + /// + /// Whether MSBuild server was started successfully. + private bool TryLaunchServer() + { + string serverLaunchMutexName = $@"Global\server-launch-{_pipeName}"; + using var serverLaunchMutex = ServerNamedMutex.OpenOrCreateMutex(serverLaunchMutexName, out bool mutexCreatedNew); + if (!mutexCreatedNew) + { + // Some other client process launching a server and setting a build request for it. Fallback to usual msbuild app build. + CommunicationsUtilities.Trace("Another process launching the msbuild server, falling back to former behavior."); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.ServerBusy; + return false; + } + + string[] msBuildServerOptions = new string[] { + _dllLocation, + "/nologo", + "/nodemode:8" + }; + + try + { + Process msbuildProcess = LaunchNode(_exeLocation, string.Join(" ", msBuildServerOptions), _serverEnvironmentVariables); + CommunicationsUtilities.Trace("Server is launched with PID: {0}", msbuildProcess.Id); + } + catch (Exception ex) + { + CommunicationsUtilities.Trace("Failed to launch the msbuild server: {0}", ex); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.LaunchError; + return false; + } + + return true; + } + + private Process LaunchNode(string exeLocation, string msBuildServerArguments, Dictionary serverEnvironmentVariables) + { + ProcessStartInfo processStartInfo = new() + { + FileName = exeLocation, + Arguments = msBuildServerArguments, + UseShellExecute = false + }; + + foreach (var entry in serverEnvironmentVariables) + { + processStartInfo.Environment[entry.Key] = entry.Value; + } + + // We remove env to enable MSBuild Server that might be equal to 1, so we do not get an infinite recursion here. + processStartInfo.Environment[Traits.UseMSBuildServerEnvVarName] = "0"; + + processStartInfo.CreateNoWindow = true; + processStartInfo.UseShellExecute = false; + + return Process.Start(processStartInfo) ?? throw new InvalidOperationException("MSBuild server node failed to launch."); + } + + private bool TrySendBuildCommand(string commandLine) + { + try + { + ServerNodeBuildCommand buildCommand = GetServerNodeBuildCommand(commandLine); + WritePacket(_nodeStream, buildCommand); + CommunicationsUtilities.Trace("Build command send..."); + } + catch (Exception ex) + { + CommunicationsUtilities.Trace("Failed to send build command to server: {0}", ex); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; + return false; + } + + return true; + } + + private ServerNodeBuildCommand GetServerNodeBuildCommand(string commandLine) + { + Dictionary envVars = new(); + + foreach (DictionaryEntry envVar in Environment.GetEnvironmentVariables()) + { + envVars[(string)envVar.Key] = (envVar.Value as string) ?? string.Empty; + } + + foreach (var pair in _serverEnvironmentVariables) + { + envVars[pair.Key] = pair.Value; + } + + // We remove env variable used to invoke MSBuild server as that might be equal to 1, so we do not get an infinite recursion here. + envVars[Traits.UseMSBuildServerEnvVarName] = "0"; + + return new ServerNodeBuildCommand( + commandLine, + startupDirectory: Directory.GetCurrentDirectory(), + buildProcessEnvironment: envVars, + CultureInfo.CurrentCulture, + CultureInfo.CurrentUICulture); + } + + private ServerNodeHandshake GetHandshake() + { + return new ServerNodeHandshake(CommunicationsUtilities.GetHandshakeOptions(taskHost: false, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture())); + } + + /// + /// Handle cancellation. + /// + private void HandleCancellation() + { + // TODO. + // Send cancellation command to server. + // SendCancelCommand(_nodeStream); + + Console.WriteLine("MSBuild client cancelled."); + CommunicationsUtilities.Trace("MSBuild client cancelled."); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.Cancelled; + _buildFinished = true; + } + + /// + /// Handle packet pump error. + /// + private void HandlePacketPumpError(MSBuildClientPacketPump packetPump) + { + CommunicationsUtilities.Trace("MSBuild client error: packet pump unexpectedly shut down: {0}", packetPump.PacketPumpException); + throw packetPump.PacketPumpException ?? new Exception("Packet pump unexpectedly shut down"); + } + + /// + /// Dispatches the packet to the correct handler. + /// + private void HandlePacket(INodePacket packet) + { + switch (packet.Type) + { + case NodePacketType.ServerNodeConsoleWrite: + HandleServerNodeConsoleWrite((ServerNodeConsoleWrite)packet); + break; + case NodePacketType.ServerNodeBuildResult: + HandleServerNodeBuildResult((ServerNodeBuildResult)packet); + break; + default: throw new InvalidOperationException($"Unexpected packet type {packet.GetType().Name}"); + } + } + + private void HandleServerNodeConsoleWrite(ServerNodeConsoleWrite consoleWrite) + { + switch (consoleWrite.OutputType) + { + case ConsoleOutput.Standard: + Console.Write(consoleWrite.Text); + break; + case ConsoleOutput.Error: + Console.Error.Write(consoleWrite.Text); + break; + default: + throw new InvalidOperationException($"Unexpected console output type {consoleWrite.OutputType}"); + } + } + + private void HandleServerNodeBuildResult(ServerNodeBuildResult response) + { + CommunicationsUtilities.Trace("Build response received: exit code {0}, exit type '{1}'", response.ExitCode, response.ExitType); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.Success; + _exitResult.MSBuildAppExitTypeString = response.ExitType; + _buildFinished = true; + } + + /// + /// Connects to MSBuild server. + /// + /// Whether the client connected to MSBuild server successfully. + private bool TryConnectToServer(int timeout) + { + try + { + _nodeStream.Connect(timeout); + + int[] handshakeComponents = _handshake.RetrieveHandshakeComponents(); + for (int i = 0; i < handshakeComponents.Length; i++) + { + CommunicationsUtilities.Trace("Writing handshake part {0} ({1}) to pipe {2}", i, handshakeComponents[i], _pipeName); + _nodeStream.WriteIntForHandshake(handshakeComponents[i]); + } + + // This indicates that we have finished all the parts of our handshake; hopefully the endpoint has as well. + _nodeStream.WriteEndOfHandshakeSignal(); + + CommunicationsUtilities.Trace("Reading handshake from pipe {0}", _pipeName); + +#if NETCOREAPP2_1_OR_GREATER || MONO + _nodeStream.ReadEndOfHandshakeSignal(false, 1000); +#else + _nodeStream.ReadEndOfHandshakeSignal(false); +#endif + + CommunicationsUtilities.Trace("Successfully connected to pipe {0}...!", _pipeName); + } + catch (Exception ex) + { + CommunicationsUtilities.Trace("Failed to connect to server: {0}", ex); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; + return false; + } + + return true; + } + + private void WritePacket(Stream nodeStream, INodePacket packet) + { + MemoryStream memoryStream = _packetMemoryStream; + memoryStream.SetLength(0); + + ITranslator writeTranslator = BinaryTranslator.GetWriteTranslator(memoryStream); + + // Write header + memoryStream.WriteByte((byte)packet.Type); + + // Pad for packet length + _binaryWriter.Write(0); + + // Reset the position in the write buffer. + packet.Translate(writeTranslator); + + int packetStreamLength = (int)memoryStream.Position; + + // Now write in the actual packet length + memoryStream.Position = 1; + _binaryWriter.Write(packetStreamLength - 5); + + nodeStream.Write(memoryStream.GetBuffer(), 0, packetStreamLength); + } + } +} diff --git a/src/Build/BackEnd/Client/MSBuildClientExitResult.cs b/src/Build/BackEnd/Client/MSBuildClientExitResult.cs new file mode 100644 index 00000000000..648e755f002 --- /dev/null +++ b/src/Build/BackEnd/Client/MSBuildClientExitResult.cs @@ -0,0 +1,24 @@ +// 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.Execution +{ + /// + /// Enumeration of the various ways in which the MSBuildClient execution can exit. + /// + public sealed class MSBuildClientExitResult + { + /// + /// The MSBuild client exit type. + /// Covers different ways MSBuild client execution can finish. + /// Build errors are not included. The client could finish successfully and the build at the same time could result in a build error. + /// + public MSBuildClientExitType MSBuildClientExitType { get; set; } + + /// + /// The build exit type. Possible values: MSBuildApp.ExitType serialized into a string. + /// This field is null if MSBuild client execution was not successful. + /// + public string? MSBuildAppExitTypeString { get; set; } + } +} diff --git a/src/Build/BackEnd/Client/MSBuildClientExitType.cs b/src/Build/BackEnd/Client/MSBuildClientExitType.cs new file mode 100644 index 00000000000..70bbc0113c8 --- /dev/null +++ b/src/Build/BackEnd/Client/MSBuildClientExitType.cs @@ -0,0 +1,33 @@ +// 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.Execution +{ + public enum MSBuildClientExitType + { + /// + /// The MSBuild client successfully processed the build request. + /// + Success, + /// + /// Server is busy. + /// + ServerBusy, + /// + /// Client was unable to connect to the server. + /// + ConnectionError, + /// + /// Client was unable to launch the server. + /// + LaunchError, + /// + /// The build stopped unexpectedly, for example, + /// because a named pipe between the server and the client was unexpectedly closed. + /// + Unexpected, + /// + /// The build was cancelled. + /// + Cancelled + } +} diff --git a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs new file mode 100644 index 00000000000..c2402156971 --- /dev/null +++ b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +#if !FEATURE_APM +using System.Threading.Tasks; +#endif + +namespace Microsoft.Build.BackEnd.Client +{ + internal sealed class MSBuildClientPacketPump : INodePacketHandler, INodePacketFactory, IDisposable + { + /// + /// The queue of packets we have received but which have not yet been processed. + /// + public ConcurrentQueue ReceivedPacketsQueue { get; } + + /// + /// Set when packet pump receive packets and put them to . + /// + public AutoResetEvent PacketReceivedEvent { get; } + + /// + /// Set when the packet pump unexpectedly terminates (due to connection problems or because of deserialization issues). + /// + public ManualResetEvent PacketPumpErrorEvent { get; } + + /// + /// Exception appeared when the packet pump unexpectedly terminates. + /// + public Exception? PacketPumpException { get; set; } + + /// + /// Set when packet pump should shutdown. + /// + private readonly ManualResetEvent _packetPumpShutdownEvent; + + /// + /// The packet factory. + /// + private readonly NodePacketFactory _packetFactory; + + /// + /// The memory stream for a read buffer. + /// + private readonly MemoryStream _readBufferMemoryStream; + + /// + /// The thread which runs the asynchronous packet pump + /// + private Thread? _packetPumpThread; + + /// + /// The stream from where to read packets. + /// + private readonly Stream _stream; + + /// + /// The binary translator for reading packets. + /// + readonly ITranslator _binaryReadTranslator; + + public MSBuildClientPacketPump(Stream stream) + { + _stream = stream; + _packetFactory = new NodePacketFactory(); + + ReceivedPacketsQueue = new ConcurrentQueue(); + PacketReceivedEvent = new AutoResetEvent(false); + PacketPumpErrorEvent = new ManualResetEvent(false); + _packetPumpShutdownEvent = new ManualResetEvent(false); + + _readBufferMemoryStream = new MemoryStream(); + _binaryReadTranslator = BinaryTranslator.GetReadTranslator(_readBufferMemoryStream, InterningBinaryReader.CreateSharedBuffer()); + } + + #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. + public void 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. + public void 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. + public void 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. + public void 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. + public void PacketReceived(int node, INodePacket packet) + { + ReceivedPacketsQueue.Enqueue(packet); + PacketReceivedEvent.Set(); + } + + #endregion + + #region Packet Pump + /// + /// Initializes the packet pump thread. + /// + public void Start() + { + _packetPumpThread = new Thread(PacketPumpProc) + { + IsBackground = true, + Name = "MSBuild Client Packet Pump" + }; + _packetPumpThread.Start(); + } + + /// + /// Stops the packet pump thread. + /// + public void Stop() + { + _packetPumpShutdownEvent.Set(); + _packetPumpThread?.Join(); + } + + /// + /// This method handles the packet pump reading. It will terminate when the terminate event is + /// set. + /// + /// + /// Instead of throwing an exception, puts it in and raises event . + /// + private void PacketPumpProc() + { + RunReadLoop(_stream, _packetPumpShutdownEvent); + } + + private void RunReadLoop(Stream localStream, ManualResetEvent localPacketPumpShutdownEvent) + { + CommunicationsUtilities.Trace("Entering read loop."); + + try + { + byte[] headerByte = new byte[5]; +#if FEATURE_APM + IAsyncResult result = localStream.BeginRead(headerByte, 0, headerByte.Length, null, null); +#else + Task readTask = CommunicationsUtilities.ReadAsync(localStream, headerByte, headerByte.Length); +#endif + + bool continueReading = true; + do + { + // Ordering of the wait handles is important. The first signalled wait handle in the array + // will be returned by WaitAny if multiple wait handles are signalled. We prefer to have the + // terminate event triggered so that we cannot get into a situation where packets are being + // spammed to the client and it never gets an opportunity to shutdown. + WaitHandle[] handles = new WaitHandle[] { + localPacketPumpShutdownEvent, +#if FEATURE_APM + result.AsyncWaitHandle +#else + ((IAsyncResult)readTask).AsyncWaitHandle +#endif + }; + int waitId = WaitHandle.WaitAny(handles); + switch (waitId) + { + case 0: + // Fulfill the request for shutdown of the message pump. + CommunicationsUtilities.Trace("Shutdown message pump thread."); + continueReading = false; + break; + + case 1: + { + // Client recieved a packet header. Read the rest of a package. + int headerBytesRead = 0; +#if FEATURE_APM + headerBytesRead = localStream.EndRead(result); +#else + headerBytesRead = readTask.Result; +#endif + + if ((headerBytesRead != headerByte.Length) && !localPacketPumpShutdownEvent.WaitOne(0)) + { + // Incomplete read. Abort. + if (headerBytesRead == 0) + { + ErrorUtilities.ThrowInternalError("Server disconnected abruptly"); + } + else + { + ErrorUtilities.ThrowInternalError("Incomplete header read from server. {0} of {1} bytes read", headerBytesRead, headerByte.Length); + } + } + + NodePacketType packetType = (NodePacketType)Enum.ToObject(typeof(NodePacketType), headerByte[0]); + + int packetLength = BinaryPrimitives.ReadInt32LittleEndian(new Span(headerByte, 1, 4)); + int packetBytesRead = 0; + + _readBufferMemoryStream.Position = 0; + _readBufferMemoryStream.SetLength(packetLength); + byte[] packetData = _readBufferMemoryStream.GetBuffer(); + + packetBytesRead = localStream.Read(packetData, 0, packetLength); + + if (packetBytesRead != packetLength) + { + // Incomplete read. Abort. + ErrorUtilities.ThrowInternalError("Incomplete header read from server. {0} of {1} bytes read", headerBytesRead, headerByte.Length); + } + + try + { + _packetFactory.DeserializeAndRoutePacket(0, packetType, _binaryReadTranslator); + } + catch + { + // Error while deserializing or handling packet. Logging additional info. + CommunicationsUtilities.Trace("Packet factory failed to receive package. Exception while deserializing packet {0}.", packetType); + throw; + } + + if (packetType == NodePacketType.ServerNodeBuildResult) + { + continueReading = false; + } + else + { + // Start reading the next package header. +#if FEATURE_APM + result = localStream.BeginRead(headerByte, 0, headerByte.Length, null, null); +#else + readTask = CommunicationsUtilities.ReadAsync(localStream, headerByte, headerByte.Length); +#endif + } + } + break; + + default: + ErrorUtilities.ThrowInternalError("WaitId {0} out of range.", waitId); + break; + } + } + while (continueReading); + } + catch (Exception ex) + { + CommunicationsUtilities.Trace("Exception occurred in the packet pump: {0}", ex); + PacketPumpException = ex; + PacketPumpErrorEvent.Set(); + } + + CommunicationsUtilities.Trace("Ending read loop."); + } + #endregion + + public void Dispose() => Stop(); + } +} diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index e6889579a10..eb8edeec7dc 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -88,7 +88,7 @@ public NodeEngineShutdownReason Run(out Exception? shutdownException) var handshake = new ServerNodeHandshake( CommunicationsUtilities.GetHandshakeOptions(taskHost: false, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture())); - string pipeName = NamedPipeUtil.GetPipeNameOrPath("MSBuildServer-" + handshake.ComputeHash()); + string pipeName = GetPipeName(handshake); string serverRunningMutexName = $@"{ServerNamedMutex.RunningServerMutexNamePrefix}{pipeName}"; _serverBusyMutexName = $@"{ServerNamedMutex.BusyServerMutexNamePrefix}{pipeName}"; @@ -137,6 +137,8 @@ public NodeEngineShutdownReason Run(out Exception? shutdownException) #endregion + internal static string GetPipeName(ServerNodeHandshake handshake) => NamedPipeUtil.GetPipeNameOrPath("MSBuildServer-" + handshake.ComputeHash()); + #region INodePacketFactory Members /// diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 0bdc0df6561..d14821f787f 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -1,4 +1,4 @@ - + @@ -142,6 +142,10 @@ + + + + diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index 6a1467598ac..b7e25f06956 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,3 +1,19 @@ +Microsoft.Build.Execution.MSBuildClient +Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult +Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void +Microsoft.Build.Execution.MSBuildClientExitResult +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitResult() -> void +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.set -> void +Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.Cancelled = 5 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Execution.MSBuildClientExitType 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 +Microsoft.Build.Execution.OutOfProcServerNode.Run(out System.Exception shutdownException) -> Microsoft.Build.Execution.NodeEngineShutdownReason \ No newline at end of file diff --git a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 6cdbdc2bcc1..b7e25f06956 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,4 +1,19 @@ - +Microsoft.Build.Execution.MSBuildClient +Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult +Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void +Microsoft.Build.Execution.MSBuildClientExitResult +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitResult() -> void +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.set -> void +Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.Cancelled = 5 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Execution.MSBuildClientExitType +Microsoft.Build.Execution.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Execution.MSBuildClientExitType 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/Framework/Traits.cs b/src/Framework/Traits.cs index cf60eb140c9..eb12d904b42 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -102,6 +102,11 @@ public Traits() /// public readonly int DictionaryBasedItemRemoveThreshold = ParseIntFromEnvironmentVariableOrDefault("MSBUILDDICTIONARYBASEDITEMREMOVETHRESHOLD", 100); + /// + /// Name of environment variables used to enable MSBuild server. + /// + public const string UseMSBuildServerEnvVarName = "MSBUILDUSESERVER"; + public readonly bool DebugEngine = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildDebugEngine")); public readonly bool DebugScheduler; public readonly bool DebugNodeCommunication; diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 9ea8cc2fcf8..958815d4673 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -174,6 +174,7 @@ true + diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs new file mode 100644 index 00000000000..de83c3a10e3 --- /dev/null +++ b/src/MSBuild/MSBuildClientApp.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using Microsoft.Build.Execution; +using Microsoft.Build.Shared; +using System.Threading; + +#if RUNTIME_TYPE_NETCORE || MONO +using System.IO; +using System.Diagnostics; +#endif + +namespace Microsoft.Build.CommandLine +{ + /// + /// This class implements client for MSBuild server. It + /// 1. starts the MSBuild server in a separate process if it does not yet exist. + /// 2. establishes a connection with MSBuild server and sends a build request. + /// 3. if server is busy, it falls back to old build behavior. + /// + internal static class MSBuildClientApp + { + /// + /// This is the entry point for the MSBuild client. + /// + /// The command line to process. The first argument + /// on the command line is assumed to be the name/path of the executable, and + /// is ignored. + /// Cancellation token. + /// A value of type that indicates whether the build succeeded, + /// or the manner in which it failed. + /// + /// The locations of msbuild exe/dll and dotnet.exe would be automatically detected if called from dotnet or msbuild cli. Calling this function from other executables might not work. + /// + public static MSBuildApp.ExitType Execute( +#if FEATURE_GET_COMMANDLINE + string commandLine, +#else + string[] commandLine, +#endif + CancellationToken cancellationToken + ) + { + string? exeLocation; + string? dllLocation; + +#if RUNTIME_TYPE_NETCORE || MONO + // Run the child process with the same host as the currently-running process. + // Mono automatically uses the current mono, to execute a managed assembly. + if (!NativeMethodsShared.IsMono) + { + // _exeFileLocation consists the msbuild dll instead. + dllLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; + exeLocation = GetCurrentHost(); + } + else + { + // _exeFileLocation consists the msbuild dll instead. + exeLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; + dllLocation = String.Empty; + } +#else + exeLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; + dllLocation = String.Empty; +#endif + + return Execute( + commandLine, + cancellationToken, + exeLocation, + dllLocation + ); + } + + /// + /// This is the entry point for the MSBuild client. + /// + /// The command line to process. The first argument + /// on the command line is assumed to be the name/path of the executable, and + /// is ignored. + /// Cancellation token. + /// Location of executable file to launch the server process. + /// That should be either dotnet.exe or MSBuild.exe location. + /// Location of dll file to launch the server process if needed. + /// Empty if executable is msbuild.exe and not empty if dotnet.exe. + /// A value of type that indicates whether the build succeeded, + /// or the manner in which it failed. + public static MSBuildApp.ExitType Execute( +#if FEATURE_GET_COMMANDLINE + string commandLine, +#else + string[] commandLine, +#endif + CancellationToken cancellationToken, + string exeLocation, + string dllLocation + ) + { + // MSBuild client orchestration. +#if !FEATURE_GET_COMMANDLINE + string commandLineString = string.Join(" ", commandLine); +#else + string commandLineString = commandLine; +#endif + MSBuildClient msbuildClient = new MSBuildClient(exeLocation, dllLocation); + MSBuildClientExitResult exitResult = msbuildClient.Execute(commandLineString, cancellationToken); + + if (exitResult.MSBuildClientExitType == MSBuildClientExitType.ServerBusy || + exitResult.MSBuildClientExitType == MSBuildClientExitType.ConnectionError) + { + // Server is busy, fallback to old behavior. + return MSBuildApp.Execute(commandLine); + } + + if (exitResult.MSBuildClientExitType == MSBuildClientExitType.Success && + Enum.TryParse(exitResult.MSBuildAppExitTypeString, out MSBuildApp.ExitType MSBuildAppExitType)) + { + // The client successfully set up a build task for MSBuild server and received the result. + // (Which could be a failure as well). Return the received exit type. + return MSBuildAppExitType; + } + + return MSBuildApp.ExitType.MSBuildClientFailure; + } + + // Copied from NodeProviderOutOfProcBase.cs +#if RUNTIME_TYPE_NETCORE || MONO + private static string? CurrentHost; + private static string GetCurrentHost() + { + if (CurrentHost == null) + { + string dotnetExe = Path.Combine(FileUtilities.GetFolderAbove(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, 2), + NativeMethodsShared.IsWindows ? "dotnet.exe" : "dotnet"); + if (File.Exists(dotnetExe)) + { + CurrentHost = dotnetExe; + } + else + { + using (Process currentProcess = Process.GetCurrentProcess()) + { + CurrentHost = currentProcess.MainModule?.FileName ?? throw new InvalidOperationException("Failed to retrieve process executable."); + } + } + } + + return CurrentHost; + } +#endif + } +} diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 83c62ace30a..129df900912 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -84,7 +84,12 @@ public enum ExitType /// /// A project cache failed unexpectedly. /// - ProjectCacheFailure + ProjectCacheFailure, + /// + /// The client for MSBuild server failed unexpectedly, for example, + /// because the server process died or hung. + /// + MSBuildClientFailure } /// @@ -216,14 +221,30 @@ string[] args DumpCounters(true /* initialize only */); } - // return 0 on success, non-zero on failure - int exitCode = ((s_initialized && Execute( + int exitCode; + if (Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1") + { + // Use the client app to execute build in msbuild server. Opt-in feature. + exitCode = ((s_initialized && MSBuildClientApp.Execute( +#if FEATURE_GET_COMMANDLINE + Environment.CommandLine, +#else + ConstructArrayArg(args), +#endif + s_buildCancellationSource.Token + ) == ExitType.Success) ? 0 : 1); + } + else + { + // return 0 on success, non-zero on failure + exitCode = ((s_initialized && Execute( #if FEATURE_GET_COMMANDLINE Environment.CommandLine #else ConstructArrayArg(args) #endif - ) == ExitType.Success) ? 0 : 1); + ) == ExitType.Success) ? 0 : 1); + } if (Environment.GetEnvironmentVariable("MSBUILDDUMPPROCESSCOUNTERS") == "1") { From cdacf78b5968eea55d5e15f4969ecfc54dafa118 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Tue, 3 May 2022 14:45:09 +0200 Subject: [PATCH 04/43] Change server mutex name generationt to support posix --- src/Build/BackEnd/Client/MSBuildClient.cs | 6 ++--- .../NodeProviderOutOfProcBase.cs | 2 +- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 26 +++++++++++-------- src/Build/BackEnd/Node/ServerNamedMutex.cs | 3 --- src/Shared/CommunicationsUtilities.cs | 21 ++++++++++----- src/Shared/NamedPipeUtil.cs | 6 ++--- src/Shared/NodeEndpointOutOfProcBase.cs | 2 +- 7 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 8f2d33a8d56..6ad55123bc3 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -115,8 +115,8 @@ public MSBuildClient(string exeLocation, string dllLocation) /// or the manner in which it failed. public MSBuildClientExitResult Execute(string commandLine, CancellationToken cancellationToken) { - string serverRunningMutexName = $@"{ServerNamedMutex.RunningServerMutexNamePrefix}{_pipeName}"; - string serverBusyMutexName = $@"{ServerNamedMutex.BusyServerMutexNamePrefix}{_pipeName}"; + string serverRunningMutexName = OutOfProcServerNode.GetRunningServerMutexName(_handshake); + string serverBusyMutexName = OutOfProcServerNode.GetBusyServerMutexName(_handshake); // Start server it if is not running. bool serverIsAlreadyRunning = ServerNamedMutex.WasOpen(serverRunningMutexName); @@ -213,7 +213,7 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can /// Whether MSBuild server was started successfully. private bool TryLaunchServer() { - string serverLaunchMutexName = $@"Global\server-launch-{_pipeName}"; + string serverLaunchMutexName = $@"Global\server-launch-{_handshake.ComputeHash()}"; using var serverLaunchMutex = ServerNamedMutex.OpenOrCreateMutex(serverLaunchMutexName, out bool mutexCreatedNew); if (!mutexCreatedNew) { diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index ec334e698b5..b667e7a60f2 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -378,7 +378,7 @@ private void ValidateRemotePipeSecurityOnWindows(NamedPipeClientStream nodeStrea private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake handshake) { // Try and connect to the process. - string pipeName = NamedPipeUtil.GetPipeNameOrPath(nodeProcessId); + string pipeName = NamedPipeUtil.GetPlatformSpecificPipeName(nodeProcessId); NamedPipeClientStream nodeStream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous #if FEATURE_PIPEOPTIONS_CURRENTUSERONLY diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index eb8edeec7dc..25b39f379be 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -77,32 +77,29 @@ public OutOfProcServerNode(Func buildFu } #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 exception which caused shutdown, if any. /// The reason for shutting down. public NodeEngineShutdownReason Run(out Exception? shutdownException) { var handshake = new ServerNodeHandshake( CommunicationsUtilities.GetHandshakeOptions(taskHost: false, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture())); - string pipeName = GetPipeName(handshake); - - string serverRunningMutexName = $@"{ServerNamedMutex.RunningServerMutexNamePrefix}{pipeName}"; - _serverBusyMutexName = $@"{ServerNamedMutex.BusyServerMutexNamePrefix}{pipeName}"; + _serverBusyMutexName = GetBusyServerMutexName(handshake); - // 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); + // 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. + using var serverRunningMutex = ServerNamedMutex.OpenOrCreateMutex(GetRunningServerMutexName(handshake), out bool mutexCreatedNew); if (!mutexCreatedNew) { shutdownException = new InvalidOperationException("MSBuild server is already running!"); return NodeEngineShutdownReason.Error; } - _nodeEndpoint = new ServerNodeEndpointOutOfProc(pipeName, handshake); + _nodeEndpoint = new ServerNodeEndpointOutOfProc(GetPipeName(handshake), handshake); _nodeEndpoint.OnLinkStatusChanged += OnLinkStatusChanged; _nodeEndpoint.Listen(this); @@ -137,7 +134,14 @@ public NodeEngineShutdownReason Run(out Exception? shutdownException) #endregion - internal static string GetPipeName(ServerNodeHandshake handshake) => NamedPipeUtil.GetPipeNameOrPath("MSBuildServer-" + handshake.ComputeHash()); + internal static string GetPipeName(ServerNodeHandshake handshake) + => NamedPipeUtil.GetPlatformSpecificPipeName($"MSBuildServer-{handshake.ComputeHash()}"); + + internal static string GetRunningServerMutexName(ServerNodeHandshake handshake) + => $@"Global\server-running-{handshake.ComputeHash()}"; + + internal static string GetBusyServerMutexName(ServerNodeHandshake handshake) + => $@"Global\server-busy-{handshake.ComputeHash()}"; #region INodePacketFactory Members diff --git a/src/Build/BackEnd/Node/ServerNamedMutex.cs b/src/Build/BackEnd/Node/ServerNamedMutex.cs index e149cda704b..2d6ab100d10 100644 --- a/src/Build/BackEnd/Node/ServerNamedMutex.cs +++ b/src/Build/BackEnd/Node/ServerNamedMutex.cs @@ -8,9 +8,6 @@ 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; } diff --git a/src/Shared/CommunicationsUtilities.cs b/src/Shared/CommunicationsUtilities.cs index 131a64a0786..50e34e2bc2a 100644 --- a/src/Shared/CommunicationsUtilities.cs +++ b/src/Shared/CommunicationsUtilities.cs @@ -136,6 +136,11 @@ public virtual int[] RetrieveHandshakeComponents() internal sealed class ServerNodeHandshake : Handshake { + /// + /// Caching computed hash. + /// + private string _computedHash = null; + public override byte? ExpectedVersionInFirstByte => null; internal ServerNodeHandshake(HandshakeOptions nodeType) @@ -178,12 +183,16 @@ public override string GetKey() /// 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); + if (_computedHash == null) + { + var input = GetKey(); + using var sha = SHA256.Create(); + var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(input)); + _computedHash = Convert.ToBase64String(bytes) + .Replace("/", "_") + .Replace("=", string.Empty); + } + return _computedHash; } } diff --git a/src/Shared/NamedPipeUtil.cs b/src/Shared/NamedPipeUtil.cs index 9db07e16722..dfc76317e84 100644 --- a/src/Shared/NamedPipeUtil.cs +++ b/src/Shared/NamedPipeUtil.cs @@ -8,7 +8,7 @@ namespace Microsoft.Build.Shared { internal static class NamedPipeUtil { - internal static string GetPipeNameOrPath(int? processId = null) + internal static string GetPlatformSpecificPipeName(int? processId = null) { if (processId is null) { @@ -17,10 +17,10 @@ internal static string GetPipeNameOrPath(int? processId = null) string pipeName = $"MSBuild{processId}"; - return GetPipeNameOrPath(pipeName); + return GetPlatformSpecificPipeName(pipeName); } - internal static string GetPipeNameOrPath(string pipeName) + internal static string GetPlatformSpecificPipeName(string pipeName) { if (NativeMethodsShared.IsUnixLike) { diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 9b15f5baf2f..6477869dc05 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -195,7 +195,7 @@ internal void InternalConstruct(string pipeName = null) _packetStream = new MemoryStream(); _binaryWriter = new BinaryWriter(_packetStream); - pipeName ??= NamedPipeUtil.GetPipeNameOrPath(); + pipeName ??= NamedPipeUtil.GetPlatformSpecificPipeName(); #if FEATURE_PIPE_SECURITY && FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR if (!NativeMethodsShared.IsMono) From e0a7bc8adb1df733b4e30ac5c15f8d4e511be11e Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Mon, 9 May 2022 04:54:55 -0700 Subject: [PATCH 05/43] Fix appending FORCECONSOLECOLOR log parametr --- src/MSBuild/XMake.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 129df900912..f3665f0c80b 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -3081,6 +3081,12 @@ internal static string AggregateParameters(string anyPrefixingParameter, string[ // Join the logger parameters into one string separated by semicolons string result = anyPrefixingParameter ?? string.Empty; + // Ensure traling ';' so parametersToAggregate are properly separated + if (!string.IsNullOrEmpty(result) && result[result.Length - 1] != ';') + { + result += ';'; + } + result += string.Join(";", parametersToAggregate); return result; From f97f022e03df0f8b697290d3eb26882e06a6b74c Mon Sep 17 00:00:00 2001 From: Forgind Date: Mon, 16 May 2022 04:58:00 -0700 Subject: [PATCH 06/43] Server instrumentation (#7602) * Some instrumentation * Add more details to ETW * Use class-wide variables --- documentation/specs/event-source.md | 1 + src/Build/BackEnd/Client/MSBuildClient.cs | 31 ++++++++++++++++--- .../BackEnd/Client/MSBuildClientPacketPump.cs | 2 +- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 4 ++- src/Framework/MSBuildEventSource.cs | 12 +++++++ 5 files changed, 43 insertions(+), 7 deletions(-) diff --git a/documentation/specs/event-source.md b/documentation/specs/event-source.md index 18936da2249..cf482946781 100644 --- a/documentation/specs/event-source.md +++ b/documentation/specs/event-source.md @@ -21,6 +21,7 @@ EventSource is primarily used to profile code. For MSBuild specifically, a major | GenerateResourceOverall | Uses resource APIs to transform resource files into strongly-typed resource classes. | | LoadDocument | Loads an XMLDocumentWithLocation from a path. | MSBuildExe | Executes MSBuild from the command line. | +| MSBuildServerBuild | Executes a build from the MSBuildServer node. | | PacketReadSize | Reports the size of a packet sent between nodes. Note that this does not include time information. | | Parse | Parses an XML document into a ProjectRootElement. | | ProjectGraphConstruction | Constructs a dependency graph among projects. | diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 6ad55123bc3..57c19c4a361 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -11,6 +11,7 @@ using System.Threading; using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.Client; +using Microsoft.Build.Eventing; using Microsoft.Build.Framework; using Microsoft.Build.Internal; using Microsoft.Build.Shared; @@ -74,6 +75,12 @@ public sealed class MSBuildClient /// private readonly BinaryWriter _binaryWriter; + /// + /// Used to estimate the size of the build with an ETW trace. + /// + private int _numConsoleWritePackets; + private long _sizeOfConsoleWritePackets; + /// /// Public constructor with parameters. /// @@ -115,14 +122,19 @@ public MSBuildClient(string exeLocation, string dllLocation) /// or the manner in which it failed. public MSBuildClientExitResult Execute(string commandLine, CancellationToken cancellationToken) { + CommunicationsUtilities.Trace("Executing build with command line '{0}'", commandLine); string serverRunningMutexName = OutOfProcServerNode.GetRunningServerMutexName(_handshake); string serverBusyMutexName = OutOfProcServerNode.GetBusyServerMutexName(_handshake); // Start server it if is not running. bool serverIsAlreadyRunning = ServerNamedMutex.WasOpen(serverRunningMutexName); - if (!serverIsAlreadyRunning && !TryLaunchServer()) + if (!serverIsAlreadyRunning) { - return _exitResult; + CommunicationsUtilities.Trace("Server was not running. Starting server now."); + if (!TryLaunchServer()) + { + return _exitResult; + } } // Check that server is not busy. @@ -144,6 +156,7 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can // Send build command. // Let's send it outside the packet pump so that we easier and quicker deal with possible issues with connection to server. + MSBuildEventSource.Log.MSBuildServerBuildStart(commandLine); if (!TrySendBuildCommand(commandLine)) { CommunicationsUtilities.Trace("Failure to connect to a server."); @@ -151,6 +164,9 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can return _exitResult; } + _numConsoleWritePackets = 0; + _sizeOfConsoleWritePackets = 0; + try { // Start packet pump @@ -201,6 +217,7 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can _exitResult.MSBuildClientExitType = MSBuildClientExitType.Unexpected; } + MSBuildEventSource.Log.MSBuildServerBuildStop(commandLine, _numConsoleWritePackets, _sizeOfConsoleWritePackets, _exitResult.MSBuildClientExitType.ToString(), _exitResult.MSBuildAppExitTypeString); CommunicationsUtilities.Trace("Build finished."); return _exitResult; } @@ -245,7 +262,8 @@ private bool TryLaunchServer() } private Process LaunchNode(string exeLocation, string msBuildServerArguments, Dictionary serverEnvironmentVariables) - { + { + CommunicationsUtilities.Trace("Launching server node from {0} with arguments {1}", exeLocation, msBuildServerArguments); ProcessStartInfo processStartInfo = new() { FileName = exeLocation, @@ -273,7 +291,7 @@ private bool TrySendBuildCommand(string commandLine) { ServerNodeBuildCommand buildCommand = GetServerNodeBuildCommand(commandLine); WritePacket(_nodeStream, buildCommand); - CommunicationsUtilities.Trace("Build command send..."); + CommunicationsUtilities.Trace("Build command sent..."); } catch (Exception ex) { @@ -347,7 +365,10 @@ private void HandlePacket(INodePacket packet) switch (packet.Type) { case NodePacketType.ServerNodeConsoleWrite: - HandleServerNodeConsoleWrite((ServerNodeConsoleWrite)packet); + ServerNodeConsoleWrite writePacket = (packet as ServerNodeConsoleWrite)!; + HandleServerNodeConsoleWrite(writePacket); + _numConsoleWritePackets++; + _sizeOfConsoleWritePackets += writePacket.Text.Length; break; case NodePacketType.ServerNodeBuildResult: HandleServerNodeBuildResult((ServerNodeBuildResult)packet); diff --git a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs index c2402156971..b2c82c88ab6 100644 --- a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs +++ b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs @@ -214,7 +214,7 @@ private void RunReadLoop(Stream localStream, ManualResetEvent localPacketPumpShu case 1: { - // Client recieved a packet header. Read the rest of a package. + // Client recieved a packet header. Read the rest of it. int headerBytesRead = 0; #if FEATURE_APM headerBytesRead = localStream.EndRead(result); diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 25b39f379be..7c119ce2929 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -85,13 +85,14 @@ public OutOfProcServerNode(Func buildFu /// The reason for shutting down. public NodeEngineShutdownReason Run(out Exception? shutdownException) { - var handshake = new ServerNodeHandshake( + 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) { @@ -275,6 +276,7 @@ private void HandlePacket(INodePacket packet) 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) { diff --git a/src/Framework/MSBuildEventSource.cs b/src/Framework/MSBuildEventSource.cs index b4e335f7849..708f5f6a31b 100644 --- a/src/Framework/MSBuildEventSource.cs +++ b/src/Framework/MSBuildEventSource.cs @@ -599,6 +599,18 @@ public void OutOfProcSdkResolverServiceRequestSdkPathFromMainNodeStop(int submis WriteEvent(80, submissionId, sdkName, solutionPath, projectPath, success, wasResultCached); } + [Event(81, Keywords = Keywords.All)] + public void MSBuildServerBuildStart(string commandLine) + { + WriteEvent(81, commandLine); + } + + [Event(82, Keywords = Keywords.All)] + public void MSBuildServerBuildStop(string commandLine, int countOfConsoleMessages, long sumSizeOfConsoleMessages, string clientExitType, string serverExitType) + { + WriteEvent(82, commandLine, countOfConsoleMessages, sumSizeOfConsoleMessages, clientExitType, serverExitType); + } + #endregion } } From 5c2eead81edc4c6e01ce50b87470a3d75c5777fc Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Tue, 24 May 2022 13:22:56 +0200 Subject: [PATCH 07/43] Added cancelation feature (#7638) * Added cancelation feature --- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 39 +++++++++++++++++-- .../BackEnd/Node/ServerNodeBuildCancel.cs | 20 ++++++++++ src/Build/Microsoft.Build.csproj | 1 + .../PublicAPI/net/PublicAPI.Unshipped.txt | 4 +- .../netstandard/PublicAPI.Unshipped.txt | 2 +- src/MSBuild/XMake.cs | 9 ++++- src/Shared/INodePacket.cs | 6 +++ 7 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 src/Build/BackEnd/Node/ServerNodeBuildCancel.cs diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 7c119ce2929..f795a3eceae 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -9,6 +9,7 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.Shared; using Microsoft.Build.Internal; +using System.Threading.Tasks; namespace Microsoft.Build.Execution { @@ -19,6 +20,8 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket { private readonly Func _buildFunction; + private readonly Action _onCancel; + /// /// The endpoint used to talk to the host. /// @@ -59,11 +62,14 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket /// private readonly bool _debugCommunications; + private Task? _buildTask; + private string _serverBusyMutexName = default!; - public OutOfProcServerNode(Func buildFunction) + public OutOfProcServerNode(Func buildFunction, Action onCancel) { _buildFunction = buildFunction; + _onCancel = onCancel; new Dictionary(); _debugCommunications = (Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM") == "1"); @@ -74,6 +80,7 @@ public OutOfProcServerNode(Func buildFu (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 @@ -105,7 +112,7 @@ public NodeEngineShutdownReason Run(out Exception? shutdownException) _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) { @@ -269,11 +276,35 @@ private void HandlePacket(INodePacket packet) switch (packet.Type) { case NodePacketType.ServerNodeBuildCommand: - HandleServerNodeBuildCommand((ServerNodeBuildCommand)packet); + HandleServerNodeBuildCommandAsync((ServerNodeBuildCommand)packet); + break; + case NodePacketType.ServerNodeBuildCancel: + _onCancel(); break; } } + private void HandleServerNodeBuildCommandAsync(ServerNodeBuildCommand command) + { + _buildTask = Task.Run(() => + { + try + { + HandleServerNodeBuildCommand(command); + } + catch(Exception e) + { + _shutdownException = e; + _shutdownReason = NodeEngineShutdownReason.Error; + _shutdownEvent.Set(); + } + finally + { + _buildTask = null; + } + }); + } + private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) { CommunicationsUtilities.Trace("Building with MSBuild server with command line {0}", command.CommandLine); @@ -285,6 +316,8 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) _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 diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs b/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs new file mode 100644 index 00000000000..349c1b8170d --- /dev/null +++ b/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs @@ -0,0 +1,20 @@ +// 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 ServerNodeBuildCancel : INodePacket + { + public NodePacketType Type => NodePacketType.ServerNodeBuildCancel; + + public void Translate(ITranslator translator) + { + } + + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + return new ServerNodeBuildCancel(); + } + } +} diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index d14821f787f..9c4bbac8ee4 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -155,6 +155,7 @@ + diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index b7e25f06956..349a8e57aac 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -15,5 +15,5 @@ Microsoft.Build.Execution.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Buil Microsoft.Build.Execution.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Execution.MSBuildClientExitType 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 +Microsoft.Build.Execution.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction, System.Action onCancel) -> 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 b7e25f06956..39c901f1b5c 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -15,5 +15,5 @@ Microsoft.Build.Execution.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Buil Microsoft.Build.Execution.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.OutOfProcServerNode -Microsoft.Build.Execution.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction) -> void +Microsoft.Build.Execution.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction, System.Action onCancel) -> 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/XMake.cs b/src/MSBuild/XMake.cs index f3665f0c80b..b7d98c179d5 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -2684,7 +2684,14 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool return (exitCode, exitType.ToString()); }; - OutOfProcServerNode node = new(buildFunction); + Action onCancel = () => + { + Console.WriteLine(ResourceUtilities.GetResourceString("AbortingBuild")); + + BuildManager.DefaultBuildManager.CancelAllSubmissions(); + }; + + OutOfProcServerNode node = new(buildFunction, onCancel); s_isServerNode = true; shutdownReason = node.Run(out nodeException); diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs index b0ec1f1f6c5..0ddbf49a0d7 100644 --- a/src/Shared/INodePacket.cs +++ b/src/Shared/INodePacket.cs @@ -207,6 +207,12 @@ internal enum NodePacketType : byte /// Keep this enum value constant intact as this is part of contract with dotnet CLI /// ServerNodeConsoleWrite = 0xF2, + + /// + /// Command to cancel ongoing build. + /// Keep this enum value constant intact as this is part of contract with dotnet CLI + /// + ServerNodeBuildCancel = 0xF3, } #endregion From 5ade6268a8d372c316c0904b0aa32941759baffe Mon Sep 17 00:00:00 2001 From: Forgind Date: Mon, 30 May 2022 07:17:24 -0700 Subject: [PATCH 08/43] Fix control sequence emission (#7630) * Fix control sequence emission * Some cleanup --- src/Build/BackEnd/Client/MSBuildClient.cs | 16 ++++++++++++++++ src/Framework/NativeMethods.cs | 9 ++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 57c19c4a361..c8b3c4adc7f 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.IO; using System.IO.Pipes; +using System.Runtime.InteropServices; using System.Threading; using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.Client; @@ -183,6 +184,11 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can packetPump.PacketReceivedEvent }; + if (NativeMethodsShared.IsWindows) + { + SupportVT100(); + } + while (!_buildFinished) { int index = WaitHandle.WaitAny(waitHandles); @@ -222,6 +228,16 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can return _exitResult; } + private void SupportVT100() + { + IntPtr stdOut = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + if (NativeMethodsShared.GetConsoleMode(stdOut, out uint consoleMode)) + { + consoleMode |= NativeMethodsShared.ENABLE_VIRTUAL_TERMINAL_PROCESSING | NativeMethodsShared.DISABLE_NEWLINE_AUTO_RETURN; + NativeMethodsShared.SetConsoleMode(stdOut, consoleMode); + } + } + private void SendCancelCommand(NamedPipeClientStream nodeStream) => throw new NotImplementedException(); /// diff --git a/src/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs index 9e68fc71c4e..5b990331277 100644 --- a/src/Framework/NativeMethods.cs +++ b/src/Framework/NativeMethods.cs @@ -10,7 +10,6 @@ using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Versioning; -using System.Text; using System.Threading; using Microsoft.Build.Shared; @@ -37,6 +36,8 @@ internal static class NativeMethods internal const uint RUNTIME_INFO_DONT_SHOW_ERROR_DIALOG = 0x40; internal const uint FILE_TYPE_CHAR = 0x0002; internal const Int32 STD_OUTPUT_HANDLE = -11; + internal const uint DISABLE_NEWLINE_AUTO_RETURN = 0x0008; + internal const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; internal const uint RPC_S_CALLPENDING = 0x80010115; internal const uint E_ABORT = (uint)0x80004004; @@ -1488,6 +1489,12 @@ internal static void VerifyThrowWin32Result(int result) [DllImport("kernel32.dll")] internal static extern IntPtr GetStdHandle(int nStdHandle); + [DllImport("kernel32.dll")] + internal static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode); + + [DllImport("kernel32.dll")] + internal static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode); + [DllImport("kernel32.dll")] internal static extern uint GetFileType(IntPtr hFile); From 8080b4c167af3a46db4092eac4a71e1b3545a969 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Fri, 27 May 2022 10:44:14 +0200 Subject: [PATCH 09/43] Solving memory leak by reusing BuildManager and ProjectRoolElementCache --- src/Build/Definition/ProjectCollection.cs | 43 ++++++++++++++++++- .../PublicAPI/net/PublicAPI.Unshipped.txt | 1 + .../netstandard/PublicAPI.Unshipped.txt | 1 + src/MSBuild/XMake.cs | 12 +++++- 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 485b905abe0..096fcc073a4 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -145,6 +145,8 @@ public void Dispose() /// private static string s_assemblyDisplayVersion; + private static ProjectRootElementCacheBase s_projectRootElementCache = null; + /// /// The projects loaded into this collection. /// @@ -302,6 +304,26 @@ public ProjectCollection(IDictionary globalProperties, IEnumerab /// If set to true, only critical events will be logged. /// If set to true, load all projects as read-only. public ProjectCollection(IDictionary globalProperties, IEnumerable loggers, IEnumerable remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly) + : this(globalProperties, loggers, remoteLoggers, toolsetDefinitionLocations, maxNodeCount, onlyLogCriticalEvents, loadProjectsReadOnly, reuseProjectRootElementCache: false) + { + } + + /// + /// Instantiates a project collection with specified global properties and loggers and using the + /// specified toolset locations, node count, and setting of onlyLogCriticalEvents. + /// Global properties and loggers may be null. + /// Throws InvalidProjectFileException if any of the global properties are reserved. + /// May throw InvalidToolsetDefinitionException. + /// + /// The default global properties to use. May be null. + /// The loggers to register. May be null and specified to any build instead. + /// Any remote loggers to register. May be null and specified to any build instead. + /// The locations from which to load toolsets. + /// The maximum number of nodes to use for building. + /// If set to true, only critical events will be logged. + /// If set to true, load all projects as read-only. + /// If set to true, it will try to reuse singleton. + public ProjectCollection(IDictionary globalProperties, IEnumerable loggers, IEnumerable remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) { _loadedProjects = new LoadedProjectCollection(); ToolsetLocations = toolsetDefinitionLocations; @@ -311,10 +333,23 @@ public ProjectCollection(IDictionary globalProperties, IEnumerab { ProjectRootElementCache = new SimpleProjectRootElementCache(); } + else if (reuseProjectRootElementCache && s_projectRootElementCache != null) + { + ProjectRootElementCache = s_projectRootElementCache; + } else { - ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk: false, loadProjectsReadOnly); + // When we are reusing ProjectRootElementCache we need to reload XMLs if it has changed between MSBuild Server sessions/builds. + // If we are not reusing, cache will be released at end of build and as we do not support project files will changes during build + // we do not need to auto reload. + bool autoReloadFromDisk = reuseProjectRootElementCache; + ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk, loadProjectsReadOnly); + if (reuseProjectRootElementCache && s_projectRootElementCache == null) + { + s_projectRootElementCache = ProjectRootElementCache; + } } + OnlyLogCriticalEvents = onlyLogCriticalEvents; try @@ -1603,6 +1638,12 @@ protected virtual void Dispose(bool disposing) if (disposing) { ShutDownLoggingService(); + if (ProjectRootElementCache != null) + { + ProjectRootElementCache.ProjectRootElementAddedHandler -= ProjectRootElementCache_ProjectRootElementAddedHandler; + ProjectRootElementCache.ProjectRootElementDirtied -= ProjectRootElementCache_ProjectRootElementDirtiedHandler; + ProjectRootElementCache.ProjectDirtied -= ProjectRootElementCache_ProjectDirtiedHandler; + } Tracing.Dump(); } } diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index 349a8e57aac..ee20877adfb 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Execution.MSBuildClient Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void diff --git a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 39c901f1b5c..44179d2f0e1 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Execution.MSBuildClient Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index b7d98c179d5..37434c6fc65 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -1082,7 +1082,8 @@ string[] commandLine toolsetDefinitionLocations, cpuCount, onlyLogCriticalEvents, - loadProjectsReadOnly: !preprocessOnly + loadProjectsReadOnly: !preprocessOnly, + reuseProjectRootElementCache: s_isServerNode ); if (toolsVersion != null && !projectCollection.ContainsToolset(toolsVersion)) @@ -1315,7 +1316,14 @@ string[] commandLine FileUtilities.ClearCacheDirectory(); projectCollection?.Dispose(); - BuildManager.DefaultBuildManager.Dispose(); + // Build manager shall be reused for all build sessions. + // If, for one reason or another, this behavior needs to change in future + // please be aware that current code creates and keep running InProcNode even + // when its owning default build manager is disposed resulting in leek of memory and threads. + if (!s_isServerNode) + { + BuildManager.DefaultBuildManager.Dispose(); + } } return success; From e41cf8a6ff737fe197b89d7c856444a9f39b89b6 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Fri, 27 May 2022 12:13:16 +0200 Subject: [PATCH 10/43] Do not clear project root element cache if in auto reload. --- src/Build/Evaluation/ProjectRootElementCache.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Build/Evaluation/ProjectRootElementCache.cs b/src/Build/Evaluation/ProjectRootElementCache.cs index 17ecae43227..208a43ed668 100644 --- a/src/Build/Evaluation/ProjectRootElementCache.cs +++ b/src/Build/Evaluation/ProjectRootElementCache.cs @@ -415,6 +415,12 @@ internal override void Clear() /// internal override void DiscardImplicitReferences() { + if (_autoReloadFromDisk) + { + // no need to clear it, as auto reload properly invalidates caches if changed. + return; + } + lock (_locker) { // Make a new Weak cache only with items that have been explicitly loaded, this will be a small number, there will most likely From 447225c121b96cdadf7bec6ca0e8d2ffb15900e2 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Fri, 27 May 2022 22:02:32 +0200 Subject: [PATCH 11/43] Reduce if Co-authored-by: Forgind --- src/Build/Definition/ProjectCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 096fcc073a4..f5d45290cd2 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -344,7 +344,7 @@ public ProjectCollection(IDictionary globalProperties, IEnumerab // we do not need to auto reload. bool autoReloadFromDisk = reuseProjectRootElementCache; ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk, loadProjectsReadOnly); - if (reuseProjectRootElementCache && s_projectRootElementCache == null) + if (reuseProjectRootElementCache) { s_projectRootElementCache = ProjectRootElementCache; } From 836f6ef4d31f43ef7f37b7c99bf61a0bb914b9d0 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Thu, 2 Jun 2022 06:05:09 -0700 Subject: [PATCH 12/43] Solving memory leak by reusing BuildManager and ProjectRoolElementCache (#7655) * Solving memory leak by reusing BuildManager and ProjectRoolElementCache * Do not clear project root element cache if in auto reload. --- src/Build/Definition/ProjectCollection.cs | 43 ++++++++++++++++++- .../Evaluation/ProjectRootElementCache.cs | 6 +++ .../PublicAPI/net/PublicAPI.Unshipped.txt | 1 + .../netstandard/PublicAPI.Unshipped.txt | 1 + src/MSBuild/XMake.cs | 12 +++++- 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/Build/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 485b905abe0..f5d45290cd2 100644 --- a/src/Build/Definition/ProjectCollection.cs +++ b/src/Build/Definition/ProjectCollection.cs @@ -145,6 +145,8 @@ public void Dispose() /// private static string s_assemblyDisplayVersion; + private static ProjectRootElementCacheBase s_projectRootElementCache = null; + /// /// The projects loaded into this collection. /// @@ -302,6 +304,26 @@ public ProjectCollection(IDictionary globalProperties, IEnumerab /// If set to true, only critical events will be logged. /// If set to true, load all projects as read-only. public ProjectCollection(IDictionary globalProperties, IEnumerable loggers, IEnumerable remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly) + : this(globalProperties, loggers, remoteLoggers, toolsetDefinitionLocations, maxNodeCount, onlyLogCriticalEvents, loadProjectsReadOnly, reuseProjectRootElementCache: false) + { + } + + /// + /// Instantiates a project collection with specified global properties and loggers and using the + /// specified toolset locations, node count, and setting of onlyLogCriticalEvents. + /// Global properties and loggers may be null. + /// Throws InvalidProjectFileException if any of the global properties are reserved. + /// May throw InvalidToolsetDefinitionException. + /// + /// The default global properties to use. May be null. + /// The loggers to register. May be null and specified to any build instead. + /// Any remote loggers to register. May be null and specified to any build instead. + /// The locations from which to load toolsets. + /// The maximum number of nodes to use for building. + /// If set to true, only critical events will be logged. + /// If set to true, load all projects as read-only. + /// If set to true, it will try to reuse singleton. + public ProjectCollection(IDictionary globalProperties, IEnumerable loggers, IEnumerable remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) { _loadedProjects = new LoadedProjectCollection(); ToolsetLocations = toolsetDefinitionLocations; @@ -311,10 +333,23 @@ public ProjectCollection(IDictionary globalProperties, IEnumerab { ProjectRootElementCache = new SimpleProjectRootElementCache(); } + else if (reuseProjectRootElementCache && s_projectRootElementCache != null) + { + ProjectRootElementCache = s_projectRootElementCache; + } else { - ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk: false, loadProjectsReadOnly); + // When we are reusing ProjectRootElementCache we need to reload XMLs if it has changed between MSBuild Server sessions/builds. + // If we are not reusing, cache will be released at end of build and as we do not support project files will changes during build + // we do not need to auto reload. + bool autoReloadFromDisk = reuseProjectRootElementCache; + ProjectRootElementCache = new ProjectRootElementCache(autoReloadFromDisk, loadProjectsReadOnly); + if (reuseProjectRootElementCache) + { + s_projectRootElementCache = ProjectRootElementCache; + } } + OnlyLogCriticalEvents = onlyLogCriticalEvents; try @@ -1603,6 +1638,12 @@ protected virtual void Dispose(bool disposing) if (disposing) { ShutDownLoggingService(); + if (ProjectRootElementCache != null) + { + ProjectRootElementCache.ProjectRootElementAddedHandler -= ProjectRootElementCache_ProjectRootElementAddedHandler; + ProjectRootElementCache.ProjectRootElementDirtied -= ProjectRootElementCache_ProjectRootElementDirtiedHandler; + ProjectRootElementCache.ProjectDirtied -= ProjectRootElementCache_ProjectDirtiedHandler; + } Tracing.Dump(); } } diff --git a/src/Build/Evaluation/ProjectRootElementCache.cs b/src/Build/Evaluation/ProjectRootElementCache.cs index 17ecae43227..208a43ed668 100644 --- a/src/Build/Evaluation/ProjectRootElementCache.cs +++ b/src/Build/Evaluation/ProjectRootElementCache.cs @@ -415,6 +415,12 @@ internal override void Clear() /// internal override void DiscardImplicitReferences() { + if (_autoReloadFromDisk) + { + // no need to clear it, as auto reload properly invalidates caches if changed. + return; + } + lock (_locker) { // Make a new Weak cache only with items that have been explicitly loaded, this will be a small number, there will most likely diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index 349a8e57aac..ee20877adfb 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Execution.MSBuildClient Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void diff --git a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 39c901f1b5c..44179d2f0e1 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,3 +1,4 @@ +Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Execution.MSBuildClient Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index b7d98c179d5..37434c6fc65 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -1082,7 +1082,8 @@ string[] commandLine toolsetDefinitionLocations, cpuCount, onlyLogCriticalEvents, - loadProjectsReadOnly: !preprocessOnly + loadProjectsReadOnly: !preprocessOnly, + reuseProjectRootElementCache: s_isServerNode ); if (toolsVersion != null && !projectCollection.ContainsToolset(toolsVersion)) @@ -1315,7 +1316,14 @@ string[] commandLine FileUtilities.ClearCacheDirectory(); projectCollection?.Dispose(); - BuildManager.DefaultBuildManager.Dispose(); + // Build manager shall be reused for all build sessions. + // If, for one reason or another, this behavior needs to change in future + // please be aware that current code creates and keep running InProcNode even + // when its owning default build manager is disposed resulting in leek of memory and threads. + if (!s_isServerNode) + { + BuildManager.DefaultBuildManager.Dispose(); + } } return success; From 88f571c04e435c39c2617edd0e7e0e4098db54dc Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Thu, 2 Jun 2022 21:52:23 +0200 Subject: [PATCH 13/43] Put msbuild server feature under ChangeWave 17.4. (#7661) --- documentation/wiki/ChangeWaves.md | 1 + src/MSBuild/XMake.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 1a778f77493..a9eb39bb9f2 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -26,6 +26,7 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t ### 17.4 - [Respect deps.json when loading assemblies](https://github.com/dotnet/msbuild/pull/7520) - [Remove opt in for new schema for CombineTargetFrameworkInfoProperties](https://github.com/dotnet/msbuild/pull/6928) +- [MSBuild server](https://github.com/dotnet/msbuild/pull/7634) ### 17.0 - [Scheduler should honor BuildParameters.DisableInprocNode](https://github.com/dotnet/msbuild/pull/6400) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 37434c6fc65..79d9f68f0d1 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -222,7 +222,7 @@ string[] args } int exitCode; - if (Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1") + if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_4) && Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1") { // Use the client app to execute build in msbuild server. Opt-in feature. exitCode = ((s_initialized && MSBuildClientApp.Execute( From 88f3410780d005e9876673bead8c4233346e8206 Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Thu, 2 Jun 2022 21:53:05 +0200 Subject: [PATCH 14/43] Add support for MSBUILDDEBUGONSTART env. var. for msbuild server client code path. (#7668) --- src/MSBuild/XMake.cs | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 79d9f68f0d1..8ee56e53d11 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -224,6 +224,8 @@ string[] args int exitCode; if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_4) && Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1") { + DebuggerLaunchCheck(); + // Use the client app to execute build in msbuild server. Opt-in feature. exitCode = ((s_initialized && MSBuildClientApp.Execute( #if FEATURE_GET_COMMANDLINE @@ -482,6 +484,26 @@ private static string GetFriendlyCounterType(PerformanceCounterType type, string } } #endif + /// + /// Launch debugger if it's requested by environment variable "MSBUILDDEBUGONSTART". + /// + private static void DebuggerLaunchCheck() + { + switch (Environment.GetEnvironmentVariable("MSBUILDDEBUGONSTART")) + { +#if FEATURE_DEBUG_LAUNCH + case "1": + Debugger.Launch(); + break; +#endif + case "2": + // Sometimes easier to attach rather than deal with JIT prompt + Process currentProcess = Process.GetCurrentProcess(); + Console.WriteLine($"Waiting for debugger to attach ({currentProcess.MainModule.FileName} PID {currentProcess.Id}). Press enter to continue..."); + Console.ReadLine(); + break; + } + } /// /// Orchestrates the execution of the application, and is also responsible @@ -506,20 +528,8 @@ string[] commandLine // with our OM and modify and save them. They'll never do this for Microsoft.*.targets, though, // and those form the great majority of our unnecessary memory use. Environment.SetEnvironmentVariable("MSBuildLoadMicrosoftTargetsReadOnly", "true"); - switch (Environment.GetEnvironmentVariable("MSBUILDDEBUGONSTART")) - { -#if FEATURE_DEBUG_LAUNCH - case "1": - Debugger.Launch(); - break; -#endif - case "2": - // Sometimes easier to attach rather than deal with JIT prompt - Process currentProcess = Process.GetCurrentProcess(); - Console.WriteLine($"Waiting for debugger to attach ({currentProcess.MainModule.FileName} PID {currentProcess.Id}). Press enter to continue..."); - Console.ReadLine(); - break; - } + + DebuggerLaunchCheck(); #if FEATURE_GET_COMMANDLINE ErrorUtilities.VerifyThrowArgumentLength(commandLine, nameof(commandLine)); From b4a34827a820976a41cd0b114835f2305f292b36 Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Mon, 6 Jun 2022 10:58:42 +0200 Subject: [PATCH 15/43] Fix msbuild server process launch. (#7673) --- src/Build/BackEnd/Client/MSBuildClient.cs | 59 ++--- .../Components/Communications/CurrentHost.cs | 49 +++++ .../Components/Communications/NodeLauncher.cs | 181 ++++++++++++++++ .../NodeProviderOutOfProcBase.cs | 203 +----------------- src/Build/Microsoft.Build.csproj | 4 +- .../PublicAPI/net/PublicAPI.Unshipped.txt | 2 +- .../netstandard/PublicAPI.Unshipped.txt | 2 +- src/MSBuild/MSBuildClientApp.cs | 37 +--- 8 files changed, 262 insertions(+), 275 deletions(-) create mode 100644 src/Build/BackEnd/Components/Communications/CurrentHost.cs create mode 100644 src/Build/BackEnd/Components/Communications/NodeLauncher.cs diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index c8b3c4adc7f..4233e1ddffe 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -8,7 +8,6 @@ using System.Globalization; using System.IO; using System.IO.Pipes; -using System.Runtime.InteropServices; using System.Threading; using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.Client; @@ -32,14 +31,10 @@ public sealed class MSBuildClient private readonly Dictionary _serverEnvironmentVariables; /// - /// Location of executable file to launch the server process. That should be either dotnet.exe or MSBuild.exe location. + /// Full path to current MSBuild.exe if executable is MSBuild.exe, + /// or to version of MSBuild.dll found to be associated with the current process. /// - private readonly string _exeLocation; - - /// - /// Location of dll file to launch the server process if needed. Empty if executable is msbuild.exe and not empty if dotnet.exe. - /// - private readonly string _dllLocation; + private readonly string _msbuildLocation; /// /// The MSBuild client execution result. @@ -85,18 +80,15 @@ public sealed class MSBuildClient /// /// Public constructor with parameters. /// - /// Location of executable file to launch the server process. - /// That should be either dotnet.exe or MSBuild.exe location. - /// Location of dll file to launch the server process if needed. - /// Empty if executable is msbuild.exe and not empty if dotnet.exe. - public MSBuildClient(string exeLocation, string dllLocation) + /// Full path to current MSBuild.exe if executable is MSBuild.exe, + /// or to version of MSBuild.dll found to be associated with the current process. + public MSBuildClient(string msbuildLocation) { _serverEnvironmentVariables = new(); _exitResult = new(); // dll & exe locations - _exeLocation = exeLocation; - _dllLocation = dllLocation; + _msbuildLocation = msbuildLocation; // Client <-> Server communication stream _handshake = GetHandshake(); @@ -257,15 +249,20 @@ private bool TryLaunchServer() } string[] msBuildServerOptions = new string[] { - _dllLocation, "/nologo", "/nodemode:8" }; + string? useMSBuildServerEnvVarValue = Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName); try { - Process msbuildProcess = LaunchNode(_exeLocation, string.Join(" ", msBuildServerOptions), _serverEnvironmentVariables); - CommunicationsUtilities.Trace("Server is launched with PID: {0}", msbuildProcess.Id); + // Disable MSBuild server for a child process, preventing an infinite recurson. + Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, ""); + + NodeLauncher nodeLauncher = new NodeLauncher(); + CommunicationsUtilities.Trace("Starting Server..."); + Process msbuildProcess = nodeLauncher.Start(_msbuildLocation, string.Join(" ", msBuildServerOptions)); + CommunicationsUtilities.Trace("Server started with PID: {0}", msbuildProcess?.Id); } catch (Exception ex) { @@ -273,32 +270,12 @@ private bool TryLaunchServer() _exitResult.MSBuildClientExitType = MSBuildClientExitType.LaunchError; return false; } - - return true; - } - - private Process LaunchNode(string exeLocation, string msBuildServerArguments, Dictionary serverEnvironmentVariables) - { - CommunicationsUtilities.Trace("Launching server node from {0} with arguments {1}", exeLocation, msBuildServerArguments); - ProcessStartInfo processStartInfo = new() - { - FileName = exeLocation, - Arguments = msBuildServerArguments, - UseShellExecute = false - }; - - foreach (var entry in serverEnvironmentVariables) + finally { - processStartInfo.Environment[entry.Key] = entry.Value; + Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, useMSBuildServerEnvVarValue); } - // We remove env to enable MSBuild Server that might be equal to 1, so we do not get an infinite recursion here. - processStartInfo.Environment[Traits.UseMSBuildServerEnvVarName] = "0"; - - processStartInfo.CreateNoWindow = true; - processStartInfo.UseShellExecute = false; - - return Process.Start(processStartInfo) ?? throw new InvalidOperationException("MSBuild server node failed to launch."); + return true; } private bool TrySendBuildCommand(string commandLine) diff --git a/src/Build/BackEnd/Components/Communications/CurrentHost.cs b/src/Build/BackEnd/Components/Communications/CurrentHost.cs new file mode 100644 index 00000000000..81116ecb054 --- /dev/null +++ b/src/Build/BackEnd/Components/Communications/CurrentHost.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; +using System.IO; +using Microsoft.Build.Shared; + +#nullable disable + +namespace Microsoft.Build.BackEnd +{ + internal static class CurrentHost + { + +#if RUNTIME_TYPE_NETCORE || MONO + private static string s_currentHost; +#endif + + /// + /// Identify the .NET host of the current process. + /// + /// The full path to the executable hosting the current process, or null if running on Full Framework on Windows. + public static string GetCurrentHost() + { +#if RUNTIME_TYPE_NETCORE || MONO + if (s_currentHost == null) + { + string dotnetExe = Path.Combine(FileUtilities.GetFolderAbove(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, 2), + NativeMethodsShared.IsWindows ? "dotnet.exe" : "dotnet"); + if (File.Exists(dotnetExe)) + { + s_currentHost = dotnetExe; + } + else + { + using (Process currentProcess = Process.GetCurrentProcess()) + { + s_currentHost = currentProcess.MainModule.FileName; + } + } + } + + return s_currentHost; +#else + return null; +#endif + } + } +} diff --git a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs new file mode 100644 index 00000000000..652f7dda74c --- /dev/null +++ b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Build.Exceptions; +using Microsoft.Build.Framework; +using Microsoft.Build.Internal; +using Microsoft.Build.Shared; +using Microsoft.Build.Shared.FileSystem; +using BackendNativeMethods = Microsoft.Build.BackEnd.NativeMethods; + +#nullable disable + +namespace Microsoft.Build.BackEnd +{ + internal class NodeLauncher + { + /// + /// Creates a new MSBuild process + /// + public Process Start(string msbuildLocation, string commandLineArgs) + { + // Should always have been set already. + ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, nameof(msbuildLocation)); + + if (!FileSystems.Default.FileExists(msbuildLocation)) + { + throw new BuildAbortedException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("CouldNotFindMSBuildExe", msbuildLocation)); + } + + // Repeat the executable name as the first token of the command line because the command line + // parser logic expects it and will otherwise skip the first argument + commandLineArgs = $"\"{msbuildLocation}\" {commandLineArgs}"; + + BackendNativeMethods.STARTUP_INFO startInfo = new(); + startInfo.cb = Marshal.SizeOf(); + + // Null out the process handles so that the parent process does not wait for the child process + // to exit before it can exit. + uint creationFlags = 0; + if (Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout) + { + creationFlags = BackendNativeMethods.NORMALPRIORITYCLASS; + } + + if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDNODEWINDOW"))) + { + if (!Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout) + { + // Redirect the streams of worker nodes so that this MSBuild.exe's + // parent doesn't wait on idle worker nodes to close streams + // after the build is complete. + startInfo.hStdError = BackendNativeMethods.InvalidHandle; + startInfo.hStdInput = BackendNativeMethods.InvalidHandle; + startInfo.hStdOutput = BackendNativeMethods.InvalidHandle; + startInfo.dwFlags = BackendNativeMethods.STARTFUSESTDHANDLES; + creationFlags |= BackendNativeMethods.CREATENOWINDOW; + } + } + else + { + creationFlags |= BackendNativeMethods.CREATE_NEW_CONSOLE; + } + + CommunicationsUtilities.Trace("Launching node from {0}", msbuildLocation); + + string exeName = msbuildLocation; + +#if RUNTIME_TYPE_NETCORE || MONO + // Mono automagically uses the current mono, to execute a managed assembly + if (!NativeMethodsShared.IsMono) + { + // Run the child process with the same host as the currently-running process. + exeName = CurrentHost.GetCurrentHost(); + } +#endif + + if (!NativeMethodsShared.IsWindows) + { + ProcessStartInfo processStartInfo = new ProcessStartInfo(); + processStartInfo.FileName = exeName; + processStartInfo.Arguments = commandLineArgs; + if (!Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout) + { + // Redirect the streams of worker nodes so that this MSBuild.exe's + // parent doesn't wait on idle worker nodes to close streams + // after the build is complete. + processStartInfo.RedirectStandardInput = true; + processStartInfo.RedirectStandardOutput = true; + processStartInfo.RedirectStandardError = true; + processStartInfo.CreateNoWindow = (creationFlags | BackendNativeMethods.CREATENOWINDOW) == BackendNativeMethods.CREATENOWINDOW; + } + processStartInfo.UseShellExecute = false; + + Process process; + try + { + process = Process.Start(processStartInfo); + } + catch (Exception ex) + { + CommunicationsUtilities.Trace + ( + "Failed to launch node from {0}. CommandLine: {1}" + Environment.NewLine + "{2}", + msbuildLocation, + commandLineArgs, + ex.ToString() + ); + + throw new NodeFailedToLaunchException(ex); + } + + CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", process.Id, exeName); + return process; + } + else + { +#if RUNTIME_TYPE_NETCORE + // Repeat the executable name in the args to suit CreateProcess + commandLineArgs = $"\"{exeName}\" {commandLineArgs}"; +#endif + + BackendNativeMethods.PROCESS_INFORMATION processInfo = new(); + BackendNativeMethods.SECURITY_ATTRIBUTES processSecurityAttributes = new(); + BackendNativeMethods.SECURITY_ATTRIBUTES threadSecurityAttributes = new(); + processSecurityAttributes.nLength = Marshal.SizeOf(); + threadSecurityAttributes.nLength = Marshal.SizeOf(); + + bool result = BackendNativeMethods.CreateProcess + ( + exeName, + commandLineArgs, + ref processSecurityAttributes, + ref threadSecurityAttributes, + false, + creationFlags, + BackendNativeMethods.NullPtr, + null, + ref startInfo, + out processInfo + ); + + if (!result) + { + // Creating an instance of this exception calls GetLastWin32Error and also converts it to a user-friendly string. + System.ComponentModel.Win32Exception e = new System.ComponentModel.Win32Exception(); + + CommunicationsUtilities.Trace + ( + "Failed to launch node from {0}. System32 Error code {1}. Description {2}. CommandLine: {2}", + msbuildLocation, + e.NativeErrorCode.ToString(CultureInfo.InvariantCulture), + e.Message, + commandLineArgs + ); + + throw new NodeFailedToLaunchException(e.NativeErrorCode.ToString(CultureInfo.InvariantCulture), e.Message); + } + + int childProcessId = processInfo.dwProcessId; + + if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != NativeMethods.InvalidHandle) + { + NativeMethodsShared.CloseHandle(processInfo.hProcess); + } + + if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != NativeMethods.InvalidHandle) + { + NativeMethodsShared.CloseHandle(processInfo.hThread); + } + + CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", childProcessId, exeName); + return Process.GetProcessById(childProcessId); + } + } + } +} diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index b667e7a60f2..0beb610c0aa 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -19,13 +19,8 @@ #if FEATURE_APM using Microsoft.Build.Eventing; #endif -using Microsoft.Build.Exceptions; using Microsoft.Build.Internal; using Microsoft.Build.Shared; -using Microsoft.Build.Shared.FileSystem; -using Microsoft.Build.Utilities; - -using BackendNativeMethods = Microsoft.Build.BackEnd.NativeMethods; using Task = System.Threading.Tasks.Task; using Microsoft.Build.Framework; using Microsoft.Build.BackEnd.Logging; @@ -274,7 +269,8 @@ protected NodeContext GetNode(string msbuildLocation, string commandLineArgs, in #endif // Create the node process - Process msbuildProcess = LaunchNode(msbuildLocation, commandLineArgs); + NodeLauncher nodeLauncher = new NodeLauncher(); + Process msbuildProcess = nodeLauncher.Start(msbuildLocation, commandLineArgs); _processesToIgnore.Add(GetProcessesToIgnoreKey(hostHandshake, msbuildProcess.Id)); // Note, when running under IMAGEFILEEXECUTIONOPTIONS registry key to debug, the process ID @@ -332,7 +328,7 @@ protected NodeContext GetNode(string msbuildLocation, string commandLineArgs, in msbuildLocation = "MSBuild.exe"; } - var expectedProcessName = Path.GetFileNameWithoutExtension(GetCurrentHost() ?? msbuildLocation); + var expectedProcessName = Path.GetFileNameWithoutExtension(CurrentHost.GetCurrentHost() ?? msbuildLocation); List nodeProcesses = new List(Process.GetProcessesByName(expectedProcessName)); @@ -441,199 +437,6 @@ private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake han return null; } - /// - /// Creates a new MSBuild process - /// - private Process LaunchNode(string msbuildLocation, string commandLineArgs) - { - // Should always have been set already. - ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, nameof(msbuildLocation)); - - if (!FileSystems.Default.FileExists(msbuildLocation)) - { - throw new BuildAbortedException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("CouldNotFindMSBuildExe", msbuildLocation)); - } - - // Repeat the executable name as the first token of the command line because the command line - // parser logic expects it and will otherwise skip the first argument - commandLineArgs = $"\"{msbuildLocation}\" {commandLineArgs}"; - - BackendNativeMethods.STARTUP_INFO startInfo = new(); - startInfo.cb = Marshal.SizeOf(); - - // Null out the process handles so that the parent process does not wait for the child process - // to exit before it can exit. - uint creationFlags = 0; - if (Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout) - { - creationFlags = BackendNativeMethods.NORMALPRIORITYCLASS; - } - - if (String.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDNODEWINDOW"))) - { - if (!Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout) - { - // Redirect the streams of worker nodes so that this MSBuild.exe's - // parent doesn't wait on idle worker nodes to close streams - // after the build is complete. - startInfo.hStdError = BackendNativeMethods.InvalidHandle; - startInfo.hStdInput = BackendNativeMethods.InvalidHandle; - startInfo.hStdOutput = BackendNativeMethods.InvalidHandle; - startInfo.dwFlags = BackendNativeMethods.STARTFUSESTDHANDLES; - creationFlags |= BackendNativeMethods.CREATENOWINDOW; - } - } - else - { - creationFlags |= BackendNativeMethods.CREATE_NEW_CONSOLE; - } - - CommunicationsUtilities.Trace("Launching node from {0}", msbuildLocation); - - string exeName = msbuildLocation; - -#if RUNTIME_TYPE_NETCORE || MONO - // Mono automagically uses the current mono, to execute a managed assembly - if (!NativeMethodsShared.IsMono) - { - // Run the child process with the same host as the currently-running process. - exeName = GetCurrentHost(); - } -#endif - - if (!NativeMethodsShared.IsWindows) - { - ProcessStartInfo processStartInfo = new ProcessStartInfo(); - processStartInfo.FileName = exeName; - processStartInfo.Arguments = commandLineArgs; - if (!Traits.Instance.EscapeHatches.EnsureStdOutForChildNodesIsPrimaryStdout) - { - // Redirect the streams of worker nodes so that this MSBuild.exe's - // parent doesn't wait on idle worker nodes to close streams - // after the build is complete. - processStartInfo.RedirectStandardInput = true; - processStartInfo.RedirectStandardOutput = true; - processStartInfo.RedirectStandardError = true; - processStartInfo.CreateNoWindow = (creationFlags | BackendNativeMethods.CREATENOWINDOW) == BackendNativeMethods.CREATENOWINDOW; - } - processStartInfo.UseShellExecute = false; - - Process process; - try - { - process = Process.Start(processStartInfo); - } - catch (Exception ex) - { - CommunicationsUtilities.Trace - ( - "Failed to launch node from {0}. CommandLine: {1}" + Environment.NewLine + "{2}", - msbuildLocation, - commandLineArgs, - ex.ToString() - ); - - throw new NodeFailedToLaunchException(ex); - } - - CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", process.Id, exeName); - return process; - } - else - { -#if RUNTIME_TYPE_NETCORE - // Repeat the executable name in the args to suit CreateProcess - commandLineArgs = $"\"{exeName}\" {commandLineArgs}"; -#endif - - BackendNativeMethods.PROCESS_INFORMATION processInfo = new(); - BackendNativeMethods.SECURITY_ATTRIBUTES processSecurityAttributes = new(); - BackendNativeMethods.SECURITY_ATTRIBUTES threadSecurityAttributes = new(); - processSecurityAttributes.nLength = Marshal.SizeOf(); - threadSecurityAttributes.nLength = Marshal.SizeOf(); - - bool result = BackendNativeMethods.CreateProcess - ( - exeName, - commandLineArgs, - ref processSecurityAttributes, - ref threadSecurityAttributes, - false, - creationFlags, - BackendNativeMethods.NullPtr, - null, - ref startInfo, - out processInfo - ); - - if (!result) - { - // Creating an instance of this exception calls GetLastWin32Error and also converts it to a user-friendly string. - System.ComponentModel.Win32Exception e = new System.ComponentModel.Win32Exception(); - - CommunicationsUtilities.Trace - ( - "Failed to launch node from {0}. System32 Error code {1}. Description {2}. CommandLine: {2}", - msbuildLocation, - e.NativeErrorCode.ToString(CultureInfo.InvariantCulture), - e.Message, - commandLineArgs - ); - - throw new NodeFailedToLaunchException(e.NativeErrorCode.ToString(CultureInfo.InvariantCulture), e.Message); - } - - int childProcessId = processInfo.dwProcessId; - - if (processInfo.hProcess != IntPtr.Zero && processInfo.hProcess != NativeMethods.InvalidHandle) - { - NativeMethodsShared.CloseHandle(processInfo.hProcess); - } - - if (processInfo.hThread != IntPtr.Zero && processInfo.hThread != NativeMethods.InvalidHandle) - { - NativeMethodsShared.CloseHandle(processInfo.hThread); - } - - CommunicationsUtilities.Trace("Successfully launched {1} node with PID {0}", childProcessId, exeName); - return Process.GetProcessById(childProcessId); - } - } - -#if RUNTIME_TYPE_NETCORE || MONO - private static string CurrentHost; -#endif - - /// - /// Identify the .NET host of the current process. - /// - /// The full path to the executable hosting the current process, or null if running on Full Framework on Windows. - private static string GetCurrentHost() - { -#if RUNTIME_TYPE_NETCORE || MONO - if (CurrentHost == null) - { - string dotnetExe = Path.Combine(FileUtilities.GetFolderAbove(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory, 2), - NativeMethodsShared.IsWindows ? "dotnet.exe" : "dotnet"); - if (File.Exists(dotnetExe)) - { - CurrentHost = dotnetExe; - } - else - { - using (Process currentProcess = Process.GetCurrentProcess()) - { - CurrentHost = currentProcess.MainModule.FileName; - } - } - } - - return CurrentHost; -#else - return null; -#endif - } - /// /// Class which wraps up the communications infrastructure for a given node. /// diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 9c4bbac8ee4..f6c13b14634 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -1,4 +1,4 @@ - + @@ -149,6 +149,7 @@ + @@ -350,6 +351,7 @@ + diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index ee20877adfb..a39e6b88d8c 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Execution.MSBuildClient Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult -Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void +Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string msbuildLocation) -> void Microsoft.Build.Execution.MSBuildClientExitResult Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void diff --git a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 44179d2f0e1..1019cb1d919 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Execution.MSBuildClient Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult -Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string exeLocation, string dllLocation) -> void +Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string msbuildLocation) -> void Microsoft.Build.Execution.MSBuildClientExitResult Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index de83c3a10e3..ef39eb8f1b3 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -42,34 +42,12 @@ public static MSBuildApp.ExitType Execute( CancellationToken cancellationToken ) { - string? exeLocation; - string? dllLocation; - -#if RUNTIME_TYPE_NETCORE || MONO - // Run the child process with the same host as the currently-running process. - // Mono automatically uses the current mono, to execute a managed assembly. - if (!NativeMethodsShared.IsMono) - { - // _exeFileLocation consists the msbuild dll instead. - dllLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; - exeLocation = GetCurrentHost(); - } - else - { - // _exeFileLocation consists the msbuild dll instead. - exeLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; - dllLocation = String.Empty; - } -#else - exeLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; - dllLocation = String.Empty; -#endif + string msbuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; return Execute( commandLine, cancellationToken, - exeLocation, - dllLocation + msbuildLocation ); } @@ -80,10 +58,8 @@ CancellationToken cancellationToken /// on the command line is assumed to be the name/path of the executable, and /// is ignored. /// Cancellation token. - /// Location of executable file to launch the server process. - /// That should be either dotnet.exe or MSBuild.exe location. - /// Location of dll file to launch the server process if needed. - /// Empty if executable is msbuild.exe and not empty if dotnet.exe. + /// Full path to current MSBuild.exe if executable is MSBuild.exe, + /// or to version of MSBuild.dll found to be associated with the current process. /// A value of type that indicates whether the build succeeded, /// or the manner in which it failed. public static MSBuildApp.ExitType Execute( @@ -93,8 +69,7 @@ public static MSBuildApp.ExitType Execute( string[] commandLine, #endif CancellationToken cancellationToken, - string exeLocation, - string dllLocation + string msbuildLocation ) { // MSBuild client orchestration. @@ -103,7 +78,7 @@ string dllLocation #else string commandLineString = commandLine; #endif - MSBuildClient msbuildClient = new MSBuildClient(exeLocation, dllLocation); + MSBuildClient msbuildClient = new MSBuildClient(msbuildLocation); MSBuildClientExitResult exitResult = msbuildClient.Execute(commandLineString, cancellationToken); if (exitResult.MSBuildClientExitType == MSBuildClientExitType.ServerBusy || From 206f91857b144ce66c72663135e32689f34c6a4d Mon Sep 17 00:00:00 2001 From: Forgind Date: Tue, 7 Jun 2022 05:27:10 -0700 Subject: [PATCH 16/43] Add test for MSBuild Server (#7592) * Add giant test for MSBuild Server * Add comm traces * Remove test that uses MSBUILDNOINPROCNODE flag: it checks the wrong behavior. * Add comments about WaitForExit and set a timeout for the process execution. Co-authored-by: AR-May <67507805+AR-May@users.noreply.github.com> Co-authored-by: Roman Konecny --- ...Microsoft.Build.Engine.OM.UnitTests.csproj | 4 +- src/Build.OM.UnitTests/NugetRestoreTests.cs | 4 +- src/Build/BackEnd/Client/MSBuildClient.cs | 4 +- .../Microsoft.Build.Framework.csproj | 5 + src/MSBuild.UnitTests/MSBuildServer_Tests.cs | 217 ++++++++++++++++++ ...crosoft.Build.CommandLine.UnitTests.csproj | 1 + .../Microsoft.Build.Tasks.UnitTests.csproj | 1 + src/UnitTests.Shared/RunnerUtilities.cs | 37 ++- 8 files changed, 255 insertions(+), 18 deletions(-) create mode 100644 src/MSBuild.UnitTests/MSBuildServer_Tests.cs diff --git a/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj b/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj index 9760dcf7a92..51d86b2f804 100644 --- a/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj +++ b/src/Build.OM.UnitTests/Microsoft.Build.Engine.OM.UnitTests.csproj @@ -1,4 +1,4 @@ - + @@ -78,8 +78,8 @@ TestData\GlobbingTestData.cs + - App.config Designer diff --git a/src/Build.OM.UnitTests/NugetRestoreTests.cs b/src/Build.OM.UnitTests/NugetRestoreTests.cs index 3c75c36b772..daf8cbea8d0 100644 --- a/src/Build.OM.UnitTests/NugetRestoreTests.cs +++ b/src/Build.OM.UnitTests/NugetRestoreTests.cs @@ -10,8 +10,6 @@ #endif using Xunit.Abstractions; -#nullable disable - namespace Microsoft.Build.Engine.OM.UnitTests { public sealed class NugetRestoreTests @@ -29,7 +27,7 @@ public NugetRestoreTests(ITestOutputHelper output) [Fact] public void TestOldNuget() { - string msbuildExePath = Path.GetDirectoryName(RunnerUtilities.PathToCurrentlyRunningMsBuildExe); + string msbuildExePath = Path.GetDirectoryName(RunnerUtilities.PathToCurrentlyRunningMsBuildExe)!; using TestEnvironment testEnvironment = TestEnvironment.Create(); TransientTestFolder folder = testEnvironment.CreateFolder(createFolder: true); // The content of the solution isn't known to matter, but having a custom solution makes it easier to add requirements should they become evident. diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 4233e1ddffe..23f7853ed34 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft. All rights reserved. +// Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; @@ -416,7 +416,7 @@ private bool TryConnectToServer(int timeout) CommunicationsUtilities.Trace("Reading handshake from pipe {0}", _pipeName); #if NETCOREAPP2_1_OR_GREATER || MONO - _nodeStream.ReadEndOfHandshakeSignal(false, 1000); + _nodeStream.ReadEndOfHandshakeSignal(false, 1000); #else _nodeStream.ReadEndOfHandshakeSignal(false); #endif diff --git a/src/Framework/Microsoft.Build.Framework.csproj b/src/Framework/Microsoft.Build.Framework.csproj index e24a13ff151..b4c3190f3b4 100644 --- a/src/Framework/Microsoft.Build.Framework.csproj +++ b/src/Framework/Microsoft.Build.Framework.csproj @@ -25,6 +25,11 @@ + + + + + Shared\Constants.cs diff --git a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs new file mode 100644 index 00000000000..a7ecdde6176 --- /dev/null +++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs @@ -0,0 +1,217 @@ +// 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.Diagnostics; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Shared; +using Microsoft.Build.UnitTests; +using Microsoft.Build.UnitTests.Shared; +#if NETFRAMEWORK +using Microsoft.IO; +#else +using System.IO; +#endif +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Build.Engine.UnitTests +{ + public class SleepingTask : Microsoft.Build.Utilities.Task + { + public int SleepTime { get; set; } + + /// + /// Sleep for SleepTime milliseconds. + /// + /// Success on success. + public override bool Execute() + { + Thread.Sleep(SleepTime); + return !Log.HasLoggedErrors; + } + } + + public class ProcessIdTask : Microsoft.Build.Utilities.Task + { + [Output] + public int Pid { get; set; } + + /// + /// Log the id for this process. + /// + /// + public override bool Execute() + { + Pid = Process.GetCurrentProcess().Id; + return true; + } + } + + public class MSBuildServer_Tests : IDisposable + { + private readonly ITestOutputHelper _output; + private readonly TestEnvironment _env; + private static string printPidContents = @$" + + + + + + + + +"; + private static string sleepingTaskContents = @$" + + + + + +"; + + public MSBuildServer_Tests(ITestOutputHelper output) + { + _output = output; + _env = TestEnvironment.Create(_output); + } + + public void Dispose() => _env.Dispose(); + + [Fact] + public void MSBuildServerTest() + { + TransientTestFile project = _env.CreateFile("testProject.proj", printPidContents); + _env.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); + string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success, false, _output); + success.ShouldBeTrue(); + int pidOfInitialProcess = ParseNumber(output, "Process ID is "); + int pidOfServerProcess = ParseNumber(output, "Server ID is "); + pidOfInitialProcess.ShouldNotBe(pidOfServerProcess, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); + + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + int newPidOfInitialProcess = ParseNumber(output, "Process ID is "); + newPidOfInitialProcess.ShouldNotBe(pidOfServerProcess, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); + newPidOfInitialProcess.ShouldNotBe(pidOfInitialProcess, "Process started by two MSBuild executions should be different."); + pidOfServerProcess.ShouldBe(ParseNumber(output, "Server ID is "), "Node used by both the first and second build should be the same."); + + // Prep to kill the long-lived task we're about to start. + Task t = Task.Run(() => + { + // Wait for the long-lived task to start + // If this test seems to fail randomly, increase this time. + Thread.Sleep(1000); + + // Kill the server + ProcessExtensions.KillTree(Process.GetProcessById(pidOfServerProcess), 1000); + }); + + // Start long-lived task execution + TransientTestFile sleepProject = _env.CreateFile("napProject.proj", sleepingTaskContents); + RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, sleepProject.Path, out _); + + t.Wait(); + + // Ensure that a new build can still succeed and that its server node is different. + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + newPidOfInitialProcess = ParseNumber(output, "Process ID is "); + int newServerProcessId = ParseNumber(output, "Server ID is "); + newPidOfInitialProcess.ShouldNotBe(pidOfInitialProcess, "Process started by two MSBuild executions should be different."); + newPidOfInitialProcess.ShouldNotBe(newServerProcessId, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); + pidOfServerProcess.ShouldNotBe(newServerProcessId, "Node used by both the first and second build should not be the same."); + } + + [Fact] + public void VerifyMixedLegacyBehavior() + { + TransientTestFile project = _env.CreateFile("testProject.proj", printPidContents); + _env.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); + + string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success, false, _output); + success.ShouldBeTrue(); + int pidOfInitialProcess = ParseNumber(output, "Process ID is "); + int pidOfServerProcess = ParseNumber(output, "Server ID is "); + pidOfInitialProcess.ShouldNotBe(pidOfServerProcess, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); + + Environment.SetEnvironmentVariable("MSBUILDUSESERVER", ""); + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + pidOfInitialProcess = ParseNumber(output, "Process ID is "); + int pidOfNewserverProcess = ParseNumber(output, "Server ID is "); + pidOfInitialProcess.ShouldBe(pidOfNewserverProcess, "We did not start a server node to execute the target, so its pid should be the same."); + + Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + pidOfInitialProcess = ParseNumber(output, "Process ID is "); + pidOfNewserverProcess = ParseNumber(output, "Server ID is "); + pidOfInitialProcess.ShouldNotBe(pidOfNewserverProcess, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); + pidOfServerProcess.ShouldBe(pidOfNewserverProcess, "Server node should be the same as from earlier."); + } + + [Fact] + public void BuildsWhileBuildIsRunningOnServer() + { + _env.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); + TransientTestFile project = _env.CreateFile("testProject.proj", printPidContents); + TransientTestFile sleepProject = _env.CreateFile("napProject.proj", sleepingTaskContents); + + int pidOfServerProcess = -1; + Task? t = null; + try + { + // Start a server node and find its PID. + string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success, false, _output); + pidOfServerProcess = ParseNumber(output, "Server ID is "); + + t = Task.Run(() => + { + RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, sleepProject.Path, out _, false, _output); + }); + + // The server will soon be in use; make sure we don't try to use it before that happens. + Thread.Sleep(1000); + + Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "0"); + + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); + + Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); + + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Server ID is "), "The server should be otherwise occupied."); + pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); + ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "Process ID and Server ID should coincide."); + } + finally + { + if (pidOfServerProcess > -1) + { + ProcessExtensions.KillTree(Process.GetProcessById(pidOfServerProcess), 1000); + } + + if (t is not null) + { + t.Wait(); + } + } + } + + private int ParseNumber(string searchString, string toFind) + { + Regex regex = new(@$"{toFind}(\d+)"); + Match match = regex.Match(searchString); + return int.Parse(match.Groups[1].Value); + } + } +} diff --git a/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj b/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj index e301c3a16b3..ea90a86c84e 100644 --- a/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj +++ b/src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj @@ -28,6 +28,7 @@ RegistryDelegates.cs + RegistryHelper.cs diff --git a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj index e0f20bd2eec..e40af2fed60 100644 --- a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj +++ b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj @@ -57,6 +57,7 @@ TestEnvironment.cs + diff --git a/src/UnitTests.Shared/RunnerUtilities.cs b/src/UnitTests.Shared/RunnerUtilities.cs index 7911ea669d6..366b1bc4280 100644 --- a/src/UnitTests.Shared/RunnerUtilities.cs +++ b/src/UnitTests.Shared/RunnerUtilities.cs @@ -1,5 +1,4 @@ using Microsoft.Build.Shared; -using Microsoft.Build.Utilities; using System; using System.Diagnostics; using Xunit.Abstractions; @@ -18,7 +17,7 @@ public static class RunnerUtilities /// public static string ExecMSBuild(string msbuildParameters, out bool successfulExit, ITestOutputHelper outputHelper = null) { - return ExecMSBuild(PathToCurrentlyRunningMsBuildExe, msbuildParameters, out successfulExit, false, outputHelper); + return ExecMSBuild(PathToCurrentlyRunningMsBuildExe, msbuildParameters, out successfulExit, outputHelper: outputHelper); } /// @@ -72,12 +71,15 @@ private static string ResolveRuntimeExecutableName() /// public static string RunProcessAndGetOutput(string process, string parameters, out bool successfulExit, bool shellExecute = false, ITestOutputHelper outputHelper = null) { + outputHelper?.WriteLine($"{DateTime.Now.ToString("hh:mm:ss tt")}:RunProcessAndGetOutput:1"); + if (shellExecute) { // we adjust the psi data manually because on net core using ProcessStartInfo.UseShellExecute throws NotImplementedException AdjustForShellExecution(ref process, ref parameters); } + outputHelper?.WriteLine($"{DateTime.Now.ToString("hh:mm:ss tt")}:RunProcessAndGetOutput:2"); var psi = new ProcessStartInfo(process) { CreateNoWindow = true, @@ -87,11 +89,13 @@ public static string RunProcessAndGetOutput(string process, string parameters, o UseShellExecute = false, Arguments = parameters }; - var output = string.Empty; + string output = string.Empty; + int pid = -1; + outputHelper?.WriteLine($"{DateTime.Now.ToString("hh:mm:ss tt")}:RunProcessAndGetOutput:3"); using (var p = new Process { EnableRaisingEvents = true, StartInfo = psi }) { - p.OutputDataReceived += delegate (object sender, DataReceivedEventArgs args) + DataReceivedEventHandler handler = delegate (object sender, DataReceivedEventArgs args) { if (args != null) { @@ -99,13 +103,8 @@ public static string RunProcessAndGetOutput(string process, string parameters, o } }; - p.ErrorDataReceived += delegate (object sender, DataReceivedEventArgs args) - { - if (args != null) - { - output += args.Data + "\r\n"; - } - }; + p.OutputDataReceived += handler; + p.ErrorDataReceived += handler; outputHelper?.WriteLine("Executing [{0} {1}]", process, parameters); Console.WriteLine("Executing [{0} {1}]", process, parameters); @@ -114,19 +113,35 @@ public static string RunProcessAndGetOutput(string process, string parameters, o p.BeginOutputReadLine(); p.BeginErrorReadLine(); p.StandardInput.Dispose(); + + if (!p.WaitForExit(30_000)) + { + // Let's not create a unit test for which we need more than 30 sec to execute. + // Please consider carefully if you would like to increase the timeout. + p.KillTree(1000); + throw new TimeoutException($"Test failed due to timeout: process {p.Id} is active for more than 30 sec."); + } + + // We need the WaitForExit call without parameters because our processing of output/error streams is not synchronous. + // See https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process.waitforexit?view=net-6.0#system-diagnostics-process-waitforexit(system-int32). + // The overload WaitForExit() waits for the error and output to be handled. The WaitForExit(int timeout) overload does not, so we could lose the data. p.WaitForExit(); + pid = p.Id; successfulExit = p.ExitCode == 0; } outputHelper?.WriteLine("==== OUTPUT ===="); outputHelper?.WriteLine(output); + outputHelper?.WriteLine("Process ID is " + pid + "\r\n"); outputHelper?.WriteLine("=============="); Console.WriteLine("==== OUTPUT ===="); Console.WriteLine(output); + Console.WriteLine("Process ID is " + pid + "\r\n"); Console.WriteLine("=============="); + output += "Process ID is " + pid + "\r\n"; return output; } } From 90d6f322ac9e08ce654d0f764d1f5039fc8bf9ab Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Tue, 7 Jun 2022 17:17:05 +0200 Subject: [PATCH 17/43] Added cancelation support for client. (#7659) * Added cancellation support for client. * Added cancellation support for client. * Fixing wrong merge * Removed "Cancelled" exit type * Resolving comments --- src/Build/BackEnd/Client/MSBuildClient.cs | 52 +++++++++---------- .../BackEnd/Client/MSBuildClientExitType.cs | 6 +-- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 15 ++---- .../BackEnd/Node/ServerNodeBuildCancel.cs | 2 +- .../PublicAPI/net/PublicAPI.Unshipped.txt | 3 +- .../netstandard/PublicAPI.Unshipped.txt | 3 +- src/MSBuild/XMake.cs | 14 ++--- 7 files changed, 37 insertions(+), 58 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 23f7853ed34..74a9a6d8e90 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -188,6 +188,9 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can { case 0: HandleCancellation(); + // After the cancelation, we want to wait to server gracefuly finish the build. + // We have to replace the cancelation handle, because WaitAny would cause to repeatedly hit this branch of code. + waitHandles[0] = CancellationToken.None.WaitHandle; break; case 1: @@ -196,8 +199,7 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can case 2: while (packetPump.ReceivedPacketsQueue.TryDequeue(out INodePacket? packet) && - !_buildFinished && - !cancellationToken.IsCancellationRequested) + !_buildFinished) { if (packet != null) { @@ -230,7 +232,24 @@ private void SupportVT100() } } - private void SendCancelCommand(NamedPipeClientStream nodeStream) => throw new NotImplementedException(); + private bool TrySendPacket(Func packetResolver) + { + INodePacket? packet = null; + try + { + packet = packetResolver(); + WritePacket(_nodeStream, packet); + CommunicationsUtilities.Trace($"Command packet of type '{packet.Type}' sent..."); + } + catch (Exception ex) + { + CommunicationsUtilities.Trace($"Failed to send command packet of type '{packet?.Type.ToString() ?? "Unknown"}' to server: {0}", ex); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; + return false; + } + + return true; + } /// /// Launches MSBuild server. @@ -278,23 +297,9 @@ private bool TryLaunchServer() return true; } - private bool TrySendBuildCommand(string commandLine) - { - try - { - ServerNodeBuildCommand buildCommand = GetServerNodeBuildCommand(commandLine); - WritePacket(_nodeStream, buildCommand); - CommunicationsUtilities.Trace("Build command sent..."); - } - catch (Exception ex) - { - CommunicationsUtilities.Trace("Failed to send build command to server: {0}", ex); - _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; - return false; - } + private bool TrySendBuildCommand(string commandLine) => TrySendPacket(() => GetServerNodeBuildCommand(commandLine)); - return true; - } + private bool TrySendCancelCommand() => TrySendPacket(() => new ServerNodeBuildCancel()); private ServerNodeBuildCommand GetServerNodeBuildCommand(string commandLine) { @@ -331,14 +336,9 @@ private ServerNodeHandshake GetHandshake() /// private void HandleCancellation() { - // TODO. - // Send cancellation command to server. - // SendCancelCommand(_nodeStream); + TrySendCancelCommand(); - Console.WriteLine("MSBuild client cancelled."); - CommunicationsUtilities.Trace("MSBuild client cancelled."); - _exitResult.MSBuildClientExitType = MSBuildClientExitType.Cancelled; - _buildFinished = true; + CommunicationsUtilities.Trace("MSBuild client sent cancelation command."); } /// diff --git a/src/Build/BackEnd/Client/MSBuildClientExitType.cs b/src/Build/BackEnd/Client/MSBuildClientExitType.cs index 70bbc0113c8..c72bc0a6878 100644 --- a/src/Build/BackEnd/Client/MSBuildClientExitType.cs +++ b/src/Build/BackEnd/Client/MSBuildClientExitType.cs @@ -24,10 +24,6 @@ public enum MSBuildClientExitType /// The build stopped unexpectedly, for example, /// because a named pipe between the server and the client was unexpectedly closed. /// - Unexpected, - /// - /// The build was cancelled. - /// - Cancelled + Unexpected } } diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index f795a3eceae..5017de2d535 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -20,8 +20,6 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket { private readonly Func _buildFunction; - private readonly Action _onCancel; - /// /// The endpoint used to talk to the host. /// @@ -62,14 +60,11 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket /// private readonly bool _debugCommunications; - private Task? _buildTask; - private string _serverBusyMutexName = default!; - public OutOfProcServerNode(Func buildFunction, Action onCancel) + public OutOfProcServerNode(Func buildFunction) { _buildFunction = buildFunction; - _onCancel = onCancel; new Dictionary(); _debugCommunications = (Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM") == "1"); @@ -279,14 +274,14 @@ private void HandlePacket(INodePacket packet) HandleServerNodeBuildCommandAsync((ServerNodeBuildCommand)packet); break; case NodePacketType.ServerNodeBuildCancel: - _onCancel(); + BuildManager.DefaultBuildManager.CancelAllSubmissions(); break; } } private void HandleServerNodeBuildCommandAsync(ServerNodeBuildCommand command) { - _buildTask = Task.Run(() => + Task.Run(() => { try { @@ -298,10 +293,6 @@ private void HandleServerNodeBuildCommandAsync(ServerNodeBuildCommand command) _shutdownReason = NodeEngineShutdownReason.Error; _shutdownEvent.Set(); } - finally - { - _buildTask = null; - } }); } diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs b/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs index 349c1b8170d..fba7f613819 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs @@ -5,7 +5,7 @@ namespace Microsoft.Build.BackEnd { internal sealed class ServerNodeBuildCancel : INodePacket - { + { public NodePacketType Type => NodePacketType.ServerNodeBuildCancel; public void Translate(ITranslator translator) diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index a39e6b88d8c..445f48157ac 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -9,12 +9,11 @@ Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitResult() -> v Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.set -> void Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.Cancelled = 5 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.OutOfProcServerNode -Microsoft.Build.Execution.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction, System.Action onCancel) -> void +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 1019cb1d919..eff393cd99f 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -9,12 +9,11 @@ Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitResult() -> v Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.set -> void Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.Cancelled = 5 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Execution.MSBuildClientExitType Microsoft.Build.Execution.OutOfProcServerNode -Microsoft.Build.Execution.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction, System.Action onCancel) -> void +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/XMake.cs b/src/MSBuild/XMake.cs index 8ee56e53d11..fd1e59da287 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -224,6 +224,8 @@ string[] args int exitCode; if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave17_4) && Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName) == "1") { + Console.CancelKeyPress += Console_CancelKeyPress; + DebuggerLaunchCheck(); // Use the client app to execute build in msbuild server. Opt-in feature. @@ -876,8 +878,7 @@ private static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs { if (e.SpecialKey == ConsoleSpecialKey.ControlBreak) { - e.Cancel = false; // required; the process will now be terminated rudely - return; + Environment.Exit(1); // the process will now be terminated rudely } e.Cancel = true; // do not terminate rudely @@ -2702,14 +2703,7 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool return (exitCode, exitType.ToString()); }; - Action onCancel = () => - { - Console.WriteLine(ResourceUtilities.GetResourceString("AbortingBuild")); - - BuildManager.DefaultBuildManager.CancelAllSubmissions(); - }; - - OutOfProcServerNode node = new(buildFunction, onCancel); + OutOfProcServerNode node = new(buildFunction); s_isServerNode = true; shutdownReason = node.Run(out nodeException); From 4b42b69046ae70cf5490773e77334bd01fc32a88 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Thu, 16 Jun 2022 05:08:10 -0700 Subject: [PATCH 18/43] Fix graceful disconnection (#7701) * Handle race condition * Clean running server nodes in tests. --- .../Communications/NodeEndpointInProc.cs | 6 ++ src/Build/BackEnd/Node/OutOfProcServerNode.cs | 11 +-- ...Microsoft.Build.Framework.UnitTests.csproj | 1 + src/MSBuild.UnitTests/MSBuildServer_Tests.cs | 77 ++++++++++--------- src/Shared/INodeEndpoint.cs | 6 ++ src/Shared/NodeEndpointOutOfProcBase.cs | 31 +++++++- src/Shared/UnitTests/TestEnvironment.cs | 28 +++++++ 7 files changed, 116 insertions(+), 44 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs index fe81fa4298d..35dcda21565 100644 --- a/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs +++ b/src/Build/BackEnd/Components/Communications/NodeEndpointInProc.cs @@ -221,6 +221,12 @@ public void SendData(INodePacket packet) EnqueuePacket(packet); } } + + public void ClientWillDisconnect() + { + // We do not need to do anything here for InProc node. + } + #endregion #region Internal Methods diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 5017de2d535..982da1af8ea 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -214,13 +214,13 @@ private NodeEngineShutdownReason HandleShutdown(out Exception? exception) { CommunicationsUtilities.Trace("Shutting down with reason: {0}, and exception: {1}.", _shutdownReason, _shutdownException); - exception = _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); - if (_nodeEndpoint.LinkStatus == LinkStatus.Active) - { - _nodeEndpoint.OnLinkStatusChanged -= OnLinkStatusChanged; - } + exception = _shutdownException; + _nodeEndpoint.OnLinkStatusChanged -= OnLinkStatusChanged; _nodeEndpoint.Disconnect(); CommunicationsUtilities.Trace("Shut down complete."); @@ -339,6 +339,7 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) // 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); diff --git a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj index b66f3a66b80..e3b953a332a 100644 --- a/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj +++ b/src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj @@ -34,6 +34,7 @@ + diff --git a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs index a7ecdde6176..28b9ae44e3e 100644 --- a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs +++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs @@ -109,7 +109,7 @@ public void MSBuildServerTest() Thread.Sleep(1000); // Kill the server - ProcessExtensions.KillTree(Process.GetProcessById(pidOfServerProcess), 1000); + Process.GetProcessById(pidOfServerProcess).KillTree(1000); }); // Start long-lived task execution @@ -120,9 +120,12 @@ public void MSBuildServerTest() // Ensure that a new build can still succeed and that its server node is different. output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); newPidOfInitialProcess = ParseNumber(output, "Process ID is "); int newServerProcessId = ParseNumber(output, "Server ID is "); + // Register process to clean up (be killed) after tests ends. + _env.WithTransientProcess(newServerProcessId); newPidOfInitialProcess.ShouldNotBe(pidOfInitialProcess, "Process started by two MSBuild executions should be different."); newPidOfInitialProcess.ShouldNotBe(newServerProcessId, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); pidOfServerProcess.ShouldNotBe(newServerProcessId, "Node used by both the first and second build should not be the same."); @@ -138,6 +141,8 @@ public void VerifyMixedLegacyBehavior() success.ShouldBeTrue(); int pidOfInitialProcess = ParseNumber(output, "Process ID is "); int pidOfServerProcess = ParseNumber(output, "Server ID is "); + // Register process to clean up (be killed) after tests ends. + _env.WithTransientProcess(pidOfServerProcess); pidOfInitialProcess.ShouldNotBe(pidOfServerProcess, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); Environment.SetEnvironmentVariable("MSBUILDUSESERVER", ""); @@ -154,6 +159,12 @@ public void VerifyMixedLegacyBehavior() pidOfNewserverProcess = ParseNumber(output, "Server ID is "); pidOfInitialProcess.ShouldNotBe(pidOfNewserverProcess, "We started a server node to execute the target rather than running it in-proc, so its pid should be different."); pidOfServerProcess.ShouldBe(pidOfNewserverProcess, "Server node should be the same as from earlier."); + + if (pidOfServerProcess != pidOfNewserverProcess) + { + // Register process to clean up (be killed) after tests ends. + _env.WithTransientProcess(pidOfNewserverProcess); + } } [Fact] @@ -163,48 +174,40 @@ public void BuildsWhileBuildIsRunningOnServer() TransientTestFile project = _env.CreateFile("testProject.proj", printPidContents); TransientTestFile sleepProject = _env.CreateFile("napProject.proj", sleepingTaskContents); - int pidOfServerProcess = -1; - Task? t = null; - try - { - // Start a server node and find its PID. - string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success, false, _output); - pidOfServerProcess = ParseNumber(output, "Server ID is "); + int pidOfServerProcess; + Task t; + // Start a server node and find its PID. + string output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out bool success, false, _output); + pidOfServerProcess = ParseNumber(output, "Server ID is "); + _env.WithTransientProcess(pidOfServerProcess); - t = Task.Run(() => - { - RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, sleepProject.Path, out _, false, _output); - }); + t = Task.Run(() => + { + RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, sleepProject.Path, out _, false, _output); + }); - // The server will soon be in use; make sure we don't try to use it before that happens. - Thread.Sleep(1000); + // The server will soon be in use; make sure we don't try to use it before that happens. + Thread.Sleep(1000); - Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "0"); + Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "0"); - output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); - success.ShouldBeTrue(); - ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); - Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); + Environment.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); - output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); - success.ShouldBeTrue(); - pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Server ID is "), "The server should be otherwise occupied."); - pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); - ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "Process ID and Server ID should coincide."); - } - finally - { - if (pidOfServerProcess > -1) - { - ProcessExtensions.KillTree(Process.GetProcessById(pidOfServerProcess), 1000); - } - - if (t is not null) - { - t.Wait(); - } - } + output = RunnerUtilities.ExecMSBuild(BuildEnvironmentHelper.Instance.CurrentMSBuildExePath, project.Path, out success, false, _output); + success.ShouldBeTrue(); + pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Server ID is "), "The server should be otherwise occupied."); + pidOfServerProcess.ShouldNotBe(ParseNumber(output, "Process ID is "), "There should not be a server node for this build."); + ParseNumber(output, "Server ID is ").ShouldBe(ParseNumber(output, "Process ID is "), "Process ID and Server ID should coincide."); + + // Clean up process and tasks + // 1st kill registered processes + _env.Dispose(); + // 2nd wait for sleep task which will ends as soon as the process is killed above. + t.Wait(); } private int ParseNumber(string searchString, string toFind) diff --git a/src/Shared/INodeEndpoint.cs b/src/Shared/INodeEndpoint.cs index cb8ce4a4c0a..ef2f319f023 100644 --- a/src/Shared/INodeEndpoint.cs +++ b/src/Shared/INodeEndpoint.cs @@ -103,5 +103,11 @@ LinkStatus LinkStatus /// The packet to be sent. void SendData(INodePacket packet); #endregion + + /// + /// Called when we are about to send last packet to finalize graceful disconnection with client. + /// This is needed to handle race condition when both client and server is gracefully about to close connection. + /// + void ClientWillDisconnect(); } } diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 6477869dc05..0be21ce32c0 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -73,6 +73,13 @@ internal abstract class NodeEndpointOutOfProcBase : INodeEndpoint /// private AutoResetEvent _terminatePacketPump; + /// + /// True if this side is gracefully disconnecting. + /// In such case we have sent last packet to client side and we expect + /// client will soon broke pipe connection - unless server do it first. + /// + private bool _isClientDisconnecting; + /// /// The thread which runs the asynchronous packet pump /// @@ -179,6 +186,14 @@ public void SendData(INodePacket packet) } } + /// + /// Called when we are about to send last packet to finalize graceful disconnection with client. + /// + public void ClientWillDisconnect() + { + _isClientDisconnecting = true; + } + #endregion #region Construction @@ -312,6 +327,7 @@ private void InitializeAsyncPacketThread() { lock (_asyncDataMonitor) { + _isClientDisconnecting = false; _packetPump = new Thread(PacketPumpProc); _packetPump.IsBackground = true; _packetPump.Name = "OutOfProc Endpoint Packet Pump"; @@ -548,14 +564,25 @@ private void RunReadLoop(Stream localReadPipe, Stream localWritePipe, // Incomplete read. Abort. if (bytesRead == 0) { - CommunicationsUtilities.Trace("Parent disconnected abruptly"); + if (_isClientDisconnecting) + { + CommunicationsUtilities.Trace("Parent disconnected gracefully."); + // Do not change link status to failed as this could make node think connection has failed + // and recycle node, while this is perfectly expected and handled race condition + // (both client and node is about to close pipe and client can be faster). + } + else + { + CommunicationsUtilities.Trace("Parent disconnected abruptly."); + ChangeLinkStatus(LinkStatus.Failed); + } } else { CommunicationsUtilities.Trace("Incomplete header read from server. {0} of {1} bytes read", bytesRead, headerByte.Length); + ChangeLinkStatus(LinkStatus.Failed); } - ChangeLinkStatus(LinkStatus.Failed); exitLoop = true; break; } diff --git a/src/Shared/UnitTests/TestEnvironment.cs b/src/Shared/UnitTests/TestEnvironment.cs index 6ede3f2d7fb..2db94fa9e83 100644 --- a/src/Shared/UnitTests/TestEnvironment.cs +++ b/src/Shared/UnitTests/TestEnvironment.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; @@ -328,6 +329,15 @@ public TransientTestState SetCurrentDirectory(string newWorkingDirectory) return WithTransientTestState(new TransientWorkingDirectory(newWorkingDirectory)); } + /// + /// Register process ID to be finished/killed after tests ends. + /// + public TransientTestProcess WithTransientProcess(int processId) + { + TransientTestProcess transientTestProcess = new(processId); + return WithTransientTestState(transientTestProcess); + } + #endregion private class DefaultOutput : ITestOutputHelper @@ -560,6 +570,24 @@ public override void Revert() } } + public class TransientTestProcess : TransientTestState + { + private readonly int _processId; + + public TransientTestProcess(int processId) + { + _processId = processId; + } + + public override void Revert() + { + if (_processId > -1) + { + Process.GetProcessById(_processId).KillTree(1000); + } + } + } + public class TransientTestFile : TransientTestState { From 39a56d0daa531e24aff5f04bcbd03dc20bcbec83 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Thu, 16 Jun 2022 19:14:29 +0200 Subject: [PATCH 19/43] Moving public types not intended to use externally to "Microsoft.Build.Experimental" namespace. (#7688) --- .../BackEnd/RedirectConsoleWriter_Tests.cs | 2 +- src/Build/BackEnd/Client/MSBuildClient.cs | 3 +- .../BackEnd/Client/MSBuildClientExitResult.cs | 2 +- .../BackEnd/Client/MSBuildClientExitType.cs | 2 +- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 3 +- .../PublicAPI/net/PublicAPI.Unshipped.txt | 36 +++++++++---------- .../netstandard/PublicAPI.Unshipped.txt | 36 +++++++++---------- src/MSBuild/MSBuildClientApp.cs | 2 +- src/MSBuild/XMake.cs | 1 + 9 files changed, 45 insertions(+), 42 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs index 9e58d151b66..121ea908677 100644 --- a/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs +++ b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs @@ -5,7 +5,7 @@ using System; using System.Text; using System.Threading.Tasks; -using Microsoft.Build.Execution; +using Microsoft.Build.Experimental; using Xunit; namespace Microsoft.Build.Engine.UnitTests.BackEnd diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 74a9a6d8e90..fad0f82acea 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -12,11 +12,12 @@ using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.Client; using Microsoft.Build.Eventing; +using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Internal; using Microsoft.Build.Shared; -namespace Microsoft.Build.Execution +namespace Microsoft.Build.Experimental { /// /// This class is the public entry point for executing builds in msbuild server. diff --git a/src/Build/BackEnd/Client/MSBuildClientExitResult.cs b/src/Build/BackEnd/Client/MSBuildClientExitResult.cs index 648e755f002..8cb466741fa 100644 --- a/src/Build/BackEnd/Client/MSBuildClientExitResult.cs +++ b/src/Build/BackEnd/Client/MSBuildClientExitResult.cs @@ -1,7 +1,7 @@ // 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.Execution +namespace Microsoft.Build.Experimental { /// /// Enumeration of the various ways in which the MSBuildClient execution can exit. diff --git a/src/Build/BackEnd/Client/MSBuildClientExitType.cs b/src/Build/BackEnd/Client/MSBuildClientExitType.cs index c72bc0a6878..b8061b3295e 100644 --- a/src/Build/BackEnd/Client/MSBuildClientExitType.cs +++ b/src/Build/BackEnd/Client/MSBuildClientExitType.cs @@ -1,6 +1,6 @@ // 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.Execution +namespace Microsoft.Build.Experimental { public enum MSBuildClientExitType { diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 982da1af8ea..531ece6f3f1 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -10,8 +10,9 @@ using Microsoft.Build.Shared; using Microsoft.Build.Internal; using System.Threading.Tasks; +using Microsoft.Build.Execution; -namespace Microsoft.Build.Execution +namespace Microsoft.Build.Experimental { /// /// This class represents an implementation of INode for out-of-proc server nodes aka MSBuild server diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index 445f48157ac..da542899bb9 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,19 +1,19 @@ Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void -Microsoft.Build.Execution.MSBuildClient -Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult -Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string msbuildLocation) -> void -Microsoft.Build.Execution.MSBuildClientExitResult -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitResult() -> void -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.set -> void -Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Execution.MSBuildClientExitType -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 +Microsoft.Build.Experimental.MSBuildClient +Microsoft.Build.Experimental.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Experimental.MSBuildClientExitResult +Microsoft.Build.Experimental.MSBuildClient.MSBuildClient(string msbuildLocation) -> void +Microsoft.Build.Experimental.MSBuildClientExitResult +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitResult() -> void +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitType.set -> void +Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.OutOfProcServerNode +Microsoft.Build.Experimental.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction) -> void +Microsoft.Build.Experimental.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 eff393cd99f..da542899bb9 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,19 +1,19 @@ Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void -Microsoft.Build.Execution.MSBuildClient -Microsoft.Build.Execution.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Execution.MSBuildClientExitResult -Microsoft.Build.Execution.MSBuildClient.MSBuildClient(string msbuildLocation) -> void -Microsoft.Build.Execution.MSBuildClientExitResult -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitResult() -> void -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitResult.MSBuildClientExitType.set -> void -Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Execution.MSBuildClientExitType -Microsoft.Build.Execution.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Execution.MSBuildClientExitType -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 +Microsoft.Build.Experimental.MSBuildClient +Microsoft.Build.Experimental.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Experimental.MSBuildClientExitResult +Microsoft.Build.Experimental.MSBuildClient.MSBuildClient(string msbuildLocation) -> void +Microsoft.Build.Experimental.MSBuildClientExitResult +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitResult() -> void +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitType.set -> void +Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.OutOfProcServerNode +Microsoft.Build.Experimental.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction) -> void +Microsoft.Build.Experimental.OutOfProcServerNode.Run(out System.Exception shutdownException) -> Microsoft.Build.Execution.NodeEngineShutdownReason diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index ef39eb8f1b3..fb6a1fa4f02 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -2,9 +2,9 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using Microsoft.Build.Execution; using Microsoft.Build.Shared; using System.Threading; +using Microsoft.Build.Experimental; #if RUNTIME_TYPE_NETCORE || MONO using System.IO; diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index fd1e59da287..ebe6acb78af 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -35,6 +35,7 @@ using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; using BinaryLogger = Microsoft.Build.Logging.BinaryLogger; using Microsoft.Build.Shared.Debugging; +using Microsoft.Build.Experimental; #nullable disable From 1c9ed2757ed13e8b4af22913d5f9a2a2525270c2 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Mon, 20 Jun 2022 10:04:07 -0700 Subject: [PATCH 20/43] Propagete Console properties to MSBuild Server (#7683) Fixes #7658 Context See #7658 Changes Made MSBuild Server clients detects: ConsoleBufferWidth, AcceptAnsiColorCodes, ConsoleIsScreen, ConsoleBackgroundColor of current console and sent it to Server in ServerNodeBuildCommand. Server overrides ConsoleConfigueation so our loggers can get target console configuration. Testing Manual Notes There are no expected functional changes for NON Server and hence also VS scenarios. Co-authored-by: Forgind Co-authored-by: AR-May <67507805+AR-May@users.noreply.github.com> --- src/Build/BackEnd/Client/MSBuildClient.cs | 112 ++++++- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 26 +- .../BackEnd/Node/ServerNodeBuildCommand.cs | 16 +- src/Build/Logging/BaseConsoleLogger.cs | 276 +++++++++++++++--- src/Build/Logging/ConsoleLogger.cs | 9 +- .../ParallelLogger/ParallelConsoleLogger.cs | 2 +- src/Framework/NativeMethods.cs | 1 + src/MSBuild/Resources/Strings.resx | 4 +- src/MSBuild/Resources/xlf/Strings.cs.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.de.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.es.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.fr.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.it.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.ja.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.ko.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.pl.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.ru.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.tr.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 6 +- src/MSBuild/XMake.cs | 2 +- 22 files changed, 439 insertions(+), 87 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index fad0f82acea..273cfd69f84 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -11,6 +11,7 @@ using System.Threading; using Microsoft.Build.BackEnd; using Microsoft.Build.BackEnd.Client; +using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Eventing; using Microsoft.Build.Execution; using Microsoft.Build.Framework; @@ -78,6 +79,11 @@ public sealed class MSBuildClient private int _numConsoleWritePackets; private long _sizeOfConsoleWritePackets; + /// + /// Capture configuration of Client Console. + /// + private TargetConsoleConfiguration? _consoleConfiguration; + /// /// Public constructor with parameters. /// @@ -148,6 +154,8 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can return _exitResult; } + ConfigureAndQueryConsoleProperties(); + // Send build command. // Let's send it outside the packet pump so that we easier and quicker deal with possible issues with connection to server. MSBuildEventSource.Log.MSBuildServerBuildStart(commandLine); @@ -177,11 +185,6 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can packetPump.PacketReceivedEvent }; - if (NativeMethodsShared.IsWindows) - { - SupportVT100(); - } - while (!_buildFinished) { int index = WaitHandle.WaitAny(waitHandles); @@ -223,14 +226,100 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can return _exitResult; } - private void SupportVT100() + private void ConfigureAndQueryConsoleProperties() + { + var (acceptAnsiColorCodes, outputIsScreen) = QueryIsScreenAndTryEnableAnsiColorCodes(); + int bufferWidth = QueryConsoleBufferWidth(); + ConsoleColor backgroundColor = QueryConsoleBackgroundColor(); + + _consoleConfiguration = new TargetConsoleConfiguration(bufferWidth, acceptAnsiColorCodes, outputIsScreen, backgroundColor); + } + + private (bool acceptAnsiColorCodes, bool outputIsScreen) QueryIsScreenAndTryEnableAnsiColorCodes() + { + bool acceptAnsiColorCodes = false; + bool outputIsScreen = false; + + if (NativeMethodsShared.IsWindows) + { + try + { + IntPtr stdOut = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + if (NativeMethodsShared.GetConsoleMode(stdOut, out uint consoleMode)) + { + bool success; + if ((consoleMode & NativeMethodsShared.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == NativeMethodsShared.ENABLE_VIRTUAL_TERMINAL_PROCESSING && + (consoleMode & NativeMethodsShared.DISABLE_NEWLINE_AUTO_RETURN) == NativeMethodsShared.DISABLE_NEWLINE_AUTO_RETURN) + { + // Console is already in required state + success = true; + } + else + { + consoleMode |= NativeMethodsShared.ENABLE_VIRTUAL_TERMINAL_PROCESSING | NativeMethodsShared.DISABLE_NEWLINE_AUTO_RETURN; + success = NativeMethodsShared.SetConsoleMode(stdOut, consoleMode); + } + + if (success) + { + acceptAnsiColorCodes = true; + } + + uint fileType = NativeMethodsShared.GetFileType(stdOut); + // The std out is a char type(LPT or Console) + outputIsScreen = fileType == NativeMethodsShared.FILE_TYPE_CHAR; + acceptAnsiColorCodes &= outputIsScreen; + } + } + catch (Exception ex) + { + CommunicationsUtilities.Trace("MSBuild client warning: problem during enabling support for VT100: {0}.", ex); + } + } + else + { + // On posix OSes we expect console always supports VT100 coloring unless it is redirected + acceptAnsiColorCodes = outputIsScreen = !Console.IsOutputRedirected; + } + + return (acceptAnsiColorCodes: acceptAnsiColorCodes, outputIsScreen: outputIsScreen); + } + + private int QueryConsoleBufferWidth() { - IntPtr stdOut = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); - if (NativeMethodsShared.GetConsoleMode(stdOut, out uint consoleMode)) + int consoleBufferWidth = -1; + try { - consoleMode |= NativeMethodsShared.ENABLE_VIRTUAL_TERMINAL_PROCESSING | NativeMethodsShared.DISABLE_NEWLINE_AUTO_RETURN; - NativeMethodsShared.SetConsoleMode(stdOut, consoleMode); + consoleBufferWidth = Console.BufferWidth; } + catch (Exception ex) + { + // on Win8 machines while in IDE Console.BufferWidth will throw (while it talks to native console it gets "operation aborted" native error) + // this is probably temporary workaround till we understand what is the reason for that exception + CommunicationsUtilities.Trace("MSBuild client warning: problem during querying console buffer width.", ex); + } + + return consoleBufferWidth; + } + + /// + /// Some platforms do not allow getting current background color. There + /// is not way to check, but not-supported exception is thrown. Assume + /// black, but don't crash. + /// + private ConsoleColor QueryConsoleBackgroundColor() + { + ConsoleColor consoleBackgroundColor; + try + { + consoleBackgroundColor = Console.BackgroundColor; + } + catch (PlatformNotSupportedException) + { + consoleBackgroundColor = ConsoleColor.Black; + } + + return consoleBackgroundColor; } private bool TrySendPacket(Func packetResolver) @@ -324,7 +413,8 @@ private ServerNodeBuildCommand GetServerNodeBuildCommand(string commandLine) startupDirectory: Directory.GetCurrentDirectory(), buildProcessEnvironment: envVars, CultureInfo.CurrentCulture, - CultureInfo.CurrentUICulture); + CultureInfo.CurrentUICulture, + _consoleConfiguration!); } private ServerNodeHandshake GetHandshake() diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 531ece6f3f1..0053b91705b 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -11,6 +11,7 @@ using Microsoft.Build.Internal; using System.Threading.Tasks; using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd.Logging; namespace Microsoft.Build.Experimental { @@ -312,13 +313,34 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) return; } - // set build process context + // 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 + // Configure console configuration so Loggers can change their behavior based on Target (client) Console properties. + ConsoleConfiguration.Provider = command.ConsoleConfiguration; + + // 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; diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs index 48ab050cf1e..a83adf83e8c 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -1,10 +1,11 @@ // 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; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; namespace Microsoft.Build.BackEnd { @@ -18,6 +19,7 @@ internal sealed class ServerNodeBuildCommand : INodePacket private Dictionary _buildProcessEnvironment = default!; private CultureInfo _culture = default!; private CultureInfo _uiCulture = default!; + private TargetConsoleConfiguration _consoleConfiguration = default!; /// /// Retrieves the packet type. @@ -49,6 +51,11 @@ internal sealed class ServerNodeBuildCommand : INodePacket /// public CultureInfo UICulture => _uiCulture; + /// + /// Console configuration of Client. + /// + public TargetConsoleConfiguration ConsoleConfiguration => _consoleConfiguration; + /// /// Private constructor for deserialization /// @@ -56,13 +63,17 @@ private ServerNodeBuildCommand() { } - public ServerNodeBuildCommand(string commandLine, string startupDirectory, Dictionary buildProcessEnvironment, CultureInfo culture, CultureInfo uiCulture) + public ServerNodeBuildCommand(string commandLine, string startupDirectory, Dictionary buildProcessEnvironment, CultureInfo culture, CultureInfo uiCulture, + TargetConsoleConfiguration consoleConfiguration) { + ErrorUtilities.VerifyThrowInternalNull(consoleConfiguration, nameof(consoleConfiguration)); + _commandLine = commandLine; _startupDirectory = startupDirectory; _buildProcessEnvironment = buildProcessEnvironment; _culture = culture; _uiCulture = uiCulture; + _consoleConfiguration = consoleConfiguration; } /// @@ -76,6 +87,7 @@ public void Translate(ITranslator translator) translator.TranslateDictionary(ref _buildProcessEnvironment, StringComparer.OrdinalIgnoreCase); translator.TranslateCulture(ref _culture); translator.TranslateCulture(ref _uiCulture); + translator.Translate(ref _consoleConfiguration, TargetConsoleConfiguration.FactoryForDeserialization); } /// diff --git a/src/Build/Logging/BaseConsoleLogger.cs b/src/Build/Logging/BaseConsoleLogger.cs index ea87f587b70..20bd109a974 100644 --- a/src/Build/Logging/BaseConsoleLogger.cs +++ b/src/Build/Logging/BaseConsoleLogger.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -28,38 +29,8 @@ namespace Microsoft.Build.BackEnd.Logging internal abstract class BaseConsoleLogger : INodeLogger { - /// - /// When set, we'll try reading background color. - /// - private static bool _supportReadingBackgroundColor = true; - #region Properties - /// - /// Some platforms do not allow getting current background color. There - /// is not way to check, but not-supported exception is thrown. Assume - /// black, but don't crash. - /// - internal static ConsoleColor BackgroundColor - { - get - { - if (_supportReadingBackgroundColor) - { - try - { - return Console.BackgroundColor; - } - catch (PlatformNotSupportedException) - { - _supportReadingBackgroundColor = false; - } - } - - return ConsoleColor.Black; - } - } - /// /// Gets or sets the level of detail to show in the event log. /// @@ -314,16 +285,7 @@ internal void IsRunningWithCharacterFileType() if (NativeMethodsShared.IsWindows) { - // Get the std out handle - IntPtr stdHandle = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); - - if (stdHandle != NativeMethods.InvalidHandle) - { - uint fileType = NativeMethodsShared.GetFileType(stdHandle); - - // The std out is a char type(LPT or Console) - runningWithCharacterFileType = (fileType == NativeMethodsShared.FILE_TYPE_CHAR); - } + runningWithCharacterFileType = ConsoleConfiguration.OutputIsScreen; } } @@ -367,7 +329,7 @@ internal static void SetColor(ConsoleColor c) { try { - Console.ForegroundColor = TransformColor(c, BackgroundColor); + Console.ForegroundColor = TransformColor(c, ConsoleConfiguration.BackgroundColor); } catch (IOException) { @@ -480,7 +442,7 @@ internal void InitializeConsoleMethods(LoggerVerbosity logverbosity, WriteHandle try { - ConsoleColor c = BackgroundColor; + ConsoleColor c = ConsoleConfiguration.BackgroundColor; } catch (IOException) { @@ -1278,4 +1240,234 @@ private bool ApplyVerbosityParameter(string parameterValue) #endregion } + + /// + /// Console configuration needed for proper Console logging. + /// + internal interface IConsoleConfiguration + { + /// + /// Buffer width of destination Console. + /// Console loggers are supposed, on Windows OS, to be wrapping to avoid output trimming. + /// -1 console buffer width can't be obtained. + /// + int BufferWidth { get; } + + /// + /// True if console output accept ANSI colors codes. + /// False if output is redirected to non screen type such as file or nul. + /// + bool AcceptAnsiColorCodes { get; } + + /// + /// True if console output is screen. It is expected that non screen output is post-processed and often does not need wrapping and coloring. + /// False if output is redirected to non screen type such as file or nul. + /// + bool OutputIsScreen { get; } + + /// + /// Background color of client console, -1 if not detectable + /// Some platforms do not allow getting current background color. There + /// is not way to check, but not-supported exception is thrown. Assume + /// black, but don't crash. + /// + ConsoleColor BackgroundColor { get; } + } + + /// + /// Console configuration of target Console at which we will render output. + /// It is supposed to be Console from other process to which output from this process will be redirected. + /// + internal class TargetConsoleConfiguration : IConsoleConfiguration, ITranslatable + { + private int _bufferWidth; + private bool _acceptAnsiColorCodes; + private bool _outputIsScreen; + private ConsoleColor _backgroundColor; + + public TargetConsoleConfiguration(int bufferWidth, bool acceptAnsiColorCodes, bool outputIsScreen, ConsoleColor backgroundColor) + { + _bufferWidth = bufferWidth; + _acceptAnsiColorCodes = acceptAnsiColorCodes; + _outputIsScreen = outputIsScreen; + _backgroundColor = backgroundColor; + } + + /// + /// Constructor for deserialization + /// + private TargetConsoleConfiguration() + { + } + + public int BufferWidth => _bufferWidth; + + public bool AcceptAnsiColorCodes => _acceptAnsiColorCodes; + + public bool OutputIsScreen => _outputIsScreen; + + public ConsoleColor BackgroundColor => _backgroundColor; + + public void Translate(ITranslator translator) + { + translator.Translate(ref _bufferWidth); + translator.Translate(ref _acceptAnsiColorCodes); + translator.Translate(ref _outputIsScreen); + translator.TranslateEnum(ref _backgroundColor, (int)_backgroundColor); + } + + internal static TargetConsoleConfiguration FactoryForDeserialization(ITranslator translator) + { + TargetConsoleConfiguration configuration = new(); + configuration.Translate(translator); + return configuration; + } + } + + /// + /// Console configuration of current process Console. + /// + internal class InProcessConsoleConfiguration : IConsoleConfiguration + { + /// + /// When set, we'll try reading background color. + /// + private static bool s_supportReadingBackgroundColor = true; + + public int BufferWidth => Console.BufferWidth; + + public bool AcceptAnsiColorCodes + { + get + { + bool acceptAnsiColorCodes = false; + if (NativeMethodsShared.IsWindows && !Console.IsOutputRedirected) + { + try + { + IntPtr stdOut = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + if (NativeMethodsShared.GetConsoleMode(stdOut, out uint consoleMode)) + { + acceptAnsiColorCodes = (consoleMode & NativeMethodsShared.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; + } + } + catch (Exception ex) + { + Debug.Assert(false, $"MSBuild client warning: problem during enabling support for VT100: {ex}."); + } + } + else + { + // On posix OSes we expect console always supports VT100 coloring unless it is redirected + acceptAnsiColorCodes = !Console.IsOutputRedirected; + } + + return acceptAnsiColorCodes; + } + } + + public ConsoleColor BackgroundColor + { + get + { + if (s_supportReadingBackgroundColor) + { + try + { + return Console.BackgroundColor; + } + catch (PlatformNotSupportedException) + { + s_supportReadingBackgroundColor = false; + } + } + + return ConsoleColor.Black; + } + } + + public bool OutputIsScreen + { + get + { + bool isScreen = false; + + if (NativeMethodsShared.IsWindows) + { + // Get the std out handle + IntPtr stdHandle = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + + if (stdHandle != NativeMethods.InvalidHandle) + { + uint fileType = NativeMethodsShared.GetFileType(stdHandle); + + // The std out is a char type(LPT or Console) + isScreen = fileType == NativeMethodsShared.FILE_TYPE_CHAR; + } + } + else + { + isScreen = !Console.IsOutputRedirected; + } + + return isScreen; + } + } + } + + /// + /// Target console configuration. + /// If console output is redirected to other process console, like for example MSBuild Server does, + /// we need to know property of target/final console at which our output will be rendered. + /// If console is rendered at current process Console, we grab properties from Console and/or by WinAPI. + /// + internal static class ConsoleConfiguration + { + /// + /// Get or set current target console configuration provider. + /// + public static IConsoleConfiguration Provider + { + get { return Instance.s_instance; } + set { Instance.s_instance = value; } + } + + private static class Instance + { + // Explicit static constructor to tell C# compiler + // not to mark type as beforefieldinit + static Instance() + { + } + + internal static IConsoleConfiguration s_instance = new InProcessConsoleConfiguration(); + } + + /// + /// Buffer width of destination Console. + /// Console loggers are supposed, on Windows OS, to be wrapping to avoid output trimming. + /// -1 console buffer width can't be obtained. + /// + public static int BufferWidth => Provider.BufferWidth; + + /// + /// True if console output accept ANSI colors codes. + /// False if output is redirected to non screen type such as file or nul. + /// + public static bool AcceptAnsiColorCodes => Provider.AcceptAnsiColorCodes; + + /// + /// Background color of client console, -1 if not detectable + /// Some platforms do not allow getting current background color. There + /// is not way to check, but not-supported exception is thrown. Assume + /// black, but don't crash. + /// + public static ConsoleColor BackgroundColor => Provider.BackgroundColor; + + /// + /// True if console output is screen. It is expected that non screen output is post-processed and often does not need wrapping and coloring. + /// False if output is redirected to non screen type such as file or nul. + /// + public static bool OutputIsScreen => Provider.OutputIsScreen; + } } diff --git a/src/Build/Logging/ConsoleLogger.cs b/src/Build/Logging/ConsoleLogger.cs index 543667811bf..d4320ced186 100644 --- a/src/Build/Logging/ConsoleLogger.cs +++ b/src/Build/Logging/ConsoleLogger.cs @@ -3,6 +3,7 @@ using System; +using Microsoft.Build.BackEnd.Logging; using Microsoft.Build.Framework; using Microsoft.Build.Shared; @@ -109,6 +110,7 @@ private void InitializeBaseConsoleLogger() bool useMPLogger = false; bool disableConsoleColor = false; bool forceConsoleColor = false; + bool preferConsoleColor = false; if (!string.IsNullOrEmpty(_parameters)) { string[] parameterComponents = _parameters.Split(BaseConsoleLogger.parameterDelimiters); @@ -132,10 +134,15 @@ private void InitializeBaseConsoleLogger() { forceConsoleColor = true; } + if (string.Equals(param, "PREFERCONSOLECOLOR", StringComparison.OrdinalIgnoreCase)) + { + // Use ansi color codes if current target console do support it + preferConsoleColor = ConsoleConfiguration.AcceptAnsiColorCodes; + } } } - if (forceConsoleColor) + if (forceConsoleColor || (!disableConsoleColor && preferConsoleColor)) { _colorSet = BaseConsoleLogger.SetColorAnsi; _colorReset = BaseConsoleLogger.ResetColorAnsi; diff --git a/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs index b0d0c7eb7b3..fe63ce04137 100644 --- a/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs +++ b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs @@ -88,7 +88,7 @@ private void CheckIfOutputSupportsAlignment() // Get the size of the console buffer so messages can be formatted to the console width try { - _bufferWidth = Console.BufferWidth; + _bufferWidth = ConsoleConfiguration.BufferWidth; _alignMessages = true; } catch (Exception) diff --git a/src/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs index 5b990331277..2f928186628 100644 --- a/src/Framework/NativeMethods.cs +++ b/src/Framework/NativeMethods.cs @@ -60,6 +60,7 @@ internal static class NativeMethods internal static HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); internal static IntPtr NullIntPtr = new IntPtr(0); + internal static IntPtr InvalidHandle = new IntPtr(-1); // As defined in winnt.h: internal const ushort PROCESSOR_ARCHITECTURE_INTEL = 0; diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index c69053daa0c..c33245ceae6 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -319,7 +319,9 @@ Copyright (C) Microsoft Corporation. All rights reserved. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index 7ccdc9ab43e..677d56274cc 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -512,13 +512,15 @@ Copyright (C) Microsoft Corporation. Všechna práva vyhrazena. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleloggerparameters:<parameters> + -consoleloggerparameters:<parameters> Parametry protokolovacího nástroje konzoly. (Krátký tvar: -clp) Dostupné parametry: PerformanceSummary – zobrazí dobu zpracování úloh, cílů diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index e2c30e3ac4f..12af6eb477d 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -509,13 +509,15 @@ Beispiel: mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleloggerparameters:<Parameter> + -consoleloggerparameters:<Parameter> Parameter für die Konsolenprotokollierung. (Kurzform: -clp) Folgende Parameter sind verfügbar: PerformanceSummary: Zeigt die in Aufgaben, Zielen und diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index 0195e35549f..d089c52d291 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -513,13 +513,15 @@ Copyright (C) Microsoft Corporation. Todos los derechos reservados. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<parámetros> + -consoleLoggerParameters:<parámetros> Parámetros del registrador de consola. (Forma corta: -clp) Los parámetros disponibles son: PerformanceSummary: muestra el tiempo empleado en tareas, destinos diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index 3065aadd997..b42e8b553ec 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -509,13 +509,15 @@ Copyright (C) Microsoft Corporation. Tous droits réservés. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<paramètres> + -consoleLoggerParameters:<paramètres> Paramètres du journaliseur de la console. (Forme abrégée : -clp) Paramètres disponibles : PerformanceSummary--Affiche la durée des tâches, des cibles diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 9c59271398b..80ecce717c9 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -519,13 +519,15 @@ Esempio: mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<parametri> + -consoleLoggerParameters:<parametri> Parametri per il logger di console. Forma breve: -clp. I parametri disponibili sono: PerformanceSummary: indica il tempo impiegato per le diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 9c2bee6bc61..75e8bb75f83 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -509,13 +509,15 @@ Copyright (C) Microsoft Corporation.All rights reserved. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<parameters> + -consoleLoggerParameters:<parameters> コンソール ロガーへのパラメーターです。(短縮形: -clp) 利用可能なパラメーター: PerformanceSummary--タスク、ターゲット、プロジェクトにかかった時間を diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index 3d3b611cc74..b2cbad8117d 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -509,13 +509,15 @@ Copyright (C) Microsoft Corporation. All rights reserved. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<parameters> + -consoleLoggerParameters:<parameters> 콘솔 로거에 대한 매개 변수입니다. (약식: -clp) 사용 가능한 매개 변수는 다음과 같습니다. PerformanceSummary--작업, 대상 및 프로젝트에서 소요된 시간을 diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 2c2f7464b68..e7dec45a8d7 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -519,13 +519,15 @@ Copyright (C) Microsoft Corporation. Wszelkie prawa zastrzeżone. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<parametry> + -consoleLoggerParameters:<parametry> Parametry rejestratora konsoli. (Krótka wersja: -clp) Dostępne parametry: PerformanceSummary — pokazuje czas spędzony diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index 6dc052fc5bd..80b55165e6b 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -510,13 +510,15 @@ isoladamente. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<parameters> + -consoleLoggerParameters:<parameters> Parâmetros do agente do console. (Forma abreviada: -clp) Os parâmetros disponíveis são: PerformanceSummary – mostrar o tempo gasto nas tarefas, nos destinos diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index b39acf70142..d59d0abd1c4 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -508,13 +508,15 @@ Copyright (C) Microsoft Corporation. All rights reserved. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<параметры> + -consoleLoggerParameters:<параметры> Параметры журнала консоли. (Краткая форма: -clp) Доступны следующие параметры: PerformanceSummary--выводить время, затраченное на выполнение задач, diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index eb65858ec3a..266cc59600b 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -509,13 +509,15 @@ Telif Hakkı (C) Microsoft Corporation. Tüm hakları saklıdır. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<parametreler> + -consoleLoggerParameters:<parametreler> Konsol günlükçüsü için parametreler. (Kısa biçim: -clp) Kullanılabilir parametreler: PerformanceSummary--Görevlerde, hedeflerde ve diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index a4fc1ecdd32..ccd7c4c9392 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -509,13 +509,15 @@ Copyright (C) Microsoft Corporation. All rights reserved. mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleloggerparameters:<parameters> + -consoleloggerparameters:<parameters> 控制台记录器的参数。(缩写: -clp) 可用参数包括: PerformanceSummary -- 显示在任务、目标和项目上 diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index 2dc040b10b9..f6306ec8319 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -509,13 +509,15 @@ Copyright (C) Microsoft Corporation. 著作權所有,並保留一切權利。 mode. This logging style is on by default. ForceConsoleColor--Use ANSI console colors even if console does not support it - Verbosity--overrides the -verbosity setting for this + PreferConsoleColor--Use ANSI console colors only if + target console does support it + Verbosity--overrides the -verbosity setting for this logger. Example: -consoleLoggerParameters:PerformanceSummary;NoSummary; Verbosity=minimal - -consoleLoggerParameters:<參數> + -consoleLoggerParameters:<參數> 主控台記錄器的參數。(簡短形式: -clp) 可用的參數為: PerformanceSummary--顯示工作、目標 diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index ebe6acb78af..373e4a58aa3 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -3215,7 +3215,7 @@ List loggers // Always use ANSI escape codes when the build is initiated by server if (s_isServerNode) { - consoleParameters = AggregateParameters(consoleParameters, new[] { "FORCECONSOLECOLOR" }); + consoleParameters = $"PREFERCONSOLECOLOR;{consoleParameters}"; } // Check to see if there is a possibility we will be logging from an out-of-proc node. From dec0fbc3beec227375d72693aac2b6c963a1fdc4 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Tue, 21 Jun 2022 11:27:41 -0700 Subject: [PATCH 21/43] Fix arguments passing to MSBuild Server (#7723) * Send command line as string[] in dotnet builds. --- src/Build/BackEnd/Client/MSBuildClient.cs | 48 ++++++++++++++----- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 14 +++++- .../BackEnd/Node/ServerNodeBuildCommand.cs | 20 +++++++- .../PublicAPI/net/PublicAPI.Unshipped.txt | 7 +-- .../netstandard/PublicAPI.Unshipped.txt | 7 +-- src/MSBuild/MSBuildClientApp.cs | 22 +++------ src/MSBuild/XMake.cs | 10 +--- 7 files changed, 83 insertions(+), 45 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 273cfd69f84..b2329da56b5 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -38,6 +38,16 @@ public sealed class MSBuildClient /// private readonly string _msbuildLocation; + /// + /// The command line to process. + /// The first argument on the command line is assumed to be the name/path of the executable, and is ignored. + /// +#if FEATURE_GET_COMMANDLINE + private readonly string _commandLine; +#else + private readonly string[] _commandLine; +#endif + /// /// The MSBuild client execution result. /// @@ -87,14 +97,23 @@ public sealed class MSBuildClient /// /// Public constructor with parameters. /// + /// The command line to process. The first argument + /// on the command line is assumed to be the name/path of the executable, and is ignored /// Full path to current MSBuild.exe if executable is MSBuild.exe, /// or to version of MSBuild.dll found to be associated with the current process. - public MSBuildClient(string msbuildLocation) + public MSBuildClient( +#if FEATURE_GET_COMMANDLINE + string commandLine, +#else + string[] commandLine, +#endif + string msbuildLocation) { _serverEnvironmentVariables = new(); _exitResult = new(); // dll & exe locations + _commandLine = commandLine; _msbuildLocation = msbuildLocation; // Client <-> Server communication stream @@ -114,15 +133,20 @@ public MSBuildClient(string msbuildLocation) /// Orchestrates the execution of the build on the server, /// responsible for client-server communication. /// - /// The command line to process. The first argument - /// on the command line is assumed to be the name/path of the executable, and - /// is ignored. /// Cancellation token. /// A value of type that indicates whether the build succeeded, /// or the manner in which it failed. - public MSBuildClientExitResult Execute(string commandLine, CancellationToken cancellationToken) + public MSBuildClientExitResult Execute(CancellationToken cancellationToken) { - CommunicationsUtilities.Trace("Executing build with command line '{0}'", commandLine); + // Command line in one string used only in human readable content. + string descriptiveCommandLine = +#if FEATURE_GET_COMMANDLINE + _commandLine; +#else + string.Join(" ", _commandLine); +#endif + + CommunicationsUtilities.Trace("Executing build with command line '{0}'", descriptiveCommandLine); string serverRunningMutexName = OutOfProcServerNode.GetRunningServerMutexName(_handshake); string serverBusyMutexName = OutOfProcServerNode.GetBusyServerMutexName(_handshake); @@ -158,8 +182,8 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can // Send build command. // Let's send it outside the packet pump so that we easier and quicker deal with possible issues with connection to server. - MSBuildEventSource.Log.MSBuildServerBuildStart(commandLine); - if (!TrySendBuildCommand(commandLine)) + MSBuildEventSource.Log.MSBuildServerBuildStart(descriptiveCommandLine); + if (!TrySendBuildCommand()) { CommunicationsUtilities.Trace("Failure to connect to a server."); _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; @@ -221,7 +245,7 @@ public MSBuildClientExitResult Execute(string commandLine, CancellationToken can _exitResult.MSBuildClientExitType = MSBuildClientExitType.Unexpected; } - MSBuildEventSource.Log.MSBuildServerBuildStop(commandLine, _numConsoleWritePackets, _sizeOfConsoleWritePackets, _exitResult.MSBuildClientExitType.ToString(), _exitResult.MSBuildAppExitTypeString); + MSBuildEventSource.Log.MSBuildServerBuildStop(descriptiveCommandLine, _numConsoleWritePackets, _sizeOfConsoleWritePackets, _exitResult.MSBuildClientExitType.ToString(), _exitResult.MSBuildAppExitTypeString); CommunicationsUtilities.Trace("Build finished."); return _exitResult; } @@ -387,11 +411,11 @@ private bool TryLaunchServer() return true; } - private bool TrySendBuildCommand(string commandLine) => TrySendPacket(() => GetServerNodeBuildCommand(commandLine)); + private bool TrySendBuildCommand() => TrySendPacket(() => GetServerNodeBuildCommand()); private bool TrySendCancelCommand() => TrySendPacket(() => new ServerNodeBuildCancel()); - private ServerNodeBuildCommand GetServerNodeBuildCommand(string commandLine) + private ServerNodeBuildCommand GetServerNodeBuildCommand() { Dictionary envVars = new(); @@ -409,7 +433,7 @@ private ServerNodeBuildCommand GetServerNodeBuildCommand(string commandLine) envVars[Traits.UseMSBuildServerEnvVarName] = "0"; return new ServerNodeBuildCommand( - commandLine, + _commandLine, startupDirectory: Directory.GetCurrentDirectory(), buildProcessEnvironment: envVars, CultureInfo.CurrentCulture, diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 0053b91705b..6a0b4c94242 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -20,7 +20,17 @@ namespace Microsoft.Build.Experimental /// public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacketHandler { - private readonly Func _buildFunction; + /// + /// A callback used to execute command line build. + /// + public delegate (int exitCode, string exitType) BuildCallback( +#if FEATURE_GET_COMMANDLINE + string commandLine); +#else + string[] commandLine); +#endif + + private readonly BuildCallback _buildFunction; /// /// The endpoint used to talk to the host. @@ -64,7 +74,7 @@ public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacket private string _serverBusyMutexName = default!; - public OutOfProcServerNode(Func buildFunction) + public OutOfProcServerNode(BuildCallback buildFunction) { _buildFunction = buildFunction; new Dictionary(); diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs index a83adf83e8c..32c551b78a8 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -14,7 +14,11 @@ namespace Microsoft.Build.BackEnd /// internal sealed class ServerNodeBuildCommand : INodePacket { +#if FEATURE_GET_COMMANDLINE private string _commandLine = default!; +#else + private string[] _commandLine = default!; +#endif private string _startupDirectory = default!; private Dictionary _buildProcessEnvironment = default!; private CultureInfo _culture = default!; @@ -27,9 +31,13 @@ internal sealed class ServerNodeBuildCommand : INodePacket public NodePacketType Type => NodePacketType.ServerNodeBuildCommand; /// - /// The startup directory + /// Command line including arguments /// +#if FEATURE_GET_COMMANDLINE public string CommandLine => _commandLine; +#else + public string[] CommandLine => _commandLine; +#endif /// /// The startup directory @@ -63,7 +71,15 @@ private ServerNodeBuildCommand() { } - public ServerNodeBuildCommand(string commandLine, string startupDirectory, Dictionary buildProcessEnvironment, CultureInfo culture, CultureInfo uiCulture, + public ServerNodeBuildCommand( +#if FEATURE_GET_COMMANDLINE + string commandLine, +#else + string[] commandLine, +#endif + string startupDirectory, + Dictionary buildProcessEnvironment, + CultureInfo culture, CultureInfo uiCulture, TargetConsoleConfiguration consoleConfiguration) { ErrorUtilities.VerifyThrowInternalNull(consoleConfiguration, nameof(consoleConfiguration)); diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index da542899bb9..c4de070516f 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Experimental.MSBuildClient -Microsoft.Build.Experimental.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Experimental.MSBuildClientExitResult -Microsoft.Build.Experimental.MSBuildClient.MSBuildClient(string msbuildLocation) -> void +Microsoft.Build.Experimental.MSBuildClient.Execute(System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Experimental.MSBuildClientExitResult +Microsoft.Build.Experimental.MSBuildClient.MSBuildClient(string commandLine, string msbuildLocation) -> void Microsoft.Build.Experimental.MSBuildClientExitResult Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void @@ -15,5 +15,6 @@ Microsoft.Build.Experimental.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.B Microsoft.Build.Experimental.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.OutOfProcServerNode -Microsoft.Build.Experimental.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction) -> void +Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback +Microsoft.Build.Experimental.OutOfProcServerNode.OutOfProcServerNode(Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback buildFunction) -> void Microsoft.Build.Experimental.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 da542899bb9..20875ae93b2 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,7 +1,7 @@ Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void Microsoft.Build.Experimental.MSBuildClient -Microsoft.Build.Experimental.MSBuildClient.Execute(string commandLine, System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Experimental.MSBuildClientExitResult -Microsoft.Build.Experimental.MSBuildClient.MSBuildClient(string msbuildLocation) -> void +Microsoft.Build.Experimental.MSBuildClient.Execute(System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Experimental.MSBuildClientExitResult +Microsoft.Build.Experimental.MSBuildClient.MSBuildClient(string[] commandLine, string msbuildLocation) -> void Microsoft.Build.Experimental.MSBuildClientExitResult Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildAppExitTypeString.get -> string Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildAppExitTypeString.set -> void @@ -15,5 +15,6 @@ Microsoft.Build.Experimental.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.B Microsoft.Build.Experimental.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.OutOfProcServerNode -Microsoft.Build.Experimental.OutOfProcServerNode.OutOfProcServerNode(System.Func buildFunction) -> void +Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback +Microsoft.Build.Experimental.OutOfProcServerNode.OutOfProcServerNode(Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback buildFunction) -> void Microsoft.Build.Experimental.OutOfProcServerNode.Run(out System.Exception shutdownException) -> Microsoft.Build.Execution.NodeEngineShutdownReason diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index fb6a1fa4f02..b5187d198a1 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -46,9 +46,8 @@ CancellationToken cancellationToken return Execute( commandLine, - cancellationToken, - msbuildLocation - ); + msbuildLocation, + cancellationToken); } /// @@ -57,9 +56,9 @@ CancellationToken cancellationToken /// The command line to process. The first argument /// on the command line is assumed to be the name/path of the executable, and /// is ignored. - /// Cancellation token. /// Full path to current MSBuild.exe if executable is MSBuild.exe, /// or to version of MSBuild.dll found to be associated with the current process. + /// Cancellation token. /// A value of type that indicates whether the build succeeded, /// or the manner in which it failed. public static MSBuildApp.ExitType Execute( @@ -68,18 +67,11 @@ public static MSBuildApp.ExitType Execute( #else string[] commandLine, #endif - CancellationToken cancellationToken, - string msbuildLocation - ) + string msbuildLocation, + CancellationToken cancellationToken) { - // MSBuild client orchestration. -#if !FEATURE_GET_COMMANDLINE - string commandLineString = string.Join(" ", commandLine); -#else - string commandLineString = commandLine; -#endif - MSBuildClient msbuildClient = new MSBuildClient(msbuildLocation); - MSBuildClientExitResult exitResult = msbuildClient.Execute(commandLineString, cancellationToken); + MSBuildClient msbuildClient = new MSBuildClient(commandLine, msbuildLocation); + MSBuildClientExitResult exitResult = msbuildClient.Execute(cancellationToken); if (exitResult.MSBuildClientExitType == MSBuildClientExitType.ServerBusy || exitResult.MSBuildClientExitType == MSBuildClientExitType.ConnectionError) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index 373e4a58aa3..419713a18cf 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -2679,7 +2679,7 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool { // 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) => + OutOfProcServerNode.BuildCallback buildFunction = (commandLine) => { int exitCode; ExitType exitType; @@ -2690,13 +2690,7 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool } else { - exitType = Execute( -#if FEATURE_GET_COMMANDLINE - commandLine -#else - QuotingUtilities.SplitUnquoted(commandLine).ToArray() -#endif - ); + exitType = Execute(commandLine); exitCode = exitType == ExitType.Success ? 0 : 1; } exitCode = exitType == ExitType.Success ? 0 : 1; From b92eeef502ee8f4797174104143726649bcea653 Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Thu, 23 Jun 2022 18:24:10 +0200 Subject: [PATCH 22/43] Fix wrong merge. --- .../Components/Communications/NodeProviderOutOfProcBase.cs | 5 ----- src/Build/PublicAPI/net/PublicAPI.Unshipped.txt | 2 +- src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index d6d775ab8ca..a3163aa4764 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -22,11 +22,6 @@ #endif using Microsoft.Build.Internal; using Microsoft.Build.Shared; -<<<<<<< HEAD -======= -using Microsoft.Build.Shared.FileSystem; -using BackendNativeMethods = Microsoft.Build.BackEnd.NativeMethods; ->>>>>>> main using Task = System.Threading.Tasks.Task; using Microsoft.Build.Framework; using Microsoft.Build.BackEnd.Logging; diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index c4de070516f..7ce7bc26ef8 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void +Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool useAsynchronousLogging, bool reuseProjectRootElementCache) -> void Microsoft.Build.Experimental.MSBuildClient Microsoft.Build.Experimental.MSBuildClient.Execute(System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Experimental.MSBuildClientExitResult Microsoft.Build.Experimental.MSBuildClient.MSBuildClient(string commandLine, string msbuildLocation) -> void diff --git a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 5431e1220ed..485d920eb6a 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -1,4 +1,4 @@ -Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool reuseProjectRootElementCache) -> void +Microsoft.Build.Evaluation.ProjectCollection.ProjectCollection(System.Collections.Generic.IDictionary globalProperties, System.Collections.Generic.IEnumerable loggers, System.Collections.Generic.IEnumerable remoteLoggers, Microsoft.Build.Evaluation.ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool useAsynchronousLogging, bool reuseProjectRootElementCache) -> void Microsoft.Build.Experimental.MSBuildClient Microsoft.Build.Experimental.MSBuildClient.Execute(System.Threading.CancellationToken cancellationToken) -> Microsoft.Build.Experimental.MSBuildClientExitResult Microsoft.Build.Experimental.MSBuildClient.MSBuildClient(string[] commandLine, string msbuildLocation) -> void From 8d2ea0f400c304778b1febe29c98cfc3bfb9166a Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Fri, 24 Jun 2022 10:12:45 +0200 Subject: [PATCH 23/43] Make logging asynchronous --- src/MSBuild/XMake.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index ae56e3a71e2..b3ff12d4a35 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -1095,7 +1095,7 @@ string[] commandLine cpuCount, onlyLogCriticalEvents, loadProjectsReadOnly: !preprocessOnly, - useAsynchronousLogging: false, + useAsynchronousLogging: true, reuseProjectRootElementCache: s_isServerNode ); From 981a7bd678b85ad4a7cfdd93da948eb448046ca7 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Tue, 28 Jun 2022 14:57:44 +0200 Subject: [PATCH 24/43] Update src/Build/BackEnd/Client/MSBuildClient.cs Co-authored-by: Forgind --- src/Build/BackEnd/Client/MSBuildClient.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index b2329da56b5..f8aafa5bd5d 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -481,7 +481,8 @@ private void HandlePacket(INodePacket packet) case NodePacketType.ServerNodeBuildResult: HandleServerNodeBuildResult((ServerNodeBuildResult)packet); break; - default: throw new InvalidOperationException($"Unexpected packet type {packet.GetType().Name}"); + default: + throw new InvalidOperationException($"Unexpected packet type {packet.GetType().Name}"); } } From 93c62a398e01f5b5c6459dbfad6f8e3d609aaf57 Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Wed, 29 Jun 2022 00:26:54 +0200 Subject: [PATCH 25/43] MSBuild Server: Always run MSBuild nodes with MSBUILDUSESERVER disabled. (#7745) * Always run msbuild nodes with MSBUILDUSESERVER disabled. * Address PR comment. --- src/Build/BackEnd/Client/MSBuildClient.cs | 8 ----- .../Components/Communications/NodeLauncher.cs | 30 +++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index f8aafa5bd5d..45250d3ae6b 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -386,12 +386,8 @@ private bool TryLaunchServer() "/nodemode:8" }; - string? useMSBuildServerEnvVarValue = Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName); try { - // Disable MSBuild server for a child process, preventing an infinite recurson. - Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, ""); - NodeLauncher nodeLauncher = new NodeLauncher(); CommunicationsUtilities.Trace("Starting Server..."); Process msbuildProcess = nodeLauncher.Start(_msbuildLocation, string.Join(" ", msBuildServerOptions)); @@ -403,10 +399,6 @@ private bool TryLaunchServer() _exitResult.MSBuildClientExitType = MSBuildClientExitType.LaunchError; return false; } - finally - { - Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, useMSBuildServerEnvVarValue); - } return true; } diff --git a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs index 652f7dda74c..9a08a3940a7 100644 --- a/src/Build/BackEnd/Components/Communications/NodeLauncher.cs +++ b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs @@ -23,6 +23,16 @@ internal class NodeLauncher /// Creates a new MSBuild process /// public Process Start(string msbuildLocation, string commandLineArgs) + { + // Disable MSBuild server for a child process. + // In case of starting msbuild server it prevents an infinite recurson. In case of starting msbuild node we also do not want this variable to be set. + return DisableMSBuildServer(() => StartInternal(msbuildLocation, commandLineArgs)); + } + + /// + /// Creates a new MSBuild process + /// + private Process StartInternal(string msbuildLocation, string commandLineArgs) { // Should always have been set already. ErrorUtilities.VerifyThrowInternalLength(msbuildLocation, nameof(msbuildLocation)); @@ -177,5 +187,25 @@ out processInfo return Process.GetProcessById(childProcessId); } } + + private Process DisableMSBuildServer(Func func) + { + string useMSBuildServerEnvVarValue = Environment.GetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName); + try + { + if (useMSBuildServerEnvVarValue is not null) + { + Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, "0"); + } + return func(); + } + finally + { + if (useMSBuildServerEnvVarValue is not null) + { + Environment.SetEnvironmentVariable(Traits.UseMSBuildServerEnvVarName, useMSBuildServerEnvVarValue); + } + } + } } } From 8032e698241a78222fd47b399aece71d815c4764 Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Wed, 29 Jun 2022 15:54:30 +0200 Subject: [PATCH 26/43] Fix copyright info. (#7758) --- src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs | 5 ++--- .../Components/Communications/ServerNodeEndpointOutOfProc.cs | 5 ++--- src/Build/BackEnd/Node/ConsoleOutput.cs | 5 ++--- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 4 ++-- src/Build/BackEnd/Node/ServerNamedMutex.cs | 4 ++-- src/Build/BackEnd/Node/ServerNodeBuildCancel.cs | 5 ++--- src/Build/BackEnd/Node/ServerNodeBuildCommand.cs | 4 ++-- src/Build/BackEnd/Node/ServerNodeBuildResult.cs | 5 ++--- src/MSBuild.UnitTests/MSBuildServer_Tests.cs | 4 ++-- 9 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs index 121ea908677..c3e937addd0 100644 --- a/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs +++ b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Text; diff --git a/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs b/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs index 0590a95c1ba..9616f90964b 100644 --- a/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs +++ b/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Build.Internal; diff --git a/src/Build/BackEnd/Node/ConsoleOutput.cs b/src/Build/BackEnd/Node/ConsoleOutput.cs index 8cf4092bc84..2a685c594d7 100644 --- a/src/Build/BackEnd/Node/ConsoleOutput.cs +++ b/src/Build/BackEnd/Node/ConsoleOutput.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// +// 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 { diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 6a0b4c94242..09906c1da36 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -1,5 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; diff --git a/src/Build/BackEnd/Node/ServerNamedMutex.cs b/src/Build/BackEnd/Node/ServerNamedMutex.cs index 2d6ab100d10..0988295e2b2 100644 --- a/src/Build/BackEnd/Node/ServerNamedMutex.cs +++ b/src/Build/BackEnd/Node/ServerNamedMutex.cs @@ -1,5 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Threading; diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs b/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs index fba7f613819..67cd7f0df7f 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// +// 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 { diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs index 32c551b78a8..41e33589a84 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -1,5 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.Generic; diff --git a/src/Build/BackEnd/Node/ServerNodeBuildResult.cs b/src/Build/BackEnd/Node/ServerNodeBuildResult.cs index b7b9b3e7a2c..4ea012ebafd 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildResult.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildResult.cs @@ -1,6 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// +// 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 { diff --git a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs index 28b9ae44e3e..2e4c4ba5cf1 100644 --- a/src/MSBuild.UnitTests/MSBuildServer_Tests.cs +++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs @@ -1,5 +1,5 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Diagnostics; From 200fbb9a3eb065295cd7a934a6a9c8f50f4ec3f9 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Thu, 30 Jun 2022 10:06:58 +0200 Subject: [PATCH 27/43] Fixed some commented issues (#7759) * Fixed some commented issues --- .../BackEnd/RedirectConsoleWriter_Tests.cs | 15 +++++++------ src/Build/BackEnd/Client/MSBuildClient.cs | 21 ++++++++++--------- .../BackEnd/Client/MSBuildClientPacketPump.cs | 2 +- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 14 ------------- src/MSBuild/XMake.cs | 2 +- src/Shared/CommunicationsUtilities.cs | 11 ---------- src/Shared/NodeEndpointOutOfProcBase.cs | 1 - 7 files changed, 22 insertions(+), 44 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs index c3e937addd0..bd579597c2e 100644 --- a/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs +++ b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs @@ -2,9 +2,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.Build.Experimental; +using Shouldly; using Xunit; namespace Microsoft.Build.Engine.UnitTests.BackEnd @@ -15,14 +17,15 @@ public class RedirectConsoleWriter_Tests 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(); + using (TextWriter writer = OutOfProcServerNode.RedirectConsoleWriter.Create(text => sb.Append(text))) + { + writer.WriteLine("Line 1"); + await Task.Delay(300); + writer.Write("Line 2"); + } - Assert.Equal($"Line 1{Environment.NewLine}Line 2", sb.ToString()); + sb.ToString().ShouldBe($"Line 1{Environment.NewLine}Line 2"); } } } diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 45250d3ae6b..77a7b308af2 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -157,6 +157,7 @@ public MSBuildClientExitResult Execute(CancellationToken cancellationToken) CommunicationsUtilities.Trace("Server was not running. Starting server now."); if (!TryLaunchServer()) { + _exitResult.MSBuildClientExitType = MSBuildClientExitType.LaunchError; return _exitResult; } } @@ -391,11 +392,11 @@ private bool TryLaunchServer() NodeLauncher nodeLauncher = new NodeLauncher(); CommunicationsUtilities.Trace("Starting Server..."); Process msbuildProcess = nodeLauncher.Start(_msbuildLocation, string.Join(" ", msBuildServerOptions)); - CommunicationsUtilities.Trace("Server started with PID: {0}", msbuildProcess?.Id); + CommunicationsUtilities.Trace($"Server started with PID: {msbuildProcess?.Id}"); } catch (Exception ex) { - CommunicationsUtilities.Trace("Failed to launch the msbuild server: {0}", ex); + CommunicationsUtilities.Trace($"Failed to launch the msbuild server: {ex}"); _exitResult.MSBuildClientExitType = MSBuildClientExitType.LaunchError; return false; } @@ -422,7 +423,7 @@ private ServerNodeBuildCommand GetServerNodeBuildCommand() } // We remove env variable used to invoke MSBuild server as that might be equal to 1, so we do not get an infinite recursion here. - envVars[Traits.UseMSBuildServerEnvVarName] = "0"; + envVars.Remove(Traits.UseMSBuildServerEnvVarName); return new ServerNodeBuildCommand( _commandLine, @@ -453,8 +454,8 @@ private void HandleCancellation() /// private void HandlePacketPumpError(MSBuildClientPacketPump packetPump) { - CommunicationsUtilities.Trace("MSBuild client error: packet pump unexpectedly shut down: {0}", packetPump.PacketPumpException); - throw packetPump.PacketPumpException ?? new Exception("Packet pump unexpectedly shut down"); + CommunicationsUtilities.Trace($"MSBuild client error: packet pump unexpectedly shut down: {packetPump.PacketPumpException}"); + throw packetPump.PacketPumpException ?? new InternalErrorException("Packet pump unexpectedly shut down"); } /// @@ -495,7 +496,7 @@ private void HandleServerNodeConsoleWrite(ServerNodeConsoleWrite consoleWrite) private void HandleServerNodeBuildResult(ServerNodeBuildResult response) { - CommunicationsUtilities.Trace("Build response received: exit code {0}, exit type '{1}'", response.ExitCode, response.ExitType); + CommunicationsUtilities.Trace($"Build response received: exit code {response.ExitCode}, exit type '{response.ExitType}'"); _exitResult.MSBuildClientExitType = MSBuildClientExitType.Success; _exitResult.MSBuildAppExitTypeString = response.ExitType; _buildFinished = true; @@ -514,14 +515,14 @@ private bool TryConnectToServer(int timeout) int[] handshakeComponents = _handshake.RetrieveHandshakeComponents(); for (int i = 0; i < handshakeComponents.Length; i++) { - CommunicationsUtilities.Trace("Writing handshake part {0} ({1}) to pipe {2}", i, handshakeComponents[i], _pipeName); + CommunicationsUtilities.Trace($"Writing handshake part {i} ({handshakeComponents[i]}) to pipe {_pipeName}"); _nodeStream.WriteIntForHandshake(handshakeComponents[i]); } // This indicates that we have finished all the parts of our handshake; hopefully the endpoint has as well. _nodeStream.WriteEndOfHandshakeSignal(); - CommunicationsUtilities.Trace("Reading handshake from pipe {0}", _pipeName); + CommunicationsUtilities.Trace($"Reading handshake from pipe {_pipeName}"); #if NETCOREAPP2_1_OR_GREATER || MONO _nodeStream.ReadEndOfHandshakeSignal(false, 1000); @@ -529,11 +530,11 @@ private bool TryConnectToServer(int timeout) _nodeStream.ReadEndOfHandshakeSignal(false); #endif - CommunicationsUtilities.Trace("Successfully connected to pipe {0}...!", _pipeName); + CommunicationsUtilities.Trace($"Successfully connected to pipe {_pipeName}...!"); } catch (Exception ex) { - CommunicationsUtilities.Trace("Failed to connect to server: {0}", ex); + CommunicationsUtilities.Trace($"Failed to connect to server: {ex}"); _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; return false; } diff --git a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs index b2c82c88ab6..b59c5a79e27 100644 --- a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs +++ b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs @@ -273,7 +273,7 @@ private void RunReadLoop(Stream localStream, ManualResetEvent localPacketPumpShu #if FEATURE_APM result = localStream.BeginRead(headerByte, 0, headerByte.Length, null, null); #else - readTask = CommunicationsUtilities.ReadAsync(localStream, headerByte, headerByte.Length); + readTask = CommunicationsUtilities.ReadAsync(localStream, headerByte, headerByte.Length); #endif } } diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 09906c1da36..b3a45f28d32 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; -using System.Collections.Generic; using System.Collections.Concurrent; using System.IO; using System.Threading; @@ -67,18 +66,11 @@ public delegate (int exitCode, string exitType) BuildCallback( /// private Exception? _shutdownException = null; - /// - /// Flag indicating if we should debug communications or not. - /// - private readonly bool _debugCommunications; - private string _serverBusyMutexName = default!; public OutOfProcServerNode(BuildCallback buildFunction) { _buildFunction = buildFunction; - new Dictionary(); - _debugCommunications = (Environment.GetEnvironmentVariable("MSBUILDDEBUGCOMM") == "1"); _receivedPackets = new ConcurrentQueue(); _packetReceivedEvent = new AutoResetEvent(false); @@ -253,12 +245,6 @@ private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) _shutdownEvent.Set(); break; - case LinkStatus.Inactive: - break; - - case LinkStatus.Active: - break; - default: break; } diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index b3ff12d4a35..af15342f6f4 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -2692,8 +2692,8 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool else { exitType = Execute(commandLine); - exitCode = exitType == ExitType.Success ? 0 : 1; } + exitCode = exitType == ExitType.Success ? 0 : 1; return (exitCode, exitType.ToString()); diff --git a/src/Shared/CommunicationsUtilities.cs b/src/Shared/CommunicationsUtilities.cs index b26510bd3c7..f8106579fb7 100644 --- a/src/Shared/CommunicationsUtilities.cs +++ b/src/Shared/CommunicationsUtilities.cs @@ -148,17 +148,6 @@ internal ServerNodeHandshake(HandshakeOptions nodeType) { } - /// - /// 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); - } - public override int[] RetrieveHandshakeComponents() { return new int[] diff --git a/src/Shared/NodeEndpointOutOfProcBase.cs b/src/Shared/NodeEndpointOutOfProcBase.cs index 0be21ce32c0..4c5a3357063 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -14,7 +14,6 @@ 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; From 23fb4a6b3ea483d6948a34c28ca0031cf31c84c6 Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Fri, 1 Jul 2022 10:34:21 +0200 Subject: [PATCH 28/43] Update doc (#7740) * Update doc * Fixed typo * Fixed another typo * Update documentation/MSBuild-Server.md Co-authored-by: Forgind * Update documentation/MSBuild-Server.md Co-authored-by: Forgind * Update documentation/MSBuild-Server.md Co-authored-by: Forgind * Update documentation/MSBuild-Server.md Co-authored-by: Forgind * Update documentation/MSBuild-Server.md Co-authored-by: Forgind * Update documentation/MSBuild-Server.md Co-authored-by: Forgind * Resolving comments Co-authored-by: Forgind --- documentation/MSBuild-Server.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/documentation/MSBuild-Server.md b/documentation/MSBuild-Server.md index 355cd8e1383..9337295dac6 100644 --- a/documentation/MSBuild-Server.md +++ b/documentation/MSBuild-Server.md @@ -1,6 +1,11 @@ # MSBuild Server -MSBuild Server is basically an another type of node which can accept build request from clients and utilize worker nodes in current fashion to build projects. Main purpose of the server node is to avoid expensive MSBuild process start during build from tools like .NET SDK. +MSBuild Server nodes accept build requests from clients and use worker nodes in the current fashion to build projects. The main purpose of the server node is to preserve caches between builds and avoid expensive MSBuild process start operations during build from tools like the .NET SDK. + +## Usage + +The primary ways to use MSBuild are via Visual Studio and via the CLI using the `dotnet build`/`dotnet msbuild` commands. MSBuild Server is not supported in Visual Studio because Visual Studio itself works like MSBuild Server. For the CLI, the server functionality is enabled by default and can be disabled by setting the `DOTNET_CLI_DO_NOT_USE_MSBUILD_SERVER` environment variable to value `1`. +To re-enable MSBuild Server, remove the variable or set its value to `0`. ## Communication protocol @@ -8,12 +13,12 @@ The server node uses same IPC approach as current worker nodes - named pipes. Th 1. Try to connect to server - If server is not running, start new instance - - If server is busy, fallback to classic build + - If server is busy or the connection is broken, fall back to previous build behavior 2. Initiate handshake -2. Issue build command with `EntryNodeCommand` packet +2. Issue build command with `ServerNodeBuildCommand` packet 3. Read packets from pipe - - When `EntryNodeConsoleWrite` packet type is recieved, write content to appropriate output stream with respected coloring - - When `EntryNodeResponse` packet type is recieved, build is done and client writes trace message with exit code + - Write content to the appropriate output stream (respecting coloring) with the `ServerNodeConsoleWrite` packet + - After the build completes, the `ServerNodeBuildResult` packet indicates the exit code ### Pipe name convention & handshake @@ -25,7 +30,7 @@ Handshake is a procedure ensuring that client is connecting to a compatible serv Server requires to introduce new packet types for IPC. -`EntryNodeCommand` contains all of the information necessary for a server to run a build. +`ServerNodeBuildCommand` contains all of the information necessary for a server to run a build. | Property name | Type | Description | |---|---|---| @@ -34,21 +39,22 @@ Server requires to introduce new packet types for IPC. | BuildProcessEnvironment | IDictionary | Environment variables for current build | | Culture | CultureInfo | The culture value for current build | | UICulture | CultureInfo | The UI culture value for current build | +| ConsoleConfiguration | TargetConsoleConfiguration | Console configuration of target Console at which the output will be rendered | -`EntryNodeConsoleWrite` contains information for console output. +`ServerNodeConsoleWrite` contains information for console output. | Property name | Type | Description | |---|---|---| | Text | String | The text that is written to the output stream. It includes ANSI escape codes for formatting. | | OutputType | Byte | Identification of the output stream (1 = standard output, 2 = error output) | -`EntryNodeResponse` informs about finished build. +`ServerNodeBuildResult` indicates how the build finished. | Property name | Type | Description | |---|---|---| | ExitCode | Int32 | The exit code of the build | | ExitType | String | The exit type of the build | -`EntryNodeCancel` cancels the current build. +`ServerNodeBuildCancel` cancels the current build. This type is intentionally empty and properties for build cancelation could be added in future. From 7a293b27cfaeefe938662f49ba0f5ec01ccd13c6 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Mon, 11 Jul 2022 16:41:25 +0200 Subject: [PATCH 29/43] Added argument null checking --- src/Build/BackEnd/Client/MSBuildClientPacketPump.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs index b2c82c88ab6..75b79e38a64 100644 --- a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs +++ b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs @@ -68,6 +68,8 @@ internal sealed class MSBuildClientPacketPump : INodePacketHandler, INodePacketF public MSBuildClientPacketPump(Stream stream) { + ErrorUtilities.VerifyThrowArgumentNull(stream, nameof(stream)); + _stream = stream; _packetFactory = new NodePacketFactory(); From c9a1e6116a16540689d4fa542696228149e0fef1 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Mon, 11 Jul 2022 16:58:41 +0200 Subject: [PATCH 30/43] Delete dead commented code --- src/Build/BackEnd/Node/ServerNamedMutex.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Build/BackEnd/Node/ServerNamedMutex.cs b/src/Build/BackEnd/Node/ServerNamedMutex.cs index 2d6ab100d10..834d75941f8 100644 --- a/src/Build/BackEnd/Node/ServerNamedMutex.cs +++ b/src/Build/BackEnd/Node/ServerNamedMutex.cs @@ -29,13 +29,6 @@ public ServerNamedMutex(string mutexName, out bool createdNew) 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); } From 072b447518f95b2795aa200343cd4ecdde92dfaa Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Mon, 11 Jul 2022 17:05:31 +0200 Subject: [PATCH 31/43] Delete dead code --- src/Build/BackEnd/Node/ServerNamedMutex.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Build/BackEnd/Node/ServerNamedMutex.cs b/src/Build/BackEnd/Node/ServerNamedMutex.cs index 834d75941f8..d109ca586c2 100644 --- a/src/Build/BackEnd/Node/ServerNamedMutex.cs +++ b/src/Build/BackEnd/Node/ServerNamedMutex.cs @@ -40,21 +40,6 @@ public static bool WasOpen(string mutexName) 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) From 265f3b71ef986a49b5092e72e9abf45a69352392 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Mon, 11 Jul 2022 20:58:11 +0200 Subject: [PATCH 32/43] prefix mutexes name by msbuild --- src/Build/BackEnd/Client/MSBuildClient.cs | 2 +- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index b2329da56b5..2877a3d3e9d 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -371,7 +371,7 @@ private bool TrySendPacket(Func packetResolver) /// Whether MSBuild server was started successfully. private bool TryLaunchServer() { - string serverLaunchMutexName = $@"Global\server-launch-{_handshake.ComputeHash()}"; + string serverLaunchMutexName = $@"Global\msbuild-server-launch-{_handshake.ComputeHash()}"; using var serverLaunchMutex = ServerNamedMutex.OpenOrCreateMutex(serverLaunchMutexName, out bool mutexCreatedNew); if (!mutexCreatedNew) { diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 6a0b4c94242..5d5cbc20acf 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -153,10 +153,10 @@ internal static string GetPipeName(ServerNodeHandshake handshake) => NamedPipeUtil.GetPlatformSpecificPipeName($"MSBuildServer-{handshake.ComputeHash()}"); internal static string GetRunningServerMutexName(ServerNodeHandshake handshake) - => $@"Global\server-running-{handshake.ComputeHash()}"; + => $@"Global\msbuild-server-running-{handshake.ComputeHash()}"; internal static string GetBusyServerMutexName(ServerNodeHandshake handshake) - => $@"Global\server-busy-{handshake.ComputeHash()}"; + => $@"Global\msbuild-server-busy-{handshake.ComputeHash()}"; #region INodePacketFactory Members From 2aa65bac191ca53756c5f383273c1e63f052c7ae Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Mon, 11 Jul 2022 21:29:20 +0200 Subject: [PATCH 33/43] Reuse ConnectToPipeStream from NodeProviderOutOfProcBase --- src/Build/BackEnd/Client/MSBuildClient.cs | 22 +---- .../NodeProviderOutOfProcBase.cs | 81 +++++++++++-------- 2 files changed, 47 insertions(+), 56 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 2877a3d3e9d..1fd61f6a302 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -516,27 +516,7 @@ private bool TryConnectToServer(int timeout) { try { - _nodeStream.Connect(timeout); - - int[] handshakeComponents = _handshake.RetrieveHandshakeComponents(); - for (int i = 0; i < handshakeComponents.Length; i++) - { - CommunicationsUtilities.Trace("Writing handshake part {0} ({1}) to pipe {2}", i, handshakeComponents[i], _pipeName); - _nodeStream.WriteIntForHandshake(handshakeComponents[i]); - } - - // This indicates that we have finished all the parts of our handshake; hopefully the endpoint has as well. - _nodeStream.WriteEndOfHandshakeSignal(); - - CommunicationsUtilities.Trace("Reading handshake from pipe {0}", _pipeName); - -#if NETCOREAPP2_1_OR_GREATER || MONO - _nodeStream.ReadEndOfHandshakeSignal(false, 1000); -#else - _nodeStream.ReadEndOfHandshakeSignal(false); -#endif - - CommunicationsUtilities.Trace("Successfully connected to pipe {0}...!", _pipeName); + NodeProviderOutOfProcBase.ConnectToPipeStream(_nodeStream, _pipeName, _handshake, timeout); } catch (Exception ex) { diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index a3163aa4764..cd5a88127e0 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -416,7 +416,7 @@ private string GetProcessesToIgnoreKey(Handshake hostHandshake, int nodeProcessI #if !FEATURE_PIPEOPTIONS_CURRENTUSERONLY // This code needs to be in a separate method so that we don't try (and fail) to load the Windows-only APIs when JIT-ing the code // on non-Windows operating systems - private void ValidateRemotePipeSecurityOnWindows(NamedPipeClientStream nodeStream) + private static void ValidateRemotePipeSecurityOnWindows(NamedPipeClientStream nodeStream) { SecurityIdentifier identifier = WindowsIdentity.GetCurrent().Owner; #if FEATURE_PIPE_SECURITY @@ -451,40 +451,7 @@ private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake han try { - nodeStream.Connect(timeout); - -#if !FEATURE_PIPEOPTIONS_CURRENTUSERONLY - if (NativeMethodsShared.IsWindows && !NativeMethodsShared.IsMono) - { - // Verify that the owner of the pipe is us. This prevents a security hole where a remote node has - // been faked up with ACLs that would let us attach to it. It could then issue fake build requests back to - // us, potentially causing us to execute builds that do harmful or unexpected things. The pipe owner can - // only be set to the user's own SID by a normal, unprivileged process. The conditions where a faked up - // remote node could set the owner to something else would also let it change owners on other objects, so - // this would be a security flaw upstream of us. - ValidateRemotePipeSecurityOnWindows(nodeStream); - } -#endif - - int[] handshakeComponents = handshake.RetrieveHandshakeComponents(); - for (int i = 0; i < handshakeComponents.Length; i++) - { - CommunicationsUtilities.Trace("Writing handshake part {0} ({1}) to pipe {2}", i, handshakeComponents[i], pipeName); - nodeStream.WriteIntForHandshake(handshakeComponents[i]); - } - - // This indicates that we have finished all the parts of our handshake; hopefully the endpoint has as well. - nodeStream.WriteEndOfHandshakeSignal(); - - CommunicationsUtilities.Trace("Reading handshake from pipe {0}", pipeName); - -#if NETCOREAPP2_1_OR_GREATER || MONO - nodeStream.ReadEndOfHandshakeSignal(true, timeout); -#else - nodeStream.ReadEndOfHandshakeSignal(true); -#endif - // We got a connection. - CommunicationsUtilities.Trace("Successfully connected to pipe {0}...!", pipeName); + ConnectToPipeStream(nodeStream, pipeName, handshake, timeout); return nodeStream; } catch (Exception e) when (!ExceptionHandling.IsCriticalException(e)) @@ -503,6 +470,50 @@ private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake han return null; } + /// + /// Connect to named pipe stream and ensure validate handshake and security. + /// + /// + /// Reused by MSBuild server client . + /// + internal static void ConnectToPipeStream(NamedPipeClientStream nodeStream, string pipeName, Handshake handshake, int timeout) + { + nodeStream.Connect(timeout); + +#if !FEATURE_PIPEOPTIONS_CURRENTUSERONLY + if (NativeMethodsShared.IsWindows && !NativeMethodsShared.IsMono) + { + // Verify that the owner of the pipe is us. This prevents a security hole where a remote node has + // been faked up with ACLs that would let us attach to it. It could then issue fake build requests back to + // us, potentially causing us to execute builds that do harmful or unexpected things. The pipe owner can + // only be set to the user's own SID by a normal, unprivileged process. The conditions where a faked up + // remote node could set the owner to something else would also let it change owners on other objects, so + // this would be a security flaw upstream of us. + ValidateRemotePipeSecurityOnWindows(nodeStream); + } +#endif + + int[] handshakeComponents = handshake.RetrieveHandshakeComponents(); + for (int i = 0; i < handshakeComponents.Length; i++) + { + CommunicationsUtilities.Trace("Writing handshake part {0} ({1}) to pipe {2}", i, handshakeComponents[i], pipeName); + nodeStream.WriteIntForHandshake(handshakeComponents[i]); + } + + // This indicates that we have finished all the parts of our handshake; hopefully the endpoint has as well. + nodeStream.WriteEndOfHandshakeSignal(); + + CommunicationsUtilities.Trace("Reading handshake from pipe {0}", pipeName); + +#if NETCOREAPP2_1_OR_GREATER || MONO + nodeStream.ReadEndOfHandshakeSignal(true, timeout); +#else + nodeStream.ReadEndOfHandshakeSignal(true); +#endif + // We got a connection. + CommunicationsUtilities.Trace("Successfully connected to pipe {0}...!", pipeName); + } + /// /// Class which wraps up the communications infrastructure for a given node. /// From 504f720b4fc99321d34f60169ed0e49a65a3802c Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Tue, 12 Jul 2022 00:25:42 +0200 Subject: [PATCH 34/43] Unshipped api fix --- src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt index 485d920eb6a..c97cd564039 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -18,4 +18,4 @@ Microsoft.Build.Experimental.OutOfProcServerNode Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback Microsoft.Build.Experimental.OutOfProcServerNode.OutOfProcServerNode(Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback buildFunction) -> void Microsoft.Build.Experimental.OutOfProcServerNode.Run(out System.Exception shutdownException) -> Microsoft.Build.Execution.NodeEngineShutdownReason -Microsoft.Build.Execution.HostServices.RegisterHostObject(string projectFile, string targetName, string taskName, string monikerName) -> void + From c625fc192476a6659e158202ada5707cdb94d84a Mon Sep 17 00:00:00 2001 From: MichalPavlik Date: Tue, 12 Jul 2022 11:37:20 +0200 Subject: [PATCH 35/43] Reduces allocations in case the tracing is not enabled (#7814) * Reduces allocations in case the tracing is not enabled * Resolving comment --- src/Build/BackEnd/Client/MSBuildClient.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index bb0e2033c9a..27fc6f75d0e 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -354,11 +354,11 @@ private bool TrySendPacket(Func packetResolver) { packet = packetResolver(); WritePacket(_nodeStream, packet); - CommunicationsUtilities.Trace($"Command packet of type '{packet.Type}' sent..."); + CommunicationsUtilities.Trace("Command packet of type '{0}' sent...", packet.Type); } catch (Exception ex) { - CommunicationsUtilities.Trace($"Failed to send command packet of type '{packet?.Type.ToString() ?? "Unknown"}' to server: {0}", ex); + CommunicationsUtilities.Trace("Failed to send command packet of type '{0}' to server: {1}", packet?.Type.ToString() ?? "Unknown", ex); _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; return false; } @@ -392,11 +392,11 @@ private bool TryLaunchServer() NodeLauncher nodeLauncher = new NodeLauncher(); CommunicationsUtilities.Trace("Starting Server..."); Process msbuildProcess = nodeLauncher.Start(_msbuildLocation, string.Join(" ", msBuildServerOptions)); - CommunicationsUtilities.Trace($"Server started with PID: {msbuildProcess?.Id}"); + CommunicationsUtilities.Trace("Server started with PID: {0}", msbuildProcess?.Id); } catch (Exception ex) { - CommunicationsUtilities.Trace($"Failed to launch the msbuild server: {ex}"); + CommunicationsUtilities.Trace("Failed to launch the msbuild server: {0}", ex); _exitResult.MSBuildClientExitType = MSBuildClientExitType.LaunchError; return false; } @@ -454,7 +454,7 @@ private void HandleCancellation() /// private void HandlePacketPumpError(MSBuildClientPacketPump packetPump) { - CommunicationsUtilities.Trace($"MSBuild client error: packet pump unexpectedly shut down: {packetPump.PacketPumpException}"); + CommunicationsUtilities.Trace("MSBuild client error: packet pump unexpectedly shut down: {0}", packetPump.PacketPumpException); throw packetPump.PacketPumpException ?? new InternalErrorException("Packet pump unexpectedly shut down"); } @@ -496,7 +496,7 @@ private void HandleServerNodeConsoleWrite(ServerNodeConsoleWrite consoleWrite) private void HandleServerNodeBuildResult(ServerNodeBuildResult response) { - CommunicationsUtilities.Trace($"Build response received: exit code {response.ExitCode}, exit type '{response.ExitType}'"); + CommunicationsUtilities.Trace("Build response received: exit code '{0}', exit type '{1}'", response.ExitCode, response.ExitType); _exitResult.MSBuildClientExitType = MSBuildClientExitType.Success; _exitResult.MSBuildAppExitTypeString = response.ExitType; _buildFinished = true; @@ -514,7 +514,7 @@ private bool TryConnectToServer(int timeout) } catch (Exception ex) { - CommunicationsUtilities.Trace($"Failed to connect to server: {ex}"); + CommunicationsUtilities.Trace("Failed to connect to server: {0}", ex); _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; return false; } From 6d00e2bc08be6c27af5e4cde7356dd1d6ff61a81 Mon Sep 17 00:00:00 2001 From: AR-May <67507805+AR-May@users.noreply.github.com> Date: Tue, 12 Jul 2022 16:51:47 +0200 Subject: [PATCH 36/43] Fix msbuild client exit type for connection errors. (#7816) --- src/Build/BackEnd/Client/MSBuildClient.cs | 9 ++++----- src/Build/BackEnd/Client/MSBuildClientExitType.cs | 8 ++++---- src/Build/PublicAPI/net/PublicAPI.Unshipped.txt | 2 +- src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt | 2 +- src/MSBuild/MSBuildClientApp.cs | 3 ++- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 27fc6f75d0e..8da1322684d 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -174,8 +174,6 @@ public MSBuildClientExitResult Execute(CancellationToken cancellationToken) // Connect to server. if (!TryConnectToServer(serverIsAlreadyRunning ? 1_000 : 20_000)) { - CommunicationsUtilities.Trace("Failure to connect to a server."); - _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; return _exitResult; } @@ -187,7 +185,8 @@ public MSBuildClientExitResult Execute(CancellationToken cancellationToken) if (!TrySendBuildCommand()) { CommunicationsUtilities.Trace("Failure to connect to a server."); - _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; + // Overwrite the client exit type from unexpected to connection error, since that would trigger the fallback to old build behavior. + _exitResult.MSBuildClientExitType = MSBuildClientExitType.UnableToConnect; return _exitResult; } @@ -359,7 +358,7 @@ private bool TrySendPacket(Func packetResolver) catch (Exception ex) { CommunicationsUtilities.Trace("Failed to send command packet of type '{0}' to server: {1}", packet?.Type.ToString() ?? "Unknown", ex); - _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; + _exitResult.MSBuildClientExitType = MSBuildClientExitType.Unexpected; return false; } @@ -515,7 +514,7 @@ private bool TryConnectToServer(int timeout) catch (Exception ex) { CommunicationsUtilities.Trace("Failed to connect to server: {0}", ex); - _exitResult.MSBuildClientExitType = MSBuildClientExitType.ConnectionError; + _exitResult.MSBuildClientExitType = MSBuildClientExitType.UnableToConnect; return false; } diff --git a/src/Build/BackEnd/Client/MSBuildClientExitType.cs b/src/Build/BackEnd/Client/MSBuildClientExitType.cs index b8061b3295e..e9916bd5414 100644 --- a/src/Build/BackEnd/Client/MSBuildClientExitType.cs +++ b/src/Build/BackEnd/Client/MSBuildClientExitType.cs @@ -9,15 +9,15 @@ public enum MSBuildClientExitType /// Success, /// - /// Server is busy. + /// Server is busy. This would invoke a fallback behavior. /// ServerBusy, /// - /// Client was unable to connect to the server. + /// Client was unable to connect to the server. This would invoke a fallback behavior. /// - ConnectionError, + UnableToConnect, /// - /// Client was unable to launch the server. + /// Client was unable to launch the server. This would invoke a fallback behavior. /// LaunchError, /// diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index 7ce7bc26ef8..a8ca12cca99 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -9,10 +9,10 @@ Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitResult() - Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitType.set -> void Microsoft.Build.Experimental.MSBuildClientExitType -Microsoft.Build.Experimental.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.UnableToConnect = 2 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.OutOfProcServerNode Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback diff --git a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt index c97cd564039..aa42c2c0ede 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -9,10 +9,10 @@ Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitResult() - Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitType.get -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitResult.MSBuildClientExitType.set -> void Microsoft.Build.Experimental.MSBuildClientExitType -Microsoft.Build.Experimental.MSBuildClientExitType.ConnectionError = 2 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.LaunchError = 3 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.ServerBusy = 1 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.Success = 0 -> Microsoft.Build.Experimental.MSBuildClientExitType +Microsoft.Build.Experimental.MSBuildClientExitType.UnableToConnect = 2 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.MSBuildClientExitType.Unexpected = 4 -> Microsoft.Build.Experimental.MSBuildClientExitType Microsoft.Build.Experimental.OutOfProcServerNode Microsoft.Build.Experimental.OutOfProcServerNode.BuildCallback diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index b5187d198a1..a2ea3bbfb60 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -74,7 +74,8 @@ public static MSBuildApp.ExitType Execute( MSBuildClientExitResult exitResult = msbuildClient.Execute(cancellationToken); if (exitResult.MSBuildClientExitType == MSBuildClientExitType.ServerBusy || - exitResult.MSBuildClientExitType == MSBuildClientExitType.ConnectionError) + exitResult.MSBuildClientExitType == MSBuildClientExitType.UnableToConnect || + exitResult.MSBuildClientExitType == MSBuildClientExitType.LaunchError) { // Server is busy, fallback to old behavior. return MSBuildApp.Execute(commandLine); From 26b43b08342b1f82b6c5ad5a08814b3316b080b1 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Tue, 12 Jul 2022 11:42:49 -0700 Subject: [PATCH 37/43] Emmit BuildTelemetry event (#7778) --- .../BackEnd/KnownTelemetry_Tests.cs | 121 +++++++++++++++ .../BackEnd/BuildManager/BuildManager.cs | 54 ++++++- src/Build/BackEnd/Client/MSBuildClient.cs | 17 +- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 15 +- .../BackEnd/Node/PartialBuildTelemetry.cs | 51 ++++++ .../BackEnd/Node/ServerNodeBuildCommand.cs | 12 +- src/Build/Microsoft.Build.csproj | 1 + src/Framework/NativeMethods.cs | 18 +++ src/Framework/Telemetry/BuildTelemetry.cs | 145 ++++++++++++++++++ src/Framework/Telemetry/KnownTelemetry.cs | 17 ++ src/Framework/Telemetry/TelemetryBase.cs | 24 +++ src/MSBuild/MSBuildClientApp.cs | 6 + src/MSBuild/XMake.cs | 17 +- 13 files changed, 485 insertions(+), 13 deletions(-) create mode 100644 src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs create mode 100644 src/Build/BackEnd/Node/PartialBuildTelemetry.cs create mode 100644 src/Framework/Telemetry/BuildTelemetry.cs create mode 100644 src/Framework/Telemetry/KnownTelemetry.cs create mode 100644 src/Framework/Telemetry/TelemetryBase.cs diff --git a/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs b/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs new file mode 100644 index 00000000000..a0ae7a9fafd --- /dev/null +++ b/src/Build.UnitTests/BackEnd/KnownTelemetry_Tests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable disable +using System; +using System.Globalization; +using Microsoft.Build.Framework.Telemetry; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.Telemetry; + +public class KnownTelemetry_Tests +{ + [Fact] + public void BuildTelemetryCanBeSetToNull() + { + KnownTelemetry.BuildTelemetry = new BuildTelemetry(); + KnownTelemetry.BuildTelemetry = null; + + KnownTelemetry.BuildTelemetry.ShouldBeNull(); + } + + [Fact] + public void BuildTelemetryCanBeSet() + { + BuildTelemetry buildTelemetry = new BuildTelemetry(); + KnownTelemetry.BuildTelemetry = buildTelemetry; + + KnownTelemetry.BuildTelemetry.ShouldBeSameAs(buildTelemetry); + } + + [Fact] + public void BuildTelemetryConstructedHasNoProperties() + { + BuildTelemetry buildTelemetry = new BuildTelemetry(); + + buildTelemetry.DisplayVersion.ShouldBeNull(); + buildTelemetry.EventName.ShouldBe("build"); + buildTelemetry.FinishedAt.ShouldBeNull(); + buildTelemetry.FrameworkName.ShouldBeNull(); + buildTelemetry.Host.ShouldBeNull(); + buildTelemetry.InitialServerState.ShouldBeNull(); + buildTelemetry.InnerStartAt.ShouldBeNull(); + buildTelemetry.Project.ShouldBeNull(); + buildTelemetry.ServerFallbackReason.ShouldBeNull(); + buildTelemetry.StartAt.ShouldBeNull(); + buildTelemetry.Success.ShouldBeNull(); + buildTelemetry.Target.ShouldBeNull(); + buildTelemetry.Version.ShouldBeNull(); + + buildTelemetry.UpdateEventProperties(); + buildTelemetry.Properties.ShouldBeEmpty(); + } + + [Fact] + public void BuildTelemetryCreateProperProperties() + { + BuildTelemetry buildTelemetry = new BuildTelemetry(); + + DateTime startAt = new DateTime(2023, 01, 02, 10, 11, 22); + DateTime innerStartAt = new DateTime(2023, 01, 02, 10, 20, 30); + DateTime finishedAt = new DateTime(2023, 12, 13, 14, 15, 16); + + buildTelemetry.DisplayVersion = "Some Display Version"; + buildTelemetry.FinishedAt = finishedAt; + buildTelemetry.FrameworkName = "new .NET"; + buildTelemetry.Host = "Host description"; + buildTelemetry.InitialServerState = "hot"; + buildTelemetry.InnerStartAt = innerStartAt; + buildTelemetry.Project = @"C:\\dev\\theProject"; + buildTelemetry.ServerFallbackReason = "busy"; + buildTelemetry.StartAt = startAt; + buildTelemetry.Success = true; + buildTelemetry.Target = "clean"; + buildTelemetry.Version = new Version(1, 2, 3, 4); + + buildTelemetry.UpdateEventProperties(); + buildTelemetry.Properties.Count.ShouldBe(11); + + buildTelemetry.Properties["BuildEngineDisplayVersion"].ShouldBe("Some Display Version"); + buildTelemetry.Properties["BuildEngineFrameworkName"].ShouldBe("new .NET"); + buildTelemetry.Properties["BuildEngineHost"].ShouldBe("Host description"); + buildTelemetry.Properties["InitialMSBuildServerState"].ShouldBe("hot"); + buildTelemetry.Properties["ProjectPath"].ShouldBe(@"C:\\dev\\theProject"); + buildTelemetry.Properties["ServerFallbackReason"].ShouldBe("busy"); + buildTelemetry.Properties["BuildSuccess"].ShouldBe("True"); + buildTelemetry.Properties["BuildTarget"].ShouldBe("clean"); + buildTelemetry.Properties["BuildEngineVersion"].ShouldBe("1.2.3.4"); + + // verify computed + buildTelemetry.Properties["BuildDurationInMilliseconds"] = (finishedAt - startAt).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); + buildTelemetry.Properties["InnerBuildDurationInMilliseconds"] = (finishedAt - innerStartAt).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); + } + + [Fact] + public void BuildTelemetryHandleNullsInRecordedTimes() + { + BuildTelemetry buildTelemetry = new BuildTelemetry(); + + buildTelemetry.StartAt = DateTime.MinValue; + buildTelemetry.FinishedAt = null; + buildTelemetry.UpdateEventProperties(); + buildTelemetry.Properties.ShouldBeEmpty(); + + buildTelemetry.StartAt = null; + buildTelemetry.FinishedAt = DateTime.MaxValue; + buildTelemetry.UpdateEventProperties(); + buildTelemetry.Properties.ShouldBeEmpty(); + + buildTelemetry.InnerStartAt = DateTime.MinValue; + buildTelemetry.FinishedAt = null; + buildTelemetry.UpdateEventProperties(); + buildTelemetry.Properties.ShouldBeEmpty(); + + buildTelemetry.InnerStartAt = null; + buildTelemetry.FinishedAt = DateTime.MaxValue; + buildTelemetry.UpdateEventProperties(); + buildTelemetry.Properties.ShouldBeEmpty(); + } +} diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index c4a9d2c9523..d6eaa69285d 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -25,6 +25,7 @@ using Microsoft.Build.Exceptions; using Microsoft.Build.Experimental.ProjectCache; using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Telemetry; using Microsoft.Build.Graph; using Microsoft.Build.Internal; using Microsoft.Build.Logging; @@ -456,7 +457,7 @@ public void BeginBuild(BuildParameters parameters) _nodeManager?.ShutdownAllNodes(); _taskHostNodeManager?.ShutdownAllNodes(); } - } + } } _previousLowPriority = parameters.LowPriority; @@ -470,6 +471,14 @@ public void BeginBuild(BuildParameters parameters) MSBuildEventSource.Log.BuildStart(); + // Initiate build telemetry data + DateTime now = DateTime.UtcNow; + KnownTelemetry.BuildTelemetry ??= new() + { + StartAt = now, + }; + KnownTelemetry.BuildTelemetry.InnerStartAt = now; + if (BuildParameters.DumpOpportunisticInternStats) { Strings.EnableDiagnostics(); @@ -796,6 +805,13 @@ public BuildSubmission PendBuildRequest(BuildRequestData requestData) VerifyStateInternal(BuildManagerState.Building); var newSubmission = new BuildSubmission(this, GetNextSubmissionId(), requestData, _buildParameters.LegacyThreadingSemantics); + + if (KnownTelemetry.BuildTelemetry != null) + { + KnownTelemetry.BuildTelemetry.Project ??= requestData.ProjectFullPath; + KnownTelemetry.BuildTelemetry.Target ??= string.Join(",", requestData.TargetNames); + } + _buildSubmissions.Add(newSubmission.SubmissionId, newSubmission); _noActiveSubmissionsEvent.Reset(); return newSubmission; @@ -817,6 +833,13 @@ public GraphBuildSubmission PendBuildRequest(GraphBuildRequestData requestData) VerifyStateInternal(BuildManagerState.Building); var newSubmission = new GraphBuildSubmission(this, GetNextSubmissionId(), requestData); + + if (KnownTelemetry.BuildTelemetry != null) + { + KnownTelemetry.BuildTelemetry.Project ??= requestData.ProjectGraphEntryPoints?.FirstOrDefault().ProjectFile; + KnownTelemetry.BuildTelemetry.Target ??= string.Join(",", requestData.TargetNames); + } + _graphBuildSubmissions.Add(newSubmission.SubmissionId, newSubmission); _noActiveSubmissionsEvent.Reset(); return newSubmission; @@ -965,6 +988,35 @@ public void EndBuild() } loggingService.LogBuildFinished(_overallBuildSuccess); + + if (KnownTelemetry.BuildTelemetry != null) + { + KnownTelemetry.BuildTelemetry.FinishedAt = DateTime.UtcNow; + KnownTelemetry.BuildTelemetry.Success = _overallBuildSuccess; + KnownTelemetry.BuildTelemetry.Version = ProjectCollection.Version; + KnownTelemetry.BuildTelemetry.DisplayVersion = ProjectCollection.DisplayVersion; + KnownTelemetry.BuildTelemetry.FrameworkName = NativeMethodsShared.FrameworkName; + + string host = null; + if (BuildEnvironmentState.s_runningInVisualStudio) + { + host = "VS"; + } + else if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILD_HOST_NAME"))) + { + host = Environment.GetEnvironmentVariable("MSBUILD_HOST_NAME"); + } + else if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("VSCODE_CWD")) || Environment.GetEnvironmentVariable("TERM_PROGRAM") == "vscode") + { + host = "VSCode"; + } + KnownTelemetry.BuildTelemetry.Host = host; + + KnownTelemetry.BuildTelemetry.UpdateEventProperties(); + loggingService.LogTelemetry(buildEventContext: null, KnownTelemetry.BuildTelemetry.EventName, KnownTelemetry.BuildTelemetry.Properties); + // Clean telemetry which makes it ready for next build submission. + KnownTelemetry.BuildTelemetry = null; + } } ShutdownLoggingService(loggingService); diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index 8da1322684d..c3e80eb1a0d 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -15,6 +15,7 @@ using Microsoft.Build.Eventing; using Microsoft.Build.Execution; using Microsoft.Build.Framework; +using Microsoft.Build.Framework.Telemetry; using Microsoft.Build.Internal; using Microsoft.Build.Shared; @@ -152,6 +153,10 @@ public MSBuildClientExitResult Execute(CancellationToken cancellationToken) // Start server it if is not running. bool serverIsAlreadyRunning = ServerNamedMutex.WasOpen(serverRunningMutexName); + if (KnownTelemetry.BuildTelemetry != null) + { + KnownTelemetry.BuildTelemetry.InitialServerState = serverIsAlreadyRunning ? "hot" : "cold"; + } if (!serverIsAlreadyRunning) { CommunicationsUtilities.Trace("Server was not running. Starting server now."); @@ -424,13 +429,23 @@ private ServerNodeBuildCommand GetServerNodeBuildCommand() // We remove env variable used to invoke MSBuild server as that might be equal to 1, so we do not get an infinite recursion here. envVars.Remove(Traits.UseMSBuildServerEnvVarName); + Debug.Assert(KnownTelemetry.BuildTelemetry == null || KnownTelemetry.BuildTelemetry.StartAt.HasValue, "BuildTelemetry.StartAt was not initialized!"); + + PartialBuildTelemetry? partialBuildTelemetry = KnownTelemetry.BuildTelemetry == null + ? null + : new PartialBuildTelemetry( + startedAt: KnownTelemetry.BuildTelemetry.StartAt.GetValueOrDefault(), + initialServerState: KnownTelemetry.BuildTelemetry.InitialServerState, + serverFallbackReason: KnownTelemetry.BuildTelemetry.ServerFallbackReason); + return new ServerNodeBuildCommand( _commandLine, startupDirectory: Directory.GetCurrentDirectory(), buildProcessEnvironment: envVars, CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture, - _consoleConfiguration!); + _consoleConfiguration!, + partialBuildTelemetry); } private ServerNodeHandshake GetHandshake() diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index d20ce9ebbd5..b391e3ff0ac 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -5,12 +5,13 @@ using System.Collections.Concurrent; using System.IO; using System.Threading; +using System.Threading.Tasks; using Microsoft.Build.BackEnd; using Microsoft.Build.Shared; using Microsoft.Build.Internal; -using System.Threading.Tasks; using Microsoft.Build.Execution; using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework.Telemetry; namespace Microsoft.Build.Experimental { @@ -318,6 +319,18 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) // Configure console configuration so Loggers can change their behavior based on Target (client) Console properties. ConsoleConfiguration.Provider = command.ConsoleConfiguration; + // Initiate build telemetry + if (KnownTelemetry.BuildTelemetry == null) + { + KnownTelemetry.BuildTelemetry = new BuildTelemetry(); + } + if (command.PartialBuildTelemetry != null) + { + KnownTelemetry.BuildTelemetry.StartAt = command.PartialBuildTelemetry.StartedAt; + KnownTelemetry.BuildTelemetry.InitialServerState = command.PartialBuildTelemetry.InitialServerState; + KnownTelemetry.BuildTelemetry.ServerFallbackReason = command.PartialBuildTelemetry.ServerFallbackReason; + } + // Also try our best to increase chance custom Loggers which use Console static members will work as expected. try { diff --git a/src/Build/BackEnd/Node/PartialBuildTelemetry.cs b/src/Build/BackEnd/Node/PartialBuildTelemetry.cs new file mode 100644 index 00000000000..b9960a0e752 --- /dev/null +++ b/src/Build/BackEnd/Node/PartialBuildTelemetry.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; + +namespace Microsoft.Build.BackEnd; + +/// +/// Part of BuildTelemetry which is collected on client and needs to be sent to server, +/// so server can log BuildTelemetry once it is finished. +/// +internal sealed class PartialBuildTelemetry : ITranslatable +{ + private DateTime _startedAt = default; + private string? _initialServerState = default; + private string? _serverFallbackReason = default; + + public PartialBuildTelemetry(DateTime startedAt, string? initialServerState, string? serverFallbackReason) + { + _startedAt = startedAt; + _initialServerState = initialServerState; + _serverFallbackReason = serverFallbackReason; + } + + /// + /// Constructor for deserialization + /// + private PartialBuildTelemetry() + { + } + + public DateTime? StartedAt => _startedAt; + + public string? InitialServerState => _initialServerState; + + public string? ServerFallbackReason => _serverFallbackReason; + + public void Translate(ITranslator translator) + { + translator.Translate(ref _startedAt); + translator.Translate(ref _initialServerState); + translator.Translate(ref _serverFallbackReason); + } + + internal static PartialBuildTelemetry FactoryForDeserialization(ITranslator translator) + { + PartialBuildTelemetry partialTelemetryData = new(); + partialTelemetryData.Translate(translator); + return partialTelemetryData; + } +} diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs index 41e33589a84..ee8bd565d25 100644 --- a/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -24,6 +24,7 @@ internal sealed class ServerNodeBuildCommand : INodePacket private CultureInfo _culture = default!; private CultureInfo _uiCulture = default!; private TargetConsoleConfiguration _consoleConfiguration = default!; + private PartialBuildTelemetry? _partialBuildTelemetry = default; /// /// Retrieves the packet type. @@ -64,6 +65,12 @@ internal sealed class ServerNodeBuildCommand : INodePacket /// public TargetConsoleConfiguration ConsoleConfiguration => _consoleConfiguration; + /// + /// Part of BuildTelemetry which is collected on client and needs to be sent to server, + /// so server can log BuildTelemetry once it is finished. + /// + public PartialBuildTelemetry? PartialBuildTelemetry => _partialBuildTelemetry; + /// /// Private constructor for deserialization /// @@ -80,7 +87,8 @@ public ServerNodeBuildCommand( string startupDirectory, Dictionary buildProcessEnvironment, CultureInfo culture, CultureInfo uiCulture, - TargetConsoleConfiguration consoleConfiguration) + TargetConsoleConfiguration consoleConfiguration, + PartialBuildTelemetry? partialBuildTelemetry) { ErrorUtilities.VerifyThrowInternalNull(consoleConfiguration, nameof(consoleConfiguration)); @@ -90,6 +98,7 @@ public ServerNodeBuildCommand( _culture = culture; _uiCulture = uiCulture; _consoleConfiguration = consoleConfiguration; + _partialBuildTelemetry = partialBuildTelemetry; } /// @@ -104,6 +113,7 @@ public void Translate(ITranslator translator) translator.TranslateCulture(ref _culture); translator.TranslateCulture(ref _uiCulture); translator.Translate(ref _consoleConfiguration, TargetConsoleConfiguration.FactoryForDeserialization); + translator.Translate(ref _partialBuildTelemetry, PartialBuildTelemetry.FactoryForDeserialization); } /// diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 29aa17f014d..8109bfdf69b 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -160,6 +160,7 @@ + diff --git a/src/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs index 2d3a9027b88..d0c29652824 100644 --- a/src/Framework/NativeMethods.cs +++ b/src/Framework/NativeMethods.cs @@ -758,6 +758,24 @@ internal static string OSName get { return IsWindows ? "Windows_NT" : "Unix"; } } + /// + /// Framework named as presented to users (for example in version info). + /// + internal static string FrameworkName + { + get + { +#if RUNTIME_TYPE_NETCORE + const string frameworkName = ".NET"; +#elif MONO + const string frameworkName = "Mono"; +#else + const string frameworkName = ".NET Framework"; +#endif + return frameworkName; + } + } + /// /// OS name that can be used for the msbuildExtensionsPathSearchPaths element /// for a toolset diff --git a/src/Framework/Telemetry/BuildTelemetry.cs b/src/Framework/Telemetry/BuildTelemetry.cs new file mode 100644 index 00000000000..45e7537ff7c --- /dev/null +++ b/src/Framework/Telemetry/BuildTelemetry.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Globalization; + +namespace Microsoft.Build.Framework.Telemetry +{ + /// + /// Telemetry of build. + /// + internal class BuildTelemetry : TelemetryBase + { + public override string EventName => "build"; + + /// + /// Time at which build have started. + /// + /// + /// It is time when build started, not when BuildManager start executing build. + /// For example in case of MSBuild Server it is time before we connected or launched MSBuild Server. + /// + public DateTime? StartAt { get; set; } + + /// + /// Time at which inner build have started. + /// + /// + /// It is time when build internally started, i.e. when BuildManager starts it. + /// In case of MSBuild Server it is time when Server starts build. + /// + public DateTime? InnerStartAt { get; set; } + + /// + /// Time at which build have finished. + /// + public DateTime? FinishedAt { get; set; } + + /// + /// Overall build success. + /// + public bool? Success { get; set; } + + /// + /// Build Target. + /// + public string? Target { get; set; } + + /// + /// MSBuild server fallback reason. + /// Either "ServerBusy", "ConnectionError" or null (no fallback). + /// + public string? ServerFallbackReason { get; set; } + + /// + /// Version of MSBuild. + /// + public Version? Version { get; set; } + + /// + /// Display version of the Engine suitable for display to a user. + /// + public string? DisplayVersion { get; set; } + + /// + /// Path to project file. + /// + public string? Project { get; set; } + + /// + /// Host in which MSBuild build was executed. + /// For example: "VS", "VSCode", "Azure DevOps", "GitHub Action", "CLI", ... + /// + public string? Host { get; set; } + + /// + /// State of MSBuild server process before this build. + /// One of 'cold', 'hot', null (if not run as server) + /// + public string? InitialServerState { get; set; } + + /// + /// Framework name suitable for display to a user. + /// + public string? FrameworkName { get; set; } + + public override void UpdateEventProperties() + { + if (DisplayVersion != null) + { + Properties["BuildEngineDisplayVersion"] = DisplayVersion; + } + + if (StartAt.HasValue && FinishedAt.HasValue) + { + Properties["BuildDurationInMilliseconds"] = (FinishedAt.Value - StartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); + } + + if (InnerStartAt.HasValue && FinishedAt.HasValue) + { + Properties["InnerBuildDurationInMilliseconds"] = (FinishedAt.Value - InnerStartAt.Value).TotalMilliseconds.ToString(CultureInfo.InvariantCulture); + } + + if (FrameworkName != null) + { + Properties["BuildEngineFrameworkName"] = FrameworkName; + } + + if (Host != null) + { + Properties["BuildEngineHost"] = Host; + } + + if (InitialServerState != null) + { + Properties["InitialMSBuildServerState"] = InitialServerState; + } + + if (Project != null) + { + Properties["ProjectPath"] = Project; + } + + if (ServerFallbackReason != null) + { + Properties["ServerFallbackReason"] = ServerFallbackReason; + } + + if (Success.HasValue) + { + Properties["BuildSuccess"] = Success.HasValue.ToString(CultureInfo.InvariantCulture); + } + + if (Target != null) + { + Properties["BuildTarget"] = Target; + } + + if (Version != null) + { + Properties["BuildEngineVersion"] = Version.ToString(); + } + } + } +} diff --git a/src/Framework/Telemetry/KnownTelemetry.cs b/src/Framework/Telemetry/KnownTelemetry.cs new file mode 100644 index 00000000000..5f32304d7e6 --- /dev/null +++ b/src/Framework/Telemetry/KnownTelemetry.cs @@ -0,0 +1,17 @@ +// 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.Framework.Telemetry; + +/// +/// Static class to help access and modify known telemetries. +/// +internal static class KnownTelemetry +{ + /// + /// Telemetry for build. + /// If null Telemetry is not supposed to be emitted. + /// After telemetry is emitted, sender shall null it. + /// + public static BuildTelemetry? BuildTelemetry { get; set; } +} diff --git a/src/Framework/Telemetry/TelemetryBase.cs b/src/Framework/Telemetry/TelemetryBase.cs new file mode 100644 index 00000000000..26348f1ea4f --- /dev/null +++ b/src/Framework/Telemetry/TelemetryBase.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace Microsoft.Build.Framework.Telemetry; + +internal abstract class TelemetryBase +{ + /// + /// Gets or sets the name of the event. + /// + public abstract string EventName { get; } + + /// + /// Gets or sets a list of properties associated with the event. + /// + public IDictionary Properties { get; set; } = new Dictionary(); + + /// + /// Translate all derived type members into properties which will be used to build . + /// + public abstract void UpdateEventProperties(); +} diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs index a2ea3bbfb60..9177f76aa19 100644 --- a/src/MSBuild/MSBuildClientApp.cs +++ b/src/MSBuild/MSBuildClientApp.cs @@ -5,6 +5,7 @@ using Microsoft.Build.Shared; using System.Threading; using Microsoft.Build.Experimental; +using Microsoft.Build.Framework.Telemetry; #if RUNTIME_TYPE_NETCORE || MONO using System.IO; @@ -77,6 +78,11 @@ public static MSBuildApp.ExitType Execute( exitResult.MSBuildClientExitType == MSBuildClientExitType.UnableToConnect || exitResult.MSBuildClientExitType == MSBuildClientExitType.LaunchError) { + if (KnownTelemetry.BuildTelemetry != null) + { + KnownTelemetry.BuildTelemetry.ServerFallbackReason = exitResult.MSBuildClientExitType.ToString(); + } + // Server is busy, fallback to old behavior. return MSBuildApp.Execute(commandLine); } diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs index af15342f6f4..31139c676ed 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -36,6 +36,7 @@ using BinaryLogger = Microsoft.Build.Logging.BinaryLogger; using Microsoft.Build.Shared.Debugging; using Microsoft.Build.Experimental; +using Microsoft.Build.Framework.Telemetry; #nullable disable @@ -215,6 +216,9 @@ string[] args #endif ) { + // Initialize new build telemetry and record start of this build. + KnownTelemetry.BuildTelemetry = new BuildTelemetry { StartAt = DateTime.UtcNow }; + using PerformanceLogEventListener eventListener = PerformanceLogEventListener.Create(); if (Environment.GetEnvironmentVariable("MSBUILDDUMPPROCESSCOUNTERS") == "1") @@ -525,6 +529,9 @@ string[] commandLine #endif ) { + // Initialize new build telemetry and record start of this build, if not initialized already + KnownTelemetry.BuildTelemetry ??= new BuildTelemetry { StartAt = DateTime.UtcNow }; + // Indicate to the engine that it can toss extraneous file content // when it loads microsoft.*.targets. We can't do this in the general case, // because tasks in the build can (and occasionally do) load MSBuild format files @@ -3757,15 +3764,7 @@ private static void ThrowInvalidToolsVersionInitializationException(IEnumerable< /// private static void DisplayVersionMessage() { -#if RUNTIME_TYPE_NETCORE - const string frameworkName = ".NET"; -#elif MONO - const string frameworkName = "Mono"; -#else - const string frameworkName = ".NET Framework"; -#endif - - Console.WriteLine(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("MSBuildVersionMessage", ProjectCollection.DisplayVersion, frameworkName)); + Console.WriteLine(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("MSBuildVersionMessage", ProjectCollection.DisplayVersion, NativeMethods.FrameworkName)); } /// From 60abe1f122ea18e5166a177ccdc5a2a38a963ab5 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Tue, 12 Jul 2022 21:50:20 +0200 Subject: [PATCH 38/43] Increase console refresh to 25Hz --- src/Build/BackEnd/Node/OutOfProcServerNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/Build/BackEnd/Node/OutOfProcServerNode.cs index b391e3ff0ac..55e08dc13bd 100644 --- a/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -389,7 +389,7 @@ private RedirectConsoleWriter(Action writeCallback) { _writeCallback = writeCallback; _syncWriter = Synchronized(this); - _timer = new Timer(TimerCallback, null, 0, 200); + _timer = new Timer(TimerCallback, null, 0, 40); } public static TextWriter Create(Action writeCallback) From ae46347220111a52978388facddbe5950360868b Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Wed, 13 Jul 2022 13:39:08 +0200 Subject: [PATCH 39/43] Do not recover from TrySendBuildCommand failure --- src/Build/BackEnd/Client/MSBuildClient.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Build/BackEnd/Client/MSBuildClient.cs b/src/Build/BackEnd/Client/MSBuildClient.cs index c3e80eb1a0d..4e88c67398a 100644 --- a/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -189,9 +189,6 @@ public MSBuildClientExitResult Execute(CancellationToken cancellationToken) MSBuildEventSource.Log.MSBuildServerBuildStart(descriptiveCommandLine); if (!TrySendBuildCommand()) { - CommunicationsUtilities.Trace("Failure to connect to a server."); - // Overwrite the client exit type from unexpected to connection error, since that would trigger the fallback to old build behavior. - _exitResult.MSBuildClientExitType = MSBuildClientExitType.UnableToConnect; return _exitResult; } From 9c2f0f3498db4904d3733eaedf134808023965b0 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Wed, 13 Jul 2022 15:31:59 +0200 Subject: [PATCH 40/43] Move *ConcoleConfiguration classes to its own files --- src/Build/Logging/BaseConsoleLogger.cs | 231 ------------------ src/Build/Logging/ConsoleConfiguration.cs | 63 +++++ src/Build/Logging/IConsoleConfiguration.cs | 40 +++ .../Logging/InProcessConsoleConfiguration.cs | 99 ++++++++ .../Logging/TargetConsoleConfiguration.cs | 57 +++++ src/Build/Microsoft.Build.csproj | 4 + 6 files changed, 263 insertions(+), 231 deletions(-) create mode 100644 src/Build/Logging/ConsoleConfiguration.cs create mode 100644 src/Build/Logging/IConsoleConfiguration.cs create mode 100644 src/Build/Logging/InProcessConsoleConfiguration.cs create mode 100644 src/Build/Logging/TargetConsoleConfiguration.cs diff --git a/src/Build/Logging/BaseConsoleLogger.cs b/src/Build/Logging/BaseConsoleLogger.cs index 20bd109a974..00a4e46919f 100644 --- a/src/Build/Logging/BaseConsoleLogger.cs +++ b/src/Build/Logging/BaseConsoleLogger.cs @@ -4,7 +4,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -1240,234 +1239,4 @@ private bool ApplyVerbosityParameter(string parameterValue) #endregion } - - /// - /// Console configuration needed for proper Console logging. - /// - internal interface IConsoleConfiguration - { - /// - /// Buffer width of destination Console. - /// Console loggers are supposed, on Windows OS, to be wrapping to avoid output trimming. - /// -1 console buffer width can't be obtained. - /// - int BufferWidth { get; } - - /// - /// True if console output accept ANSI colors codes. - /// False if output is redirected to non screen type such as file or nul. - /// - bool AcceptAnsiColorCodes { get; } - - /// - /// True if console output is screen. It is expected that non screen output is post-processed and often does not need wrapping and coloring. - /// False if output is redirected to non screen type such as file or nul. - /// - bool OutputIsScreen { get; } - - /// - /// Background color of client console, -1 if not detectable - /// Some platforms do not allow getting current background color. There - /// is not way to check, but not-supported exception is thrown. Assume - /// black, but don't crash. - /// - ConsoleColor BackgroundColor { get; } - } - - /// - /// Console configuration of target Console at which we will render output. - /// It is supposed to be Console from other process to which output from this process will be redirected. - /// - internal class TargetConsoleConfiguration : IConsoleConfiguration, ITranslatable - { - private int _bufferWidth; - private bool _acceptAnsiColorCodes; - private bool _outputIsScreen; - private ConsoleColor _backgroundColor; - - public TargetConsoleConfiguration(int bufferWidth, bool acceptAnsiColorCodes, bool outputIsScreen, ConsoleColor backgroundColor) - { - _bufferWidth = bufferWidth; - _acceptAnsiColorCodes = acceptAnsiColorCodes; - _outputIsScreen = outputIsScreen; - _backgroundColor = backgroundColor; - } - - /// - /// Constructor for deserialization - /// - private TargetConsoleConfiguration() - { - } - - public int BufferWidth => _bufferWidth; - - public bool AcceptAnsiColorCodes => _acceptAnsiColorCodes; - - public bool OutputIsScreen => _outputIsScreen; - - public ConsoleColor BackgroundColor => _backgroundColor; - - public void Translate(ITranslator translator) - { - translator.Translate(ref _bufferWidth); - translator.Translate(ref _acceptAnsiColorCodes); - translator.Translate(ref _outputIsScreen); - translator.TranslateEnum(ref _backgroundColor, (int)_backgroundColor); - } - - internal static TargetConsoleConfiguration FactoryForDeserialization(ITranslator translator) - { - TargetConsoleConfiguration configuration = new(); - configuration.Translate(translator); - return configuration; - } - } - - /// - /// Console configuration of current process Console. - /// - internal class InProcessConsoleConfiguration : IConsoleConfiguration - { - /// - /// When set, we'll try reading background color. - /// - private static bool s_supportReadingBackgroundColor = true; - - public int BufferWidth => Console.BufferWidth; - - public bool AcceptAnsiColorCodes - { - get - { - bool acceptAnsiColorCodes = false; - if (NativeMethodsShared.IsWindows && !Console.IsOutputRedirected) - { - try - { - IntPtr stdOut = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); - if (NativeMethodsShared.GetConsoleMode(stdOut, out uint consoleMode)) - { - acceptAnsiColorCodes = (consoleMode & NativeMethodsShared.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; - } - } - catch (Exception ex) - { - Debug.Assert(false, $"MSBuild client warning: problem during enabling support for VT100: {ex}."); - } - } - else - { - // On posix OSes we expect console always supports VT100 coloring unless it is redirected - acceptAnsiColorCodes = !Console.IsOutputRedirected; - } - - return acceptAnsiColorCodes; - } - } - - public ConsoleColor BackgroundColor - { - get - { - if (s_supportReadingBackgroundColor) - { - try - { - return Console.BackgroundColor; - } - catch (PlatformNotSupportedException) - { - s_supportReadingBackgroundColor = false; - } - } - - return ConsoleColor.Black; - } - } - - public bool OutputIsScreen - { - get - { - bool isScreen = false; - - if (NativeMethodsShared.IsWindows) - { - // Get the std out handle - IntPtr stdHandle = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); - - if (stdHandle != NativeMethods.InvalidHandle) - { - uint fileType = NativeMethodsShared.GetFileType(stdHandle); - - // The std out is a char type(LPT or Console) - isScreen = fileType == NativeMethodsShared.FILE_TYPE_CHAR; - } - } - else - { - isScreen = !Console.IsOutputRedirected; - } - - return isScreen; - } - } - } - - /// - /// Target console configuration. - /// If console output is redirected to other process console, like for example MSBuild Server does, - /// we need to know property of target/final console at which our output will be rendered. - /// If console is rendered at current process Console, we grab properties from Console and/or by WinAPI. - /// - internal static class ConsoleConfiguration - { - /// - /// Get or set current target console configuration provider. - /// - public static IConsoleConfiguration Provider - { - get { return Instance.s_instance; } - set { Instance.s_instance = value; } - } - - private static class Instance - { - // Explicit static constructor to tell C# compiler - // not to mark type as beforefieldinit - static Instance() - { - } - - internal static IConsoleConfiguration s_instance = new InProcessConsoleConfiguration(); - } - - /// - /// Buffer width of destination Console. - /// Console loggers are supposed, on Windows OS, to be wrapping to avoid output trimming. - /// -1 console buffer width can't be obtained. - /// - public static int BufferWidth => Provider.BufferWidth; - - /// - /// True if console output accept ANSI colors codes. - /// False if output is redirected to non screen type such as file or nul. - /// - public static bool AcceptAnsiColorCodes => Provider.AcceptAnsiColorCodes; - - /// - /// Background color of client console, -1 if not detectable - /// Some platforms do not allow getting current background color. There - /// is not way to check, but not-supported exception is thrown. Assume - /// black, but don't crash. - /// - public static ConsoleColor BackgroundColor => Provider.BackgroundColor; - - /// - /// True if console output is screen. It is expected that non screen output is post-processed and often does not need wrapping and coloring. - /// False if output is redirected to non screen type such as file or nul. - /// - public static bool OutputIsScreen => Provider.OutputIsScreen; - } } diff --git a/src/Build/Logging/ConsoleConfiguration.cs b/src/Build/Logging/ConsoleConfiguration.cs new file mode 100644 index 00000000000..a826d0c24e5 --- /dev/null +++ b/src/Build/Logging/ConsoleConfiguration.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable disable +using System; + +namespace Microsoft.Build.BackEnd.Logging; + +/// +/// Target console configuration. +/// If console output is redirected to other process console, like for example MSBuild Server does, +/// we need to know property of target/final console at which our output will be rendered. +/// If console is rendered at current process Console, we grab properties from Console and/or by WinAPI. +/// +internal static class ConsoleConfiguration +{ + /// + /// Get or set current target console configuration provider. + /// + public static IConsoleConfiguration Provider + { + get { return Instance.s_instance; } + set { Instance.s_instance = value; } + } + + private static class Instance + { + // Explicit static constructor to tell C# compiler + // not to mark type as beforefieldinit + static Instance() + { + } + + internal static IConsoleConfiguration s_instance = new InProcessConsoleConfiguration(); + } + + /// + /// Buffer width of destination Console. + /// Console loggers are supposed, on Windows OS, to be wrapping to avoid output trimming. + /// -1 console buffer width can't be obtained. + /// + public static int BufferWidth => Provider.BufferWidth; + + /// + /// True if console output accept ANSI colors codes. + /// False if output is redirected to non screen type such as file or nul. + /// + public static bool AcceptAnsiColorCodes => Provider.AcceptAnsiColorCodes; + + /// + /// Background color of client console, -1 if not detectable + /// Some platforms do not allow getting current background color. There + /// is not way to check, but not-supported exception is thrown. Assume + /// black, but don't crash. + /// + public static ConsoleColor BackgroundColor => Provider.BackgroundColor; + + /// + /// True if console output is screen. It is expected that non screen output is post-processed and often does not need wrapping and coloring. + /// False if output is redirected to non screen type such as file or nul. + /// + public static bool OutputIsScreen => Provider.OutputIsScreen; +} diff --git a/src/Build/Logging/IConsoleConfiguration.cs b/src/Build/Logging/IConsoleConfiguration.cs new file mode 100644 index 00000000000..86ff9c4ea47 --- /dev/null +++ b/src/Build/Logging/IConsoleConfiguration.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable disable +using System; + +namespace Microsoft.Build.BackEnd.Logging; + +/// +/// Console configuration needed for proper Console logging. +/// +internal interface IConsoleConfiguration +{ + /// + /// Buffer width of destination Console. + /// Console loggers are supposed, on Windows OS, to be wrapping to avoid output trimming. + /// -1 console buffer width can't be obtained. + /// + int BufferWidth { get; } + + /// + /// True if console output accept ANSI colors codes. + /// False if output is redirected to non screen type such as file or nul. + /// + bool AcceptAnsiColorCodes { get; } + + /// + /// True if console output is screen. It is expected that non screen output is post-processed and often does not need wrapping and coloring. + /// False if output is redirected to non screen type such as file or nul. + /// + bool OutputIsScreen { get; } + + /// + /// Background color of client console, -1 if not detectable + /// Some platforms do not allow getting current background color. There + /// is not way to check, but not-supported exception is thrown. Assume + /// black, but don't crash. + /// + ConsoleColor BackgroundColor { get; } +} diff --git a/src/Build/Logging/InProcessConsoleConfiguration.cs b/src/Build/Logging/InProcessConsoleConfiguration.cs new file mode 100644 index 00000000000..d070e246773 --- /dev/null +++ b/src/Build/Logging/InProcessConsoleConfiguration.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable disable +using System; +using System.Diagnostics; + +namespace Microsoft.Build.BackEnd.Logging; + +/// +/// Console configuration of current process Console. +/// +internal class InProcessConsoleConfiguration : IConsoleConfiguration +{ + /// + /// When set, we'll try reading background color. + /// + private static bool s_supportReadingBackgroundColor = true; + + public int BufferWidth => Console.BufferWidth; + + public bool AcceptAnsiColorCodes + { + get + { + bool acceptAnsiColorCodes = false; + if (NativeMethodsShared.IsWindows && !Console.IsOutputRedirected) + { + try + { + IntPtr stdOut = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + if (NativeMethodsShared.GetConsoleMode(stdOut, out uint consoleMode)) + { + acceptAnsiColorCodes = (consoleMode & NativeMethodsShared.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; + } + } + catch (Exception ex) + { + Debug.Assert(false, $"MSBuild client warning: problem during enabling support for VT100: {ex}."); + } + } + else + { + // On posix OSes we expect console always supports VT100 coloring unless it is redirected + acceptAnsiColorCodes = !Console.IsOutputRedirected; + } + + return acceptAnsiColorCodes; + } + } + + public ConsoleColor BackgroundColor + { + get + { + if (s_supportReadingBackgroundColor) + { + try + { + return Console.BackgroundColor; + } + catch (PlatformNotSupportedException) + { + s_supportReadingBackgroundColor = false; + } + } + + return ConsoleColor.Black; + } + } + + public bool OutputIsScreen + { + get + { + bool isScreen = false; + + if (NativeMethodsShared.IsWindows) + { + // Get the std out handle + IntPtr stdHandle = NativeMethodsShared.GetStdHandle(NativeMethodsShared.STD_OUTPUT_HANDLE); + + if (stdHandle != NativeMethods.InvalidHandle) + { + uint fileType = NativeMethodsShared.GetFileType(stdHandle); + + // The std out is a char type(LPT or Console) + isScreen = fileType == NativeMethodsShared.FILE_TYPE_CHAR; + } + } + else + { + isScreen = !Console.IsOutputRedirected; + } + + return isScreen; + } + } +} diff --git a/src/Build/Logging/TargetConsoleConfiguration.cs b/src/Build/Logging/TargetConsoleConfiguration.cs new file mode 100644 index 00000000000..57f92dad1c5 --- /dev/null +++ b/src/Build/Logging/TargetConsoleConfiguration.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +#nullable disable +using System; + +namespace Microsoft.Build.BackEnd.Logging; + +/// +/// Console configuration of target Console at which we will render output. +/// It is supposed to be Console from other process to which output from this process will be redirected. +/// +internal class TargetConsoleConfiguration : IConsoleConfiguration, ITranslatable +{ + private int _bufferWidth; + private bool _acceptAnsiColorCodes; + private bool _outputIsScreen; + private ConsoleColor _backgroundColor; + + public TargetConsoleConfiguration(int bufferWidth, bool acceptAnsiColorCodes, bool outputIsScreen, ConsoleColor backgroundColor) + { + _bufferWidth = bufferWidth; + _acceptAnsiColorCodes = acceptAnsiColorCodes; + _outputIsScreen = outputIsScreen; + _backgroundColor = backgroundColor; + } + + /// + /// Constructor for deserialization + /// + private TargetConsoleConfiguration() + { + } + + public int BufferWidth => _bufferWidth; + + public bool AcceptAnsiColorCodes => _acceptAnsiColorCodes; + + public bool OutputIsScreen => _outputIsScreen; + + public ConsoleColor BackgroundColor => _backgroundColor; + + public void Translate(ITranslator translator) + { + translator.Translate(ref _bufferWidth); + translator.Translate(ref _acceptAnsiColorCodes); + translator.Translate(ref _outputIsScreen); + translator.TranslateEnum(ref _backgroundColor, (int)_backgroundColor); + } + + internal static TargetConsoleConfiguration FactoryForDeserialization(ITranslator translator) + { + TargetConsoleConfiguration configuration = new(); + configuration.Translate(translator); + return configuration; + } +} diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 8109bfdf69b..926ea75351d 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -167,6 +167,10 @@ + + + + From aa17a3ffbdafecc1ec233c77178b615456708787 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Wed, 13 Jul 2022 15:44:37 +0200 Subject: [PATCH 41/43] Change test timing --- src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs index bd579597c2e..a5d7d8be2a2 100644 --- a/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs +++ b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs @@ -21,7 +21,7 @@ public async Task EmitConsoleMessages() using (TextWriter writer = OutOfProcServerNode.RedirectConsoleWriter.Create(text => sb.Append(text))) { writer.WriteLine("Line 1"); - await Task.Delay(300); + await Task.Delay(80); // should be somehow bigger than `RedirectConsoleWriter` flush period - see its constructor writer.Write("Line 2"); } From b87a5bea0531c56b7229ea2e111a3f5dbc0f7c15 Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Wed, 13 Jul 2022 15:46:16 +0200 Subject: [PATCH 42/43] Add comment for graph build telemetry project --- src/Build/BackEnd/BuildManager/BuildManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index d6eaa69285d..70a2d359a04 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -836,6 +836,8 @@ public GraphBuildSubmission PendBuildRequest(GraphBuildRequestData requestData) if (KnownTelemetry.BuildTelemetry != null) { + // Project graph can have multiple entry points, for purposes of identifying event for same build project, + // we believe that including only one entry point will provide enough precision. KnownTelemetry.BuildTelemetry.Project ??= requestData.ProjectGraphEntryPoints?.FirstOrDefault().ProjectFile; KnownTelemetry.BuildTelemetry.Target ??= string.Join(",", requestData.TargetNames); } From 568e61fc1cc4f4c344b046e823c2a44afac8efde Mon Sep 17 00:00:00 2001 From: Roman Konecny Date: Wed, 13 Jul 2022 15:46:39 +0200 Subject: [PATCH 43/43] Fix English in comment --- src/Build/BackEnd/BuildManager/BuildManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index 70a2d359a04..c07549f691f 100644 --- a/src/Build/BackEnd/BuildManager/BuildManager.cs +++ b/src/Build/BackEnd/BuildManager/BuildManager.cs @@ -1016,7 +1016,7 @@ public void EndBuild() KnownTelemetry.BuildTelemetry.UpdateEventProperties(); loggingService.LogTelemetry(buildEventContext: null, KnownTelemetry.BuildTelemetry.EventName, KnownTelemetry.BuildTelemetry.Properties); - // Clean telemetry which makes it ready for next build submission. + // Clean telemetry to make it ready for next build submission. KnownTelemetry.BuildTelemetry = null; } }