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. diff --git a/documentation/specs/event-source.md b/documentation/specs/event-source.md index dcd1abc5b09..f646f120114 100644 --- a/documentation/specs/event-source.md +++ b/documentation/specs/event-source.md @@ -22,6 +22,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/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 2158cd2a8a5..c527f09e244 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -27,6 +27,7 @@ A wave of features is set to "rotate out" (i.e. become standard functionality) t - [Respect deps.json when loading assemblies](https://github.com/dotnet/msbuild/pull/7520) - [Consider `Platform` as default during Platform Negotiation](https://github.com/dotnet/msbuild/pull/7511) - [Adding accepted SDK name match pattern to SDK manifests](https://github.com/dotnet/msbuild/pull/7597) +- [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/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.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.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs new file mode 100644 index 00000000000..a5d7d8be2a2 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/RedirectConsoleWriter_Tests.cs @@ -0,0 +1,31 @@ +// 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.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Build.Experimental; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.Engine.UnitTests.BackEnd +{ + public class RedirectConsoleWriter_Tests + { + [Fact] + public async Task EmitConsoleMessages() + { + StringBuilder sb = new StringBuilder(); + + using (TextWriter writer = OutOfProcServerNode.RedirectConsoleWriter.Create(text => sb.Append(text))) + { + writer.WriteLine("Line 1"); + await Task.Delay(80); // should be somehow bigger than `RedirectConsoleWriter` flush period - see its constructor + writer.Write("Line 2"); + } + + sb.ToString().ShouldBe($"Line 1{Environment.NewLine}Line 2"); + } + } +} diff --git a/src/Build/BackEnd/BuildManager/BuildManager.cs b/src/Build/BackEnd/BuildManager/BuildManager.cs index c4a9d2c9523..c07549f691f 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,15 @@ public GraphBuildSubmission PendBuildRequest(GraphBuildRequestData requestData) VerifyStateInternal(BuildManagerState.Building); var newSubmission = new GraphBuildSubmission(this, GetNextSubmissionId(), 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); + } + _graphBuildSubmissions.Add(newSubmission.SubmissionId, newSubmission); _noActiveSubmissionsEvent.Reset(); return newSubmission; @@ -965,6 +990,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 to make 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 new file mode 100644 index 00000000000..4e88c67398a --- /dev/null +++ b/src/Build/BackEnd/Client/MSBuildClient.cs @@ -0,0 +1,561 @@ +// 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.BackEnd.Logging; +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; + +namespace Microsoft.Build.Experimental +{ + /// + /// 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; + + /// + /// 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 _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. + /// + 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; + + /// + /// Used to estimate the size of the build with an ETW trace. + /// + private int _numConsoleWritePackets; + private long _sizeOfConsoleWritePackets; + + /// + /// Capture configuration of Client Console. + /// + private TargetConsoleConfiguration? _consoleConfiguration; + + /// + /// 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( +#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 + _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. + /// + /// Cancellation token. + /// A value of type that indicates whether the build succeeded, + /// or the manner in which it failed. + public MSBuildClientExitResult Execute(CancellationToken cancellationToken) + { + // 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); + + // 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."); + if (!TryLaunchServer()) + { + _exitResult.MSBuildClientExitType = MSBuildClientExitType.LaunchError; + 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)) + { + 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(descriptiveCommandLine); + if (!TrySendBuildCommand()) + { + return _exitResult; + } + + _numConsoleWritePackets = 0; + _sizeOfConsoleWritePackets = 0; + + 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(); + // 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: + HandlePacketPumpError(packetPump); + break; + + case 2: + while (packetPump.ReceivedPacketsQueue.TryDequeue(out INodePacket? packet) && + !_buildFinished) + { + 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; + } + + MSBuildEventSource.Log.MSBuildServerBuildStop(descriptiveCommandLine, _numConsoleWritePackets, _sizeOfConsoleWritePackets, _exitResult.MSBuildClientExitType.ToString(), _exitResult.MSBuildAppExitTypeString); + CommunicationsUtilities.Trace("Build finished."); + return _exitResult; + } + + 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() + { + int consoleBufferWidth = -1; + try + { + 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) + { + INodePacket? packet = null; + try + { + packet = packetResolver(); + WritePacket(_nodeStream, packet); + CommunicationsUtilities.Trace("Command packet of type '{0}' sent...", packet.Type); + } + catch (Exception ex) + { + CommunicationsUtilities.Trace("Failed to send command packet of type '{0}' to server: {1}", packet?.Type.ToString() ?? "Unknown", ex); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.Unexpected; + return false; + } + + return true; + } + + /// + /// Launches MSBuild server. + /// + /// Whether MSBuild server was started successfully. + private bool TryLaunchServer() + { + string serverLaunchMutexName = $@"Global\msbuild-server-launch-{_handshake.ComputeHash()}"; + 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[] { + "/nologo", + "/nodemode:8" + }; + + try + { + 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) + { + CommunicationsUtilities.Trace("Failed to launch the msbuild server: {0}", ex); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.LaunchError; + return false; + } + + return true; + } + + private bool TrySendBuildCommand() => TrySendPacket(() => GetServerNodeBuildCommand()); + + private bool TrySendCancelCommand() => TrySendPacket(() => new ServerNodeBuildCancel()); + + private ServerNodeBuildCommand GetServerNodeBuildCommand() + { + 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.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!, + partialBuildTelemetry); + } + + private ServerNodeHandshake GetHandshake() + { + return new ServerNodeHandshake(CommunicationsUtilities.GetHandshakeOptions(taskHost: false, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture())); + } + + /// + /// Handle cancellation. + /// + private void HandleCancellation() + { + TrySendCancelCommand(); + + CommunicationsUtilities.Trace("MSBuild client sent cancelation command."); + } + + /// + /// 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 InternalErrorException("Packet pump unexpectedly shut down"); + } + + /// + /// Dispatches the packet to the correct handler. + /// + private void HandlePacket(INodePacket packet) + { + switch (packet.Type) + { + case NodePacketType.ServerNodeConsoleWrite: + ServerNodeConsoleWrite writePacket = (packet as ServerNodeConsoleWrite)!; + HandleServerNodeConsoleWrite(writePacket); + _numConsoleWritePackets++; + _sizeOfConsoleWritePackets += writePacket.Text.Length; + 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 + { + NodeProviderOutOfProcBase.ConnectToPipeStream(_nodeStream, _pipeName, _handshake, timeout); + } + catch (Exception ex) + { + CommunicationsUtilities.Trace("Failed to connect to server: {0}", ex); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.UnableToConnect; + 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..8cb466741fa --- /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.Experimental +{ + /// + /// 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..e9916bd5414 --- /dev/null +++ b/src/Build/BackEnd/Client/MSBuildClientExitType.cs @@ -0,0 +1,29 @@ +// 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.Experimental +{ + public enum MSBuildClientExitType + { + /// + /// The MSBuild client successfully processed the build request. + /// + Success, + /// + /// Server is busy. This would invoke a fallback behavior. + /// + ServerBusy, + /// + /// Client was unable to connect to the server. This would invoke a fallback behavior. + /// + UnableToConnect, + /// + /// Client was unable to launch the server. This would invoke a fallback behavior. + /// + LaunchError, + /// + /// The build stopped unexpectedly, for example, + /// because a named pipe between the server and the client was unexpectedly closed. + /// + Unexpected + } +} diff --git a/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs new file mode 100644 index 00000000000..682fa1dfb94 --- /dev/null +++ b/src/Build/BackEnd/Client/MSBuildClientPacketPump.cs @@ -0,0 +1,304 @@ +// 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) + { + ErrorUtilities.VerifyThrowArgumentNull(stream, nameof(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 it. + 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/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/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/Components/Communications/NodeLauncher.cs b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs new file mode 100644 index 00000000000..9a08a3940a7 --- /dev/null +++ b/src/Build/BackEnd/Components/Communications/NodeLauncher.cs @@ -0,0 +1,211 @@ +// 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) + { + // 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)); + + 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); + } + } + + 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); + } + } + } + } +} diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs index c681f417bb5..cd5a88127e0 100644 --- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs +++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcBase.cs @@ -20,11 +20,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 BackendNativeMethods = Microsoft.Build.BackEnd.NativeMethods; using Task = System.Threading.Tasks.Task; using Microsoft.Build.Framework; using Microsoft.Build.BackEnd.Logging; @@ -332,7 +329,8 @@ bool StartNewNode(int nodeId) } #endif // Create the node process - Process msbuildProcess = LaunchNode(msbuildLocation, commandLineArgs); + NodeLauncher nodeLauncher = new NodeLauncher(); + Process msbuildProcess = nodeLauncher.Start(msbuildLocation, commandLineArgs); _processesToIgnore.TryAdd(GetProcessesToIgnoreKey(hostHandshake, msbuildProcess.Id), default); // Note, when running under IMAGEFILEEXECUTIONOPTIONS registry key to debug, the process ID @@ -398,7 +396,7 @@ void CreateNodeContext(int nodeId, Process nodeToReuse, Stream nodeStream) msbuildLocation = "MSBuild.exe"; } - var expectedProcessName = Path.GetFileNameWithoutExtension(GetCurrentHost() ?? msbuildLocation); + var expectedProcessName = Path.GetFileNameWithoutExtension(CurrentHost.GetCurrentHost() ?? msbuildLocation); var processes = Process.GetProcessesByName(expectedProcessName); Array.Sort(processes, (left, right) => left.Id.CompareTo(right.Id)); @@ -418,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 @@ -442,7 +440,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 @@ -453,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)) @@ -506,196 +471,47 @@ private Stream TryConnectToProcess(int nodeProcessId, int timeout, Handshake han } /// - /// Creates a new MSBuild process + /// Connect to named pipe stream and ensure validate handshake and security. /// - private Process LaunchNode(string msbuildLocation, string commandLineArgs) + /// + /// Reused by MSBuild server client . + /// + internal static void ConnectToPipeStream(NamedPipeClientStream nodeStream, string pipeName, Handshake handshake, int timeout) { - // 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}"; + nodeStream.Connect(timeout); - 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) +#if !FEATURE_PIPEOPTIONS_CURRENTUSERONLY + if (NativeMethodsShared.IsWindows && !NativeMethodsShared.IsMono) { - // Run the child process with the same host as the currently-running process. - exeName = GetCurrentHost(); + // 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 - 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 + int[] handshakeComponents = handshake.RetrieveHandshakeComponents(); + for (int i = 0; i < handshakeComponents.Length; i++) { -#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); + CommunicationsUtilities.Trace("Writing handshake part {0} ({1}) to pipe {2}", i, handshakeComponents[i], pipeName); + nodeStream.WriteIntForHandshake(handshakeComponents[i]); } - } -#if RUNTIME_TYPE_NETCORE || MONO - private static string CurrentHost; -#endif + // This indicates that we have finished all the parts of our handshake; hopefully the endpoint has as well. + nodeStream.WriteEndOfHandshakeSignal(); - /// - /// 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; - } - } - } + CommunicationsUtilities.Trace("Reading handshake from pipe {0}", pipeName); - return CurrentHost; +#if NETCOREAPP2_1_OR_GREATER || MONO + nodeStream.ReadEndOfHandshakeSignal(true, timeout); #else - return null; + nodeStream.ReadEndOfHandshakeSignal(true); #endif + // We got a connection. + CommunicationsUtilities.Trace("Successfully connected to pipe {0}...!", pipeName); } /// diff --git a/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs b/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs new file mode 100644 index 00000000000..9616f90964b --- /dev/null +++ b/src/Build/BackEnd/Components/Communications/ServerNodeEndpointOutOfProc.cs @@ -0,0 +1,37 @@ +// 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; + +namespace Microsoft.Build.BackEnd +{ + /// + /// This is an implementation of out-of-proc server node endpoint. + /// + internal sealed class ServerNodeEndpointOutOfProc : NodeEndpointOutOfProcBase + { + private readonly Handshake _handshake; + + /// + /// Instantiates an endpoint to act as a client + /// + /// The name of the pipe to which we should connect. + /// + internal ServerNodeEndpointOutOfProc( + string pipeName, + Handshake handshake) + { + _handshake = handshake; + + InternalConstruct(pipeName); + } + + /// + /// Returns the host handshake for this node endpoint + /// + protected override Handshake 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..2a685c594d7 --- /dev/null +++ b/src/Build/BackEnd/Node/ConsoleOutput.cs @@ -0,0 +1,11 @@ +// 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 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..55e08dc13bd --- /dev/null +++ b/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -0,0 +1,431 @@ +// 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.Concurrent; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Shared; +using Microsoft.Build.Internal; +using Microsoft.Build.Execution; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Framework.Telemetry; + +namespace Microsoft.Build.Experimental +{ + /// + /// This class represents an implementation of INode for out-of-proc server nodes aka MSBuild server + /// + public sealed class OutOfProcServerNode : INode, INodePacketFactory, INodePacketHandler + { + /// + /// 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. + /// + 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; + + private string _serverBusyMutexName = default!; + + public OutOfProcServerNode(BuildCallback buildFunction) + { + _buildFunction = buildFunction; + + _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); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.ServerNodeBuildCancel, ServerNodeBuildCancel.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) + { + ServerNodeHandshake handshake = new( + CommunicationsUtilities.GetHandshakeOptions(taskHost: false, architectureFlagToSet: XMakeAttributes.GetCurrentMSBuildArchitecture())); + + _serverBusyMutexName = GetBusyServerMutexName(handshake); + + // Handled race condition. If two processes spawn to start build Server one will die while + // one Server client connects to the other one and run build on it. + CommunicationsUtilities.Trace("Starting new server node with handshake {0}", handshake); + using var serverRunningMutex = ServerNamedMutex.OpenOrCreateMutex(GetRunningServerMutexName(handshake), out bool mutexCreatedNew); + if (!mutexCreatedNew) + { + shutdownException = new InvalidOperationException("MSBuild server is already running!"); + return NodeEngineShutdownReason.Error; + } + + _nodeEndpoint = new ServerNodeEndpointOutOfProc(GetPipeName(handshake), handshake); + _nodeEndpoint.OnLinkStatusChanged += OnLinkStatusChanged; + _nodeEndpoint.Listen(this); + + var waitHandles = new WaitHandle[] { _shutdownEvent, _packetReceivedEvent }; + + // Get the current directory before doing any work. We need this so we can restore the directory when the node shutsdown. + while (true) + { + int index = WaitHandle.WaitAny(waitHandles); + switch (index) + { + case 0: + NodeEngineShutdownReason shutdownReason = HandleShutdown(out shutdownException); + return shutdownReason; + + case 1: + + while (_receivedPackets.TryDequeue(out INodePacket? packet)) + { + if (packet != null) + { + HandlePacket(packet); + } + } + + break; + } + } + + // UNREACHABLE + } + + #endregion + + internal static string GetPipeName(ServerNodeHandshake handshake) + => NamedPipeUtil.GetPlatformSpecificPipeName($"MSBuildServer-{handshake.ComputeHash()}"); + + internal static string GetRunningServerMutexName(ServerNodeHandshake handshake) + => $@"Global\msbuild-server-running-{handshake.ComputeHash()}"; + + internal static string GetBusyServerMutexName(ServerNodeHandshake handshake) + => $@"Global\msbuild-server-busy-{handshake.ComputeHash()}"; + + #region INodePacketFactory Members + + /// + /// 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); + + // On Windows, a process holds a handle to the current directory, + // so reset it away from a user-requested folder that may get deleted. + NativeMethodsShared.SetCurrentDirectory(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory); + + exception = _shutdownException; + + _nodeEndpoint.OnLinkStatusChanged -= OnLinkStatusChanged; + _nodeEndpoint.Disconnect(); + + CommunicationsUtilities.Trace("Shut down complete."); + + return _shutdownReason; + } + + /// + /// 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; + + 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: + HandleServerNodeBuildCommandAsync((ServerNodeBuildCommand)packet); + break; + case NodePacketType.ServerNodeBuildCancel: + BuildManager.DefaultBuildManager.CancelAllSubmissions(); + break; + } + } + + private void HandleServerNodeBuildCommandAsync(ServerNodeBuildCommand command) + { + Task.Run(() => + { + try + { + HandleServerNodeBuildCommand(command); + } + catch(Exception e) + { + _shutdownException = e; + _shutdownReason = NodeEngineShutdownReason.Error; + _shutdownEvent.Set(); + } + }); + } + + private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) + { + CommunicationsUtilities.Trace("Building with MSBuild server with command line {0}", command.CommandLine); + using var serverBusyMutex = ServerNamedMutex.OpenOrCreateMutex(name: _serverBusyMutexName, createdNew: out var holdsMutex); + if (!holdsMutex) + { + // Client must have send request message to server even though serer is busy. + // It is not a race condition, as client exclusivity is also guaranteed by name pipe which allows only one client to connect. + _shutdownException = new InvalidOperationException("Client requested build while server is busy processing previous client build request."); + _shutdownReason = NodeEngineShutdownReason.Error; + _shutdownEvent.Set(); + + return; + } + + // Set build process context + Directory.SetCurrentDirectory(command.StartupDirectory); + CommunicationsUtilities.SetEnvironment(command.BuildProcessEnvironment); + Thread.CurrentThread.CurrentCulture = command.Culture; + Thread.CurrentThread.CurrentUICulture = command.UICulture; + + // 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 + { + if (NativeMethodsShared.IsWindows && command.ConsoleConfiguration.BufferWidth > 0) + { + Console.BufferWidth = command.ConsoleConfiguration.BufferWidth; + } + + if ((int)command.ConsoleConfiguration.BackgroundColor != -1) + { + Console.BackgroundColor = command.ConsoleConfiguration.BackgroundColor; + } + } + catch (Exception) + { + // Ignore exception, it is best effort only + } + + // Configure console output redirection + var oldOut = Console.Out; + var oldErr = Console.Error; + (int exitCode, string exitType) buildResult; + + // Dispose must be called before the server sends ServerNodeBuildResult packet + using (var outWriter = RedirectConsoleWriter.Create(text => SendPacket(new ServerNodeConsoleWrite(text, ConsoleOutput.Standard)))) + using (var errWriter = RedirectConsoleWriter.Create(text => SendPacket(new ServerNodeConsoleWrite(text, ConsoleOutput.Error)))) + { + Console.SetOut(outWriter); + Console.SetError(errWriter); + + buildResult = _buildFunction(command.CommandLine); + + Console.SetOut(oldOut); + Console.SetError(oldErr); + } + + // On Windows, a process holds a handle to the current directory, + // so reset it away from a user-requested folder that may get deleted. + NativeMethodsShared.SetCurrentDirectory(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory); + + _nodeEndpoint.ClientWillDisconnect(); + var response = new ServerNodeBuildResult(buildResult.exitCode, buildResult.exitType); + SendPacket(response); + + _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, 40); + } + + 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/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/ServerNamedMutex.cs b/src/Build/BackEnd/Node/ServerNamedMutex.cs new file mode 100644 index 00000000000..ac7244a6cc0 --- /dev/null +++ b/src/Build/BackEnd/Node/ServerNamedMutex.cs @@ -0,0 +1,66 @@ +// 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; + +namespace Microsoft.Build.Execution +{ + internal sealed class ServerNamedMutex : IDisposable + { + 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) + { + 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 void Dispose() + { + if (IsDisposed) + { + return; + } + + IsDisposed = true; + + try + { + if (IsLocked) + { + _serverMutex.ReleaseMutex(); + } + } + finally + { + _serverMutex.Dispose(); + IsLocked = false; + } + } + } +} diff --git a/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs b/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs new file mode 100644 index 00000000000..67cd7f0df7f --- /dev/null +++ b/src/Build/BackEnd/Node/ServerNodeBuildCancel.cs @@ -0,0 +1,19 @@ +// 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 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/BackEnd/Node/ServerNodeBuildCommand.cs b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs new file mode 100644 index 00000000000..ee8bd565d25 --- /dev/null +++ b/src/Build/BackEnd/Node/ServerNodeBuildCommand.cs @@ -0,0 +1,130 @@ +// 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; +using System.Globalization; +using Microsoft.Build.BackEnd.Logging; +using Microsoft.Build.Shared; + +namespace Microsoft.Build.BackEnd +{ + /// + /// Contains all of the information necessary for a entry node to run a command line. + /// + 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!; + private CultureInfo _uiCulture = default!; + private TargetConsoleConfiguration _consoleConfiguration = default!; + private PartialBuildTelemetry? _partialBuildTelemetry = default; + + /// + /// Retrieves the packet type. + /// + public NodePacketType Type => NodePacketType.ServerNodeBuildCommand; + + /// + /// Command line including arguments + /// +#if FEATURE_GET_COMMANDLINE + public string CommandLine => _commandLine; +#else + public string[] CommandLine => _commandLine; +#endif + + /// + /// 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; + + /// + /// Console configuration of Client. + /// + 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 + /// + private ServerNodeBuildCommand() + { + } + + public ServerNodeBuildCommand( +#if FEATURE_GET_COMMANDLINE + string commandLine, +#else + string[] commandLine, +#endif + string startupDirectory, + Dictionary buildProcessEnvironment, + CultureInfo culture, CultureInfo uiCulture, + TargetConsoleConfiguration consoleConfiguration, + PartialBuildTelemetry? partialBuildTelemetry) + { + ErrorUtilities.VerifyThrowInternalNull(consoleConfiguration, nameof(consoleConfiguration)); + + _commandLine = commandLine; + _startupDirectory = startupDirectory; + _buildProcessEnvironment = buildProcessEnvironment; + _culture = culture; + _uiCulture = uiCulture; + _consoleConfiguration = consoleConfiguration; + _partialBuildTelemetry = partialBuildTelemetry; + } + + /// + /// 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); + translator.Translate(ref _consoleConfiguration, TargetConsoleConfiguration.FactoryForDeserialization); + translator.Translate(ref _partialBuildTelemetry, PartialBuildTelemetry.FactoryForDeserialization); + } + + /// + /// 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..4ea012ebafd --- /dev/null +++ b/src/Build/BackEnd/Node/ServerNodeBuildResult.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. + +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/Definition/ProjectCollection.cs b/src/Build/Definition/ProjectCollection.cs index 32f9b37fe68..78f195e8f03 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. /// @@ -308,7 +310,7 @@ 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, useAsynchronousLogging: false) + : this(globalProperties, loggers, remoteLoggers, toolsetDefinitionLocations, maxNodeCount, onlyLogCriticalEvents, loadProjectsReadOnly, useAsynchronousLogging: false, reuseProjectRootElementCache: false) { } @@ -328,7 +330,8 @@ 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. /// If set to true, asynchronous logging will be used. has to called to clear resources used by async logging. - internal ProjectCollection(IDictionary globalProperties, IEnumerable loggers, IEnumerable remoteLoggers, ToolsetDefinitionLocations toolsetDefinitionLocations, int maxNodeCount, bool onlyLogCriticalEvents, bool loadProjectsReadOnly, bool useAsynchronousLogging) + /// 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 useAsynchronousLogging, bool reuseProjectRootElementCache) { _loadedProjects = new LoadedProjectCollection(); ToolsetLocations = toolsetDefinitionLocations; @@ -338,10 +341,23 @@ internal ProjectCollection(IDictionary globalProperties, IEnumer { 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 @@ -447,7 +463,7 @@ public static ProjectCollection GlobalProjectCollection // Take care to ensure that there is never more than one value observed // from this property even in the case of race conditions while lazily initializing. var local = new ProjectCollection(null, null, null, ToolsetDefinitionLocations.Default, - maxNodeCount: 1, onlyLogCriticalEvents: false, loadProjectsReadOnly: false, useAsynchronousLogging: true); + maxNodeCount: 1, onlyLogCriticalEvents: false, loadProjectsReadOnly: false, useAsynchronousLogging: true, reuseProjectRootElementCache: false); if (Interlocked.CompareExchange(ref s_globalProjectCollection, local, null) != null) { @@ -1637,6 +1653,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/Logging/BaseConsoleLogger.cs b/src/Build/Logging/BaseConsoleLogger.cs index ea87f587b70..00a4e46919f 100644 --- a/src/Build/Logging/BaseConsoleLogger.cs +++ b/src/Build/Logging/BaseConsoleLogger.cs @@ -28,38 +28,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 +284,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 +328,7 @@ internal static void SetColor(ConsoleColor c) { try { - Console.ForegroundColor = TransformColor(c, BackgroundColor); + Console.ForegroundColor = TransformColor(c, ConsoleConfiguration.BackgroundColor); } catch (IOException) { @@ -480,7 +441,7 @@ internal void InitializeConsoleMethods(LoggerVerbosity logverbosity, WriteHandle try { - ConsoleColor c = BackgroundColor; + ConsoleColor c = ConsoleConfiguration.BackgroundColor; } catch (IOException) { 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/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/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/ParallelLogger/ParallelConsoleLogger.cs b/src/Build/Logging/ParallelLogger/ParallelConsoleLogger.cs index 1f9b5eecf54..1833201110c 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/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 309d14bdc83..926ea75351d 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -147,12 +147,30 @@ + + + + + + + + + + + + + + + + + + @@ -343,6 +361,7 @@ + diff --git a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt index e69de29bb2d..a8ca12cca99 100644 --- a/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/net/PublicAPI.Unshipped.txt @@ -0,0 +1,20 @@ +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 +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.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 +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 e69de29bb2d..aa42c2c0ede 100644 --- a/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt +++ b/src/Build/PublicAPI/netstandard/PublicAPI.Unshipped.txt @@ -0,0 +1,21 @@ +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 +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.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 +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/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/Framework/MSBuildEventSource.cs b/src/Framework/MSBuildEventSource.cs index e169219952d..89b8370c392 100644 --- a/src/Framework/MSBuildEventSource.cs +++ b/src/Framework/MSBuildEventSource.cs @@ -646,7 +646,18 @@ public void LoadAssemblyAndFindTypeStop(string assemblyPath, int numberOfPublicT { WriteEvent(88, assemblyPath, numberOfPublicTypesSearched); } + + [Event(89, Keywords = Keywords.All)] + public void MSBuildServerBuildStart(string commandLine) + { + WriteEvent(89, commandLine); + } + [Event(90, Keywords = Keywords.All)] + public void MSBuildServerBuildStop(string commandLine, int countOfConsoleMessages, long sumSizeOfConsoleMessages, string clientExitType, string serverExitType) + { + WriteEvent(90, commandLine, countOfConsoleMessages, sumSizeOfConsoleMessages, clientExitType, serverExitType); + } #endregion } } 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/Framework/NativeMethods.cs b/src/Framework/NativeMethods.cs index bb17186086f..d0c29652824 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; @@ -59,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; @@ -756,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 @@ -1499,6 +1519,12 @@ internal static void VerifyThrowWin32Result(int result) [DllImport("kernel32.dll")] [SupportedOSPlatform("windows")] internal static extern uint GetFileType(IntPtr hFile); + + [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); [SuppressMessage("Microsoft.Usage", "CA2205:UseManagedEquivalentsOfWin32Api", Justification = "Using unmanaged equivalent for performance reasons")] [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] 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/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.UnitTests/MSBuildServer_Tests.cs b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs new file mode 100644 index 00000000000..2e4c4ba5cf1 --- /dev/null +++ b/src/MSBuild.UnitTests/MSBuildServer_Tests.cs @@ -0,0 +1,220 @@ +// 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.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 + Process.GetProcessById(pidOfServerProcess).KillTree(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 "); + // 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."); + } + + [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 "); + // 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", ""); + 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."); + + if (pidOfServerProcess != pidOfNewserverProcess) + { + // Register process to clean up (be killed) after tests ends. + _env.WithTransientProcess(pidOfNewserverProcess); + } + } + + [Fact] + public void BuildsWhileBuildIsRunningOnServer() + { + _env.SetEnvironmentVariable("MSBUILDUSESERVER", "1"); + TransientTestFile project = _env.CreateFile("testProject.proj", printPidContents); + TransientTestFile sleepProject = _env.CreateFile("napProject.proj", sleepingTaskContents); + + 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); + }); + + // 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."); + + // 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) + { + 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/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index a6e2e036e5e..c0866d5f734 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -175,6 +175,7 @@ true + diff --git a/src/MSBuild/MSBuildClientApp.cs b/src/MSBuild/MSBuildClientApp.cs new file mode 100644 index 00000000000..9177f76aa19 --- /dev/null +++ b/src/MSBuild/MSBuildClientApp.cs @@ -0,0 +1,127 @@ +// 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.Shared; +using System.Threading; +using Microsoft.Build.Experimental; +using Microsoft.Build.Framework.Telemetry; + +#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 msbuildLocation = BuildEnvironmentHelper.Instance.CurrentMSBuildExePath; + + return Execute( + commandLine, + msbuildLocation, + cancellationToken); + } + + /// + /// 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. + /// 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( +#if FEATURE_GET_COMMANDLINE + string commandLine, +#else + string[] commandLine, +#endif + string msbuildLocation, + CancellationToken cancellationToken) + { + MSBuildClient msbuildClient = new MSBuildClient(commandLine, msbuildLocation); + MSBuildClientExitResult exitResult = msbuildClient.Execute(cancellationToken); + + if (exitResult.MSBuildClientExitType == MSBuildClientExitType.ServerBusy || + 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); + } + + 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/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index 8bf7155cbac..5701c2d3cc9 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -317,7 +317,9 @@ 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 6119ea4e085..83498322bea 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -508,13 +508,15 @@ 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 2b6914c41b7..a4661df67c5 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -505,13 +505,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 e0d7c14e8ad..0264ff8e278 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -509,13 +509,15 @@ 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 0372c4900f0..f67550e3ebe 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -505,13 +505,15 @@ 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 4721ca9afcb..16817ae88bf 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -515,13 +515,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 e3fd2d55895..0d83ec719f6 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -505,13 +505,15 @@ 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 c15244ecb8c..996484c8d56 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -505,13 +505,15 @@ 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 b170f13dc44..1e40a134ce5 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -515,13 +515,15 @@ 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 0d41ea796de..157b81ac122 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -506,13 +506,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 9f729cfd401..339b3ce1319 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -504,13 +504,15 @@ 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 9d761e71fdf..3a7140e5fa4 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -505,13 +505,15 @@ 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 98030b258e3..ce870f5b882 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -505,13 +505,15 @@ 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 1ea6492f634..4458f52f1cc 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -505,13 +505,15 @@ 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 c5f21d2b46d..31139c676ed 100644 --- a/src/MSBuild/XMake.cs +++ b/src/MSBuild/XMake.cs @@ -35,6 +35,8 @@ using ForwardingLoggerRecord = Microsoft.Build.Logging.ForwardingLoggerRecord; using BinaryLogger = Microsoft.Build.Logging.BinaryLogger; using Microsoft.Build.Shared.Debugging; +using Microsoft.Build.Experimental; +using Microsoft.Build.Framework.Telemetry; #nullable disable @@ -84,7 +86,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 } /// @@ -209,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") @@ -216,14 +226,34 @@ string[] args DumpCounters(true /* initialize only */); } - // return 0 on success, non-zero on failure - int exitCode = ((s_initialized && Execute( + 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. + 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") { @@ -461,6 +491,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 @@ -479,26 +529,17 @@ 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 // 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)); @@ -845,8 +886,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 @@ -1061,7 +1101,9 @@ string[] commandLine toolsetDefinitionLocations, cpuCount, onlyLogCriticalEvents, - loadProjectsReadOnly: !preprocessOnly + loadProjectsReadOnly: !preprocessOnly, + useAsynchronousLogging: true, + reuseProjectRootElementCache: s_isServerNode ); if (toolsVersion != null && !projectCollection.ContainsToolset(toolsVersion)) @@ -1294,7 +1336,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; @@ -1959,6 +2008,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 +2683,36 @@ 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 + OutOfProcServerNode.BuildCallback buildFunction = (commandLine) => + { + int exitCode; + ExitType exitType; + + if (!s_initialized) + { + exitType = ExitType.InitializationError; + } + else + { + exitType = Execute(commandLine); + } + + 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()); @@ -3019,6 +3103,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; @@ -3124,6 +3214,12 @@ List loggers consoleParameters = AggregateParameters(consoleParameters, consoleLoggerParameters); } + // Always use ANSI escape codes when the build is initiated by server + if (s_isServerNode) + { + consoleParameters = $"PREFERCONSOLECOLOR;{consoleParameters}"; + } + // 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") @@ -3668,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)); } /// diff --git a/src/Shared/BinaryTranslator.cs b/src/Shared/BinaryTranslator.cs index 73888fa88a0..86438f1da31 100644 --- a/src/Shared/BinaryTranslator.cs +++ b/src/Shared/BinaryTranslator.cs @@ -455,6 +455,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(); @@ -1079,10 +1080,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 73dcf6f5a93..f8106579fb7 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,17 +77,17 @@ internal enum HandshakeOptions Arm64 = 128, } - internal readonly struct Handshake + internal class Handshake { - 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; @@ -113,7 +115,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 virtual int[] RetrieveHandshakeComponents() { return new int[] { @@ -126,6 +128,61 @@ internal int[] RetrieveHandshakeComponents() CommunicationsUtilities.AvoidEndOfHandshakeSignal(sessionId) }; } + + public virtual string GetKey() => $"{options} {salt} {fileVersionMajor} {fileVersionMinor} {fileVersionBuild} {fileVersionPrivate} {sessionId}".ToString(CultureInfo.InvariantCulture); + + public virtual byte? ExpectedVersionInFirstByte => CommunicationsUtilities.handshakeVersion; + } + + internal sealed class ServerNodeHandshake : Handshake + { + /// + /// Caching computed hash. + /// + private string _computedHash = null; + + public override byte? ExpectedVersionInFirstByte => null; + + internal ServerNodeHandshake(HandshakeOptions nodeType) + : base(nodeType) + { + } + + public override int[] RetrieveHandshakeComponents() + { + return new int[] + { + CommunicationsUtilities.AvoidEndOfHandshakeSignal(options), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(salt), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(fileVersionMajor), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(fileVersionMinor), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(fileVersionBuild), + CommunicationsUtilities.AvoidEndOfHandshakeSignal(fileVersionPrivate), + }; + } + + public override string GetKey() + { + return $"{options} {salt} {fileVersionMajor} {fileVersionMinor} {fileVersionBuild} {fileVersionPrivate}" + .ToString(CultureInfo.InvariantCulture); + } + + /// + /// Computes Handshake stable hash string representing whole state of handshake. + /// + public string ComputeHash() + { + 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/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/INodePacket.cs b/src/Shared/INodePacket.cs index 481a99bfce9..0ddbf49a0d7 100644 --- a/src/Shared/INodePacket.cs +++ b/src/Shared/INodePacket.cs @@ -189,6 +189,30 @@ 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, + + /// + /// Command to cancel ongoing build. + /// Keep this enum value constant intact as this is part of contract with dotnet CLI + /// + ServerNodeBuildCancel = 0xF3, } #endregion diff --git a/src/Shared/ITranslator.cs b/src/Shared/ITranslator.cs index 42274c2da1d..3a507470744 100644 --- a/src/Shared/ITranslator.cs +++ b/src/Shared/ITranslator.cs @@ -241,7 +241,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..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,6 +17,11 @@ internal static string GetPipeNameOrPath(int? processId = null) string pipeName = $"MSBuild{processId}"; + return GetPlatformSpecificPipeName(pipeName); + } + + internal static string GetPlatformSpecificPipeName(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..4c5a3357063 100644 --- a/src/Shared/NodeEndpointOutOfProcBase.cs +++ b/src/Shared/NodeEndpointOutOfProcBase.cs @@ -72,6 +72,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 /// @@ -178,6 +185,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 @@ -185,7 +200,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 +209,7 @@ internal void InternalConstruct() _packetStream = new MemoryStream(); _binaryWriter = new BinaryWriter(_packetStream); - string pipeName = NamedPipeUtil.GetPipeNameOrPath(); + pipeName ??= NamedPipeUtil.GetPlatformSpecificPipeName(); #if FEATURE_PIPE_SECURITY && FEATURE_NAMED_PIPE_SECURITY_CONSTRUCTOR if (!NativeMethodsShared.IsMono) @@ -311,6 +326,7 @@ private void InitializeAsyncPacketThread() { lock (_asyncDataMonitor) { + _isClientDisconnecting = false; _packetPump = new Thread(PacketPumpProc); _packetPump.IsBackground = true; _packetPump.Name = "OutOfProc Endpoint Packet Pump"; @@ -547,14 +563,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 { diff --git a/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj b/src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj index c6fad498960..27b84e0e528 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; } }