diff --git a/src/msbuild/.vsts-dotnet.yml b/src/msbuild/.vsts-dotnet.yml index 971723d8e9e6..ff755704e125 100644 --- a/src/msbuild/.vsts-dotnet.yml +++ b/src/msbuild/.vsts-dotnet.yml @@ -49,7 +49,10 @@ variables: - name: SkipApplyOptimizationData value: true - name: EnableReleaseOneLocBuild - value: true + value: false + - ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}: + - name: EnableReleaseOneLocBuild + value: true - name: Codeql.Enabled value: true - group: DotNet-MSBuild-SDLValidation-Params diff --git a/src/msbuild/Directory.Build.props b/src/msbuild/Directory.Build.props index 939e86a11897..9e77a16332d7 100644 --- a/src/msbuild/Directory.Build.props +++ b/src/msbuild/Directory.Build.props @@ -15,6 +15,12 @@ XUnitV3 + + <_MSBuildXUnitV3Pin>3.2.2 + <_MSBuildMTPPin>1.9.1 + + $(_MSBuildXUnitV3Pin) + $(_MSBuildMTPPin) Exe diff --git a/src/msbuild/eng/Version.Details.xml b/src/msbuild/eng/Version.Details.xml index cca78c3f67b0..42dac4e29045 100644 --- a/src/msbuild/eng/Version.Details.xml +++ b/src/msbuild/eng/Version.Details.xml @@ -1,6 +1,6 @@ - + @@ -123,4 +123,4 @@ 3454b2fe822e52373f2604856417b0e6bce71d70 - + \ No newline at end of file diff --git a/src/msbuild/eng/Versions.props b/src/msbuild/eng/Versions.props index 7bcfb77feaa6..ea4e77681353 100644 --- a/src/msbuild/eng/Versions.props +++ b/src/msbuild/eng/Versions.props @@ -81,7 +81,7 @@ - 10.0.106 + 10.0.107 diff --git a/src/msbuild/src/Build.UnitTests/BackEnd/LoggingService_Tests.cs b/src/msbuild/src/Build.UnitTests/BackEnd/LoggingService_Tests.cs index 7db8ea66dddf..44c8606a77c2 100644 --- a/src/msbuild/src/Build.UnitTests/BackEnd/LoggingService_Tests.cs +++ b/src/msbuild/src/Build.UnitTests/BackEnd/LoggingService_Tests.cs @@ -1153,7 +1153,12 @@ public void ProcessLoggingEventConcurrentWithShutdown_DoesNotThrow() LoggingService loggingService = (LoggingService)LoggingService.CreateLoggingService(LoggerMode.Asynchronous, 1); ((IBuildComponent)loggingService).InitializeComponent(mockHost); - loggingService.RegisterLogger(new ConsoleLogger()); + + loggingService.RegisterLogger(new MockLogger()); + + Exception loggingThreadException = null; + loggingService.OnLoggingThreadException += ex => + Interlocked.CompareExchange(ref loggingThreadException, ex, null); using ManualResetEvent startSignal = new ManualResetEvent(false); Exception caughtException = null; @@ -1166,7 +1171,10 @@ public void ProcessLoggingEventConcurrentWithShutdown_DoesNotThrow() { try { - BuildMessageEventArgs msg = new BuildMessageEventArgs($"Message {i}", null, null, MessageImportance.Low); + BuildMessageEventArgs msg = new BuildMessageEventArgs($"Message {i}", null, null, MessageImportance.Low) + { + BuildEventContext = new BuildEventContext(0, 0, 0, 0), + }; loggingService.ProcessLoggingEvent(msg); } catch (Exception ex) @@ -1186,6 +1194,7 @@ public void ProcessLoggingEventConcurrentWithShutdown_DoesNotThrow() bool joined = logThread.Join(TimeSpan.FromSeconds(10)); joined.ShouldBeTrue("Logging thread did not terminate within the allotted time."); caughtException.ShouldBeNull(); + loggingThreadException.ShouldBeNull(); } #endregion diff --git a/src/msbuild/src/Build.UnitTests/BackEnd/MSBuildClient_Tests.cs b/src/msbuild/src/Build.UnitTests/BackEnd/MSBuildClient_Tests.cs new file mode 100644 index 000000000000..ed55fd078d9c --- /dev/null +++ b/src/msbuild/src/Build.UnitTests/BackEnd/MSBuildClient_Tests.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.IO; +using System.Threading; +using Microsoft.Build.Experimental; +using Microsoft.Build.UnitTests; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Tests for the fallback behaviour. + /// + public class MSBuildClient_Tests + { + private readonly ITestOutputHelper _output; + + public MSBuildClient_Tests(ITestOutputHelper output) + { + _output = output; + } + + /// + /// When the configured msbuild executable does not exist, launching the server fails. + /// must return a recoverable exit type rather than + /// letting an exception escape — it is the host's contract that any failure inside + /// Execute routes through so callers (e.g. + /// MSBuildClientApp) can fall back to in-process execution. + /// + /// + /// Regression coverage for the .NET 10.0.300 / Aspire timeout: when + /// DOTNET_CLI_USE_MSBUILD_SERVER=true is honoured but the server child cannot + /// start (e.g. the apphost can't find the .NET runtime), + /// must not propagate a or any other exception. + /// Pre-fix, an uncaught TimeoutException from NamedPipeClientStream.Connect + /// escaped past MSBuildClientApp and crashed the CLI; this test locks in the + /// no-exception-escape contract by simply calling Execute outside any try/catch + /// and asserting on the structured result. + /// + [Fact] + public void Execute_WithUnreachableServer_DoesNotPropagateException() + { + // Isolate from any real MSBuild server / DOTNET_CLI_USE_MSBUILD_SERVER state on + // the dev or CI machine. Without this the named-mutex check in MSBuildClient can + // observe a warm server from another test/run, which makes the assertions below + // non-deterministic. + using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable("DOTNET_CLI_USE_MSBUILD_SERVER", null); + env.SetEnvironmentVariable("MSBUILDUSESERVER", null); + + string[] commandLine = ["dummy.proj"]; + string nonexistentMsBuild = Path.Combine( + Path.GetTempPath(), + "msbuildclient-tests-" + Guid.NewGuid().ToString("N"), + "MSBuild.dll"); + + MSBuildClient client = new MSBuildClient(commandLine, nonexistentMsBuild); + + // The whole point of the regression fix: this must NOT throw. xUnit fails the + // test with the offending stack if any exception escapes, which is the + // primary regression contract being verified. + MSBuildClientExitResult result = client.Execute(CancellationToken.None); + + result.ShouldNotBeNull(); + + // The unreachable-server path must produce one of the recoverable failure types + // so that MSBuildClientApp can fall back to in-process execution. Crucially, + // ServerBusy is excluded here: ServerBusy is the "another client is racing for + // the launch mutex" path and is not what an unreachable server should produce — + // accepting it would mask a real regression where the failure was misclassified. + result.MSBuildClientExitType.ShouldBeOneOf( + MSBuildClientExitType.LaunchError, + MSBuildClientExitType.UnableToConnect, + MSBuildClientExitType.UnknownServerState); + + // No server child was successfully launched, so the diagnostic helper should not + // have observed an exit code. (Once issue #13718 lands and the diagnostic helper + // is plumbed through every connect failure, this gives MSBuildClientApp the right + // signal to pick the generic "server unavailable" message rather than the more + // specific "crashed with exit code N" one.) + result.ServerProcessExitCode.ShouldBeNull(); + } + } +} diff --git a/src/msbuild/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs b/src/msbuild/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs index a5b1b7627520..fa60eee08894 100644 --- a/src/msbuild/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs +++ b/src/msbuild/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs @@ -3,8 +3,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Reflection; +using System.Text.RegularExpressions; +using Microsoft.Build.BackEnd; using Microsoft.Build.Execution; using Microsoft.Build.Framework; using Microsoft.Build.Shared; @@ -381,7 +384,256 @@ public void ExplicitTaskHostFactory_OverridesRoutingLogic() logger.FullLog.ShouldContain("TaskWithAttribute executed"); } + [Fact] + public void RequiresTransientTaskHost_ReturnsTrueForRestoreTask() + { + TaskRouter.RequiresTransientTaskHost(typeof(NuGet.Build.Tasks.RestoreTask)).ShouldBeTrue(); + } + + [Fact] + public void RequiresTransientTaskHost_ReturnsFalseForRegularTask() + { + TaskRouter.RequiresTransientTaskHost(typeof(NonEnlightenedTestTask)).ShouldBeFalse(); + TaskRouter.RequiresTransientTaskHost(typeof(AttributeTestTask)).ShouldBeFalse(); + } + + [Fact] + public void RequiresTransientTaskHost_RoutedToTaskHost_InMultiThreadedMode() + { + string projectContent = $@" + + + + + + +"; + + string projectFile = Path.Combine(_testProjectsDir, "RestoreTaskMT.proj"); + File.WriteAllText(projectFile, projectContent); + + var logger = new MockLogger(_output); + var buildParameters = new BuildParameters + { + MultiThreaded = true, + Loggers = [logger], + DisableInProcNode = false, + EnableNodeReuse = false, + }; + + var buildRequestData = new BuildRequestData( + projectFile, + new Dictionary(), + null, + ["TestTarget"], + null); + + BuildManager buildManager = BuildManager.DefaultBuildManager; + BuildResult result = buildManager.Build(buildParameters, buildRequestData); + + result.OverallResult.ShouldBe(BuildResultCode.Success); + TaskRouterTestHelper.AssertTaskUsedTaskHost(logger, "RestoreTask"); + logger.FullLog.ShouldContain("RestoreTask executed"); + } + + [Fact] + public void RequiresTransientTaskHost_RoutedToTaskHost_InServerMode() + { + string projectContent = $@" + + + + + + +"; + + string projectFile = Path.Combine(_testProjectsDir, "RestoreTaskServer.proj"); + File.WriteAllText(projectFile, projectContent); + + var logger = new MockLogger(_output); + var buildParameters = new BuildParameters + { + MultiThreaded = false, + Loggers = [logger], + DisableInProcNode = false, + EnableNodeReuse = false, + + // Simulate running under the MSBuild Server. In production this flag is + // set process-wide by OutOfProcServerNode via BuildParameters.MarkProcessAsLongLivedHost. + IsLongLivedHost = true, + }; + + var buildRequestData = new BuildRequestData( + projectFile, + new Dictionary(), + null, + ["TestTarget"], + null); + + BuildManager buildManager = BuildManager.DefaultBuildManager; + BuildResult result = buildManager.Build(buildParameters, buildRequestData); + + result.OverallResult.ShouldBe(BuildResultCode.Success); + TaskRouterTestHelper.AssertTaskUsedTaskHost(logger, "RestoreTask"); + logger.FullLog.ShouldContain("RestoreTask executed"); + } + + [Fact] + public void RequiresTransientTaskHost_GetsFreshProcess_OnEachInvocation_InMultiThreadedMode() + { + string projectContent = $@" + + + + + + +"; + + string projectFile = Path.Combine(_testProjectsDir, "RestoreTaskFreshProcess.proj"); + File.WriteAllText(projectFile, projectContent); + + var logger = new MockLogger(_output); + var buildParameters = new BuildParameters + { + MultiThreaded = true, + Loggers = [logger], + DisableInProcNode = false, + + // Reuse must stay ON so we test the workaround, not natural process death. + // With reuse off the test cannot distinguish "transient TaskHost (workaround)" + // from "sidecar TaskHost that happened to die between builds". + EnableNodeReuse = true, + }; + + var buildRequestData = new BuildRequestData( + projectFile, + new Dictionary(), + null, + ["TestTarget"], + null); + + // Two separate Build cycles. The workaround forces nodeReuse=false on the spawned + // TaskHost so it dies at EndBuild, giving the next Build a fresh process. + BuildManager buildManager = BuildManager.DefaultBuildManager; + BuildResult result1 = buildManager.Build(buildParameters, buildRequestData); + BuildResult result2 = buildManager.Build(buildParameters, buildRequestData); + + result1.OverallResult.ShouldBe(BuildResultCode.Success); + result2.OverallResult.ShouldBe(BuildResultCode.Success); + TaskRouterTestHelper.AssertTaskUsedTaskHost(logger, "RestoreTask"); + + int[] pids = ExtractReportedPids(logger.FullLog); + + pids.Length.ShouldBe(2, $"Expected two RestoreTask invocations to log a PID. Log:{Environment.NewLine}{logger.FullLog}"); + pids[0].ShouldNotBe(pids[1], "Each build must spawn a fresh TaskHost so NuGet static state cannot leak across builds."); + pids.ShouldNotContain(Process.GetCurrentProcess().Id, "TaskHost should be out-of-process from the test runner."); + } + + [Fact] + public void RequiresTransientTaskHost_GetsFreshProcess_OnEachInvocation_InServerMode() + { + string projectContent = $@" + + + + + + +"; + + string projectFile = Path.Combine(_testProjectsDir, "RestoreTaskFreshProcessServer.proj"); + File.WriteAllText(projectFile, projectContent); + var logger = new MockLogger(_output); + var buildParameters = new BuildParameters + { + MultiThreaded = false, + Loggers = [logger], + DisableInProcNode = false, + + // Reuse must stay ON so we test the workaround, not natural process death. + EnableNodeReuse = true, + + // Simulate running under the MSBuild Server. + IsLongLivedHost = true, + }; + + var buildRequestData = new BuildRequestData( + projectFile, + new Dictionary(), + null, + ["TestTarget"], + null); + + // Two separate Build cycles. The workaround forces nodeReuse=false on the spawned + // TaskHost so it dies at EndBuild, giving the next Build a fresh process. + BuildManager buildManager = BuildManager.DefaultBuildManager; + BuildResult result1 = buildManager.Build(buildParameters, buildRequestData); + BuildResult result2 = buildManager.Build(buildParameters, buildRequestData); + + result1.OverallResult.ShouldBe(BuildResultCode.Success); + result2.OverallResult.ShouldBe(BuildResultCode.Success); + TaskRouterTestHelper.AssertTaskUsedTaskHost(logger, "RestoreTask"); + + int[] pids = ExtractReportedPids(logger.FullLog); + + pids.Length.ShouldBe(2, $"Expected two RestoreTask invocations to log a PID. Log:{Environment.NewLine}{logger.FullLog}"); + pids[0].ShouldNotBe(pids[1], "Each build must spawn a fresh TaskHost so NuGet static state cannot leak across builds."); + pids.ShouldNotContain(Process.GetCurrentProcess().Id, "TaskHost should be out-of-process from the test runner."); + } + + private static int[] ExtractReportedPids(string log) + { + var pids = new List(); + foreach (Match m in Regex.Matches(log, @"RestoreTask executed in PID=(\d+)")) + { + pids.Add(int.Parse(m.Groups[1].Value)); + } + + return pids.ToArray(); + } + + [Fact] + public void RequiresTransientTaskHost_RunsInProcess_WhenNoMTOrServer() + { + string projectContent = $@" + + + + + + +"; + + string projectFile = Path.Combine(_testProjectsDir, "RestoreTaskNoMT.proj"); + File.WriteAllText(projectFile, projectContent); + + var logger = new MockLogger(_output); + var buildParameters = new BuildParameters + { + MultiThreaded = false, + Loggers = [logger], + DisableInProcNode = false, + EnableNodeReuse = false, + }; + + var buildRequestData = new BuildRequestData( + projectFile, + new Dictionary(), + null, + ["TestTarget"], + null); + + BuildManager buildManager = BuildManager.DefaultBuildManager; + BuildResult result = buildManager.Build(buildParameters, buildRequestData); + + result.OverallResult.ShouldBe(BuildResultCode.Success); + + TaskRouterTestHelper.AssertTaskRanInProcess(logger, "RestoreTask"); + logger.FullLog.ShouldContain("RestoreTask executed"); + } private string CreateTestProject(string taskName, string taskClass) { @@ -483,6 +735,25 @@ public override bool Execute() #endregion } +// Test task in the NuGet.Build.Tasks namespace to simulate the real RestoreTask for routing tests. +namespace NuGet.Build.Tasks +{ + /// + /// Simulates the NuGet RestoreTask for testing task routing workaround. + /// Has the same full name (NuGet.Build.Tasks.RestoreTask) that TaskRouter checks. + /// + public class RestoreTask : Task + { + public override bool Execute() + { + // Include the OS PID so tests can verify each invocation runs in a fresh + // TaskHost process + Log.LogMessage(MessageImportance.High, $"RestoreTask executed in PID={Process.GetCurrentProcess().Id}"); + return true; + } + } +} + // Custom attribute definition in Microsoft.Build.Framework namespace to match what TaskRouter expects // TaskRouter looks for attributes with FullName = "Microsoft.Build.Framework.MSBuildMultiThreadableTaskAttribute" // Since the real attribute is internal in Framework, we define our own test version here diff --git a/src/msbuild/src/Build.UnitTests/FileLogger_Tests.cs b/src/msbuild/src/Build.UnitTests/FileLogger_Tests.cs index 28161a9a7831..09d52d7dcaff 100644 --- a/src/msbuild/src/Build.UnitTests/FileLogger_Tests.cs +++ b/src/msbuild/src/Build.UnitTests/FileLogger_Tests.cs @@ -8,6 +8,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Logging; using Microsoft.Build.Shared; +using Shouldly; using Xunit; using EventSourceSink = Microsoft.Build.BackEnd.Logging.EventSourceSink; using Project = Microsoft.Build.Evaluation.Project; @@ -18,6 +19,13 @@ namespace Microsoft.Build.UnitTests { public class FileLogger_Tests { + private readonly ITestOutputHelper _output; + + public FileLogger_Tests(ITestOutputHelper output) + { + _output = output; + } + /// /// Basic test of the file logger. Writes to a log file in the temp directory. /// @@ -482,44 +490,48 @@ private void VerifyFileContent(string file, string expectedContent) [Fact] public void DistributedFileLoggerParameters() { - DistributedFileLogger fileLogger = new DistributedFileLogger(); - try - { - fileLogger.NodeId = 0; - fileLogger.Initialize(new EventSourceSink()); - Assert.Equal(0, string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=msbuild0.log;", StringComparison.OrdinalIgnoreCase)); - fileLogger.Shutdown(); + using TestEnvironment env = TestEnvironment.Create(_output); - fileLogger.NodeId = 3; - fileLogger.Parameters = "logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "mylogfile.log"); - fileLogger.Initialize(new EventSourceSink()); - Assert.Equal(0, string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "mylogfile3.log") + ";", StringComparison.OrdinalIgnoreCase)); - fileLogger.Shutdown(); + // Use a transient folder under TEMP as the current directory so log files (notably the + // default "msbuild0.log" produced when no logfile parameter is specified) don't leak into + // the test bin directory, where another process (e.g. an antivirus scanner or a parallel + // test) may transiently lock them and cause this test to fail. + TransientTestFolder folder = env.CreateFolder(); + env.SetCurrentDirectory(folder.Path); - fileLogger.NodeId = 4; - fileLogger.Parameters = "logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "mylogfile.log"); - fileLogger.Initialize(new EventSourceSink()); - Assert.Equal(0, string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "mylogfile4.log") + ";", StringComparison.OrdinalIgnoreCase)); - fileLogger.Shutdown(); + DistributedFileLogger fileLogger = new DistributedFileLogger(); - Directory.CreateDirectory(Path.Combine(Directory.GetCurrentDirectory(), "tempura")); - fileLogger.NodeId = 1; - fileLogger.Parameters = "logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "tempura", "mylogfile.log"); - fileLogger.Initialize(new EventSourceSink()); - Assert.Equal(0, string.Compare(fileLogger.InternalFilelogger.Parameters, "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(Directory.GetCurrentDirectory(), "tempura", "mylogfile1.log") + ";", StringComparison.OrdinalIgnoreCase)); - fileLogger.Shutdown(); - } - finally - { - if (Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "tempura"))) - { - File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "tempura", "mylogfile1.log")); - FileUtilities.DeleteWithoutTrailingBackslash(Path.Combine(Directory.GetCurrentDirectory(), "tempura")); - } - File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "mylogfile0.log")); - File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "mylogfile3.log")); - File.Delete(Path.Combine(Directory.GetCurrentDirectory(), "mylogfile4.log")); - } + fileLogger.NodeId = 0; + fileLogger.Initialize(new EventSourceSink()); + fileLogger.InternalFilelogger.Parameters.ShouldBe( + "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=msbuild0.log;", + StringCompareShould.IgnoreCase); + fileLogger.Shutdown(); + + fileLogger.NodeId = 3; + fileLogger.Parameters = "logfile=" + Path.Combine(folder.Path, "mylogfile.log"); + fileLogger.Initialize(new EventSourceSink()); + fileLogger.InternalFilelogger.Parameters.ShouldBe( + "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(folder.Path, "mylogfile3.log") + ";", + StringCompareShould.IgnoreCase); + fileLogger.Shutdown(); + + fileLogger.NodeId = 4; + fileLogger.Parameters = "logfile=" + Path.Combine(folder.Path, "mylogfile.log"); + fileLogger.Initialize(new EventSourceSink()); + fileLogger.InternalFilelogger.Parameters.ShouldBe( + "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(folder.Path, "mylogfile4.log") + ";", + StringCompareShould.IgnoreCase); + fileLogger.Shutdown(); + + TransientTestFolder subfolder = folder.CreateDirectory("tempura"); + fileLogger.NodeId = 1; + fileLogger.Parameters = "logfile=" + Path.Combine(subfolder.Path, "mylogfile.log"); + fileLogger.Initialize(new EventSourceSink()); + fileLogger.InternalFilelogger.Parameters.ShouldBe( + "ForceNoAlign;ShowEventId;ShowCommandLine;logfile=" + Path.Combine(subfolder.Path, "mylogfile1.log") + ";", + StringCompareShould.IgnoreCase); + fileLogger.Shutdown(); } [Fact] diff --git a/src/msbuild/src/Build/BackEnd/BuildManager/BuildParameters.cs b/src/msbuild/src/Build/BackEnd/BuildManager/BuildParameters.cs index 1920c649399f..be053243ffad 100644 --- a/src/msbuild/src/Build/BackEnd/BuildManager/BuildParameters.cs +++ b/src/msbuild/src/Build/BackEnd/BuildManager/BuildParameters.cs @@ -64,6 +64,12 @@ public class BuildParameters : ITranslatable /// private static string s_startupDirectory = NativeMethodsShared.GetCurrentDirectory(); + /// + /// Process-wide flag indicating that the engine is hosted in a long-lived process + /// (e.g., the MSBuild Server) that persists across multiple build invocations. + /// + private static bool s_isLongLivedHost; + /// /// Indicates whether we should warn when a property is uninitialized when it is used. /// @@ -328,6 +334,7 @@ internal BuildParameters(BuildParameters other, bool resetEnvironment = false) Question = other.Question; IsBuildCheckEnabled = other.IsBuildCheckEnabled; IsTelemetryEnabled = other.IsTelemetryEnabled; + IsLongLivedHost = other.IsLongLivedHost; ProjectCacheDescriptor = other.ProjectCacheDescriptor; _enableTargetOutputLogging = other.EnableTargetOutputLogging; } @@ -799,6 +806,25 @@ internal IToolsetProvider ToolsetProvider /// internal bool IsOutOfProc { get; set; } + /// + /// True when the engine is hosted in a long-lived process (e.g., MSBuild Server) + /// that persists across multiple build invocations. Used to opt tasks whose static + /// singleton state would leak across invocations out of sidecar TaskHost reuse. + /// See https://github.com/dotnet/msbuild/issues/13315. + /// + internal bool IsLongLivedHost + { + get => _isLongLivedHost; + set => _isLongLivedHost = value; + } + + private bool _isLongLivedHost = s_isLongLivedHost; + + /// + /// Marks the current process as a long-lived host. + /// + internal static void MarkProcessAsLongLivedHost() => s_isLongLivedHost = true; + /// public ProjectLoadSettings ProjectLoadSettings { @@ -978,6 +1004,7 @@ void ITranslatable.Translate(ITranslator translator) translator.Translate(ref _reportFileAccesses); translator.Translate(ref _enableTargetOutputLogging); translator.Translate(ref _multiThreaded); + translator.Translate(ref _isLongLivedHost); // ProjectRootElementCache is not transmitted. // ResetCaches is not transmitted. diff --git a/src/msbuild/src/Build/BackEnd/Client/MSBuildClient.cs b/src/msbuild/src/Build/BackEnd/Client/MSBuildClient.cs index c10736832560..4c6a61fb3621 100644 --- a/src/msbuild/src/Build/BackEnd/Client/MSBuildClient.cs +++ b/src/msbuild/src/Build/BackEnd/Client/MSBuildClient.cs @@ -101,6 +101,12 @@ public sealed class MSBuildClient /// private MSBuildClientPacketPump _packetPump = null!; + /// + /// PID of the server process this client launched (or null if no launch was attempted / + /// the server was already running). Used for diagnostics on connection failure. + /// + private int? _launchedServerPid; + /// /// Public constructor with parameters. /// @@ -459,8 +465,19 @@ private bool TryLaunchServer() ]; NodeLauncher nodeLauncher = new NodeLauncher(); CommunicationsUtilities.Trace("Starting Server..."); - using Process msbuildProcess = nodeLauncher.Start(new NodeLaunchData(_msbuildLocation, string.Join(" ", msBuildServerOptions)), nodeId: 0); - CommunicationsUtilities.Trace($"Server started with PID: {msbuildProcess?.Id}"); + + // Set DOTNET_ROOT so the apphost server child can locate the runtime; this + // override is replaced by the client's environment on the first build command + // (see OutOfProcServerNode.HandleServerNodeBuildCommand → SetEnvironment). + // The `!` works around dotnet/msbuild#13761. + NodeLaunchData launchData = new( + MSBuildLocation: _msbuildLocation, + CommandLineArgs: string.Join(" ", msBuildServerOptions), + EnvironmentOverrides: DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides()!); + + using Process msbuildProcess = nodeLauncher.Start(launchData, nodeId: 0); + _launchedServerPid = msbuildProcess.Id; + CommunicationsUtilities.Trace($"Server started with PID: {_launchedServerPid}"); } catch (Exception ex) { @@ -605,9 +622,36 @@ private bool TryConnectToServer(int timeoutMilliseconds) { tryAgain = false; + HandshakeResult result; + bool connected; + try + { + connected = NodeProviderOutOfProcBase.TryConnectToPipeStream( + _nodeStream, _pipeName, _handshake, Math.Max(1, timeoutMilliseconds - (int)sw.ElapsedMilliseconds), out result); + } + catch (TimeoutException) + { + // The underlying NamedPipeClientStream.Connect throws TimeoutException when the + // pipe never becomes available — typically because the server child process + // failed to start (e.g. apphost couldn't locate the runtime). Treat this as a + // recoverable connection failure so MSBuildClientApp can fall back to in-proc + // execution rather than crashing the whole CLI. + LogConnectFailureDiagnostics(timeoutMilliseconds, isTimeout: true, errorMessage: null); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.UnableToConnect; + return false; + } + catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex)) + { + // Mirror the exception-tolerant behavior of NodeProviderOutOfProcBase.TryConnectToProcess + // so any non-critical failure (UnauthorizedAccessException, IOException, + // InvalidOperationException, etc.) routes through the standard fallback path + // rather than escaping out of MSBuildClient.Execute. + LogConnectFailureDiagnostics(timeoutMilliseconds, isTimeout: false, errorMessage: ex.Message); + _exitResult.MSBuildClientExitType = MSBuildClientExitType.UnableToConnect; + return false; + } - if (NodeProviderOutOfProcBase.TryConnectToPipeStream( - _nodeStream, _pipeName, _handshake, Math.Max(1, timeoutMilliseconds - (int)sw.ElapsedMilliseconds), out HandshakeResult result)) + if (connected) { return true; } @@ -623,7 +667,7 @@ private bool TryConnectToServer(int timeoutMilliseconds) } else { - CommunicationsUtilities.Trace($"Failed to connect to server: {result.ErrorMessage}"); + LogConnectFailureDiagnostics(timeoutMilliseconds, isTimeout: result.Status is HandshakeStatus.Timeout, errorMessage: result.ErrorMessage); _exitResult.MSBuildClientExitType = MSBuildClientExitType.UnableToConnect; return false; } @@ -633,6 +677,57 @@ private bool TryConnectToServer(int timeoutMilliseconds) return false; } + /// + /// Emits a single diagnostic trace entry describing why connection to the MSBuild server + /// failed, including the launched server PID (if any) and its current state. This makes + /// the otherwise-opaque 20s timeout actionable when MSBUILDDEBUGCOMM tracing is enabled. + /// Also populates when the + /// launched server child has already exited, so the host can surface that fact to the + /// user-visible "falling back to in-proc" message instead of a generic timeout. + /// + private void LogConnectFailureDiagnostics(int timeoutMilliseconds, bool isTimeout, string? errorMessage) + { + string serverState; + if (_launchedServerPid is int pid) + { + try + { + using Process launched = Process.GetProcessById(pid); + if (launched.HasExited) + { + _exitResult.ServerProcessExitCode = launched.ExitCode; + serverState = $"PID {pid} (already exited with code {launched.ExitCode})"; + } + else + { + serverState = $"PID {pid} (still running)"; + } + } + catch (ArgumentException) + { + // Process already terminated and was reaped before we could query it. + serverState = $"PID {pid} (already exited)"; + } + catch (InvalidOperationException) + { + serverState = $"PID {pid} (state unavailable)"; + } + } + else + { + serverState = "no launch attempted (server reported as already running)"; + } + + string reason = isTimeout + ? $"timed out after {timeoutMilliseconds} ms waiting for the named pipe" + : $"connection error: {errorMessage}"; + + CommunicationsUtilities.Trace( + $"MSBuild server connection failed ({reason}). Launched server: {serverState}. " + + "Falling back to in-proc build. " + + "If the server child process exited immediately, ensure DOTNET_ROOT is set correctly so the apphost can locate the .NET runtime."); + } + private void WritePacket(Stream nodeStream, INodePacket packet) { MemoryStream memoryStream = _packetMemoryStream; diff --git a/src/msbuild/src/Build/BackEnd/Client/MSBuildClientExitResult.cs b/src/msbuild/src/Build/BackEnd/Client/MSBuildClientExitResult.cs index ef58bba7517d..76cf5d9ed4c4 100644 --- a/src/msbuild/src/Build/BackEnd/Client/MSBuildClientExitResult.cs +++ b/src/msbuild/src/Build/BackEnd/Client/MSBuildClientExitResult.cs @@ -20,5 +20,14 @@ public sealed class MSBuildClientExitResult /// This field is null if MSBuild client execution was not successful. /// public string? MSBuildAppExitTypeString { get; set; } + + /// + /// When this client launched a server child process and that process had already exited + /// by the time we observed the connection failure, this is its exit code. null + /// otherwise (server still running, never launched, or its state could not be queried). + /// Hosts use this to surface "server crashed immediately on launch" to the user instead + /// of a generic timeout message. + /// + public int? ServerProcessExitCode { get; set; } } } diff --git a/src/msbuild/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/msbuild/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs index b86f689f60d9..c597b77bc61f 100644 --- a/src/msbuild/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs +++ b/src/msbuild/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs @@ -631,11 +631,17 @@ internal bool AcquireAndSetUpHost( INodePacketFactory factory, INodePacketHandler handler, TaskHostConfiguration configuration, - in TaskHostParameters taskHostParameters) + in TaskHostParameters taskHostParameters, + out int hostProcessId, + out bool wasNewlyCreated) { + hostProcessId = -1; + wasNewlyCreated = false; + bool nodeCreationSucceeded; if (!_nodeContexts.ContainsKey(nodeKey)) { + wasNewlyCreated = true; nodeCreationSucceeded = CreateNode(nodeKey, factory, handler, configuration, taskHostParameters); } else @@ -654,6 +660,16 @@ internal bool AcquireAndSetUpHost( handlerStack.Push(handler); } + try + { + hostProcessId = context.Process?.Id ?? -1; + } + catch (Exception ex) when (!ExceptionHandling.IsCriticalException(ex)) + { + // Process has already exited or is otherwise inaccessible; PID is unavailable. + hostProcessId = -1; + } + // Configure the node. context.SendData(configuration); return true; diff --git a/src/msbuild/src/Build/BackEnd/Components/RequestBuilder/TaskRouter.cs b/src/msbuild/src/Build/BackEnd/Components/RequestBuilder/TaskRouter.cs index 3b9a6252e1a5..8262c7170d2d 100644 --- a/src/msbuild/src/Build/BackEnd/Components/RequestBuilder/TaskRouter.cs +++ b/src/msbuild/src/Build/BackEnd/Components/RequestBuilder/TaskRouter.cs @@ -84,6 +84,37 @@ private static bool HasMultiThreadableTaskAttribute(Type taskType) }); } + /// + /// Full name of a task whose static singleton state makes it unsafe to run in a + /// long-lived sidecar TaskHost (which persists across invocations). Such tasks must + /// instead run in an explicit (transient) TaskHost that terminates after execution, + /// ensuring static state is cleaned up. + /// This is a temporary workaround until the task authors fix their static state issues. + /// See https://github.com/dotnet/msbuild/issues/13315 + /// + private const string TaskRequiringTransientTaskHostFullName = "NuGet.Build.Tasks.RestoreTask"; + + /// + /// Determines if a task must be routed to an explicit (transient) TaskHost rather than + /// a reusable sidecar, because its static singleton state would leak across invocations. + /// Such tasks should run in a TaskHost that terminates after execution so all static + /// state is cleaned up. + /// + /// The type of the task to evaluate. + /// True if the task requires a transient TaskHost; false otherwise. + public static bool RequiresTransientTaskHost(Type taskType) + { + ErrorUtilities.VerifyThrowArgumentNull(taskType, nameof(taskType)); + + string? fullName = taskType.FullName; + if (fullName is null) + { + return false; + } + + return string.Equals(fullName, TaskRequiringTransientTaskHostFullName, StringComparison.Ordinal); + } + /// /// Clears the thread-safety cache. Used primarily for testing. /// diff --git a/src/msbuild/src/Build/BackEnd/Node/OutOfProcServerNode.cs b/src/msbuild/src/Build/BackEnd/Node/OutOfProcServerNode.cs index 3abc9faaaab4..dca6ba59177f 100644 --- a/src/msbuild/src/Build/BackEnd/Node/OutOfProcServerNode.cs +++ b/src/msbuild/src/Build/BackEnd/Node/OutOfProcServerNode.cs @@ -108,6 +108,13 @@ public NodeEngineShutdownReason Run(out Exception? shutdownException) return NodeEngineShutdownReason.Error; } + // Mark the process as a long-lived host so per-build BuildParameters instances + // inherit IsLongLivedHost = true. This drives the workaround that routes tasks + // whose static state would leak across invocations (e.g., NuGet RestoreTask) to + // a transient TaskHost instead of a reusable sidecar. + // See https://github.com/dotnet/msbuild/issues/13315. + BuildParameters.MarkProcessAsLongLivedHost(); + while (true) { NodeEngineShutdownReason shutdownReason = RunInternal(out shutdownException, handshake); @@ -381,6 +388,7 @@ private void HandleServerNodeBuildCommand(ServerNodeBuildCommand command) Directory.SetCurrentDirectory(command.StartupDirectory); CommunicationsUtilities.SetEnvironment(command.BuildProcessEnvironment); + Traits.UpdateFromEnvironment(); Thread.CurrentThread.CurrentCulture = command.Culture; diff --git a/src/msbuild/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs b/src/msbuild/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs index ea47b86f1ca7..a496b6ee37a5 100644 --- a/src/msbuild/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs +++ b/src/msbuild/src/Build/Instance/TaskFactories/AssemblyTaskFactory.cs @@ -348,6 +348,23 @@ internal ITask CreateTaskInstance( } } + // Workaround for tasks whose static singleton state would leak across invocations + // (e.g., NuGet RestoreTask). In MT mode or when MSBuild server is active, these tasks + // must run in a transient (non-sidecar) TaskHost so static state is cleaned up after + // each invocation. See https://github.com/dotnet/msbuild/issues/13315 + bool forceTransientTaskHost = false; + if (_loadedType?.Type != null && TaskRouter.RequiresTransientTaskHost(_loadedType.Type)) + { + bool isMultiThreaded = buildComponentHost?.BuildParameters?.MultiThreaded == true; + bool isLongLivedHost = buildComponentHost?.BuildParameters?.IsLongLivedHost == true; + + if (isMultiThreaded || isLongLivedHost) + { + useTaskFactory = true; + forceTransientTaskHost = true; + } + } + taskLoggingContext?.TargetLoggingContext?.ProjectLoggingContext?.ProjectTelemetry?.AddTaskExecution(GetType().FullName, isTaskHost: useTaskFactory); if (useTaskFactory) @@ -362,7 +379,9 @@ internal ITask CreateTaskInstance( // If the task host factory is explicitly requested, do not act as a sidecar task host. // This is important as customers use task host factories for short lived tasks to release // potential locks. - bool useSidecarTaskHost = !(_factoryIdentityParameters.TaskHostFactoryExplicitlyRequested ?? false); + // Also disable sidecar for tasks that require a transient TaskHost so their + // static state is cleaned up between invocations. + bool useSidecarTaskHost = !forceTransientTaskHost && !(_factoryIdentityParameters.TaskHostFactoryExplicitlyRequested ?? false); TaskHostTask task = new( taskLocation, diff --git a/src/msbuild/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/msbuild/src/Build/Instance/TaskFactories/TaskHostTask.cs index 0cc1e8ba1c39..00cd69ef5a60 100644 --- a/src/msbuild/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/msbuild/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -355,21 +355,44 @@ public bool Execute() try { + int hostProcessId; + bool wasNewlyCreated; + bool effectiveNodeReuse; + lock (_taskHostLock) { + effectiveNodeReuse = _buildComponentHost.BuildParameters.EnableNodeReuse && _useSidecarTaskHost; + _requiredContext = CommunicationsUtilities.GetHandshakeOptions( taskHost: true, // Determine if we should use node reuse based on build parameters or user preferences (comes from UsingTask element). - nodeReuse: _buildComponentHost.BuildParameters.EnableNodeReuse && _useSidecarTaskHost, + nodeReuse: effectiveNodeReuse, taskHostParameters: _taskHostParameters); _taskHostNodeKey = new TaskHostNodeKey(_requiredContext, _scheduledNodeId); - _connectedToTaskHost = _taskHostProvider.AcquireAndSetUpHost(_taskHostNodeKey, this, this, hostConfiguration, _taskHostParameters); + _connectedToTaskHost = _taskHostProvider.AcquireAndSetUpHost( + _taskHostNodeKey, + this, + this, + hostConfiguration, + _taskHostParameters, + out hostProcessId, + out wasNewlyCreated); } if (_connectedToTaskHost) { + _taskLoggingContext.LogComment( + MessageImportance.Low, + "TaskHostDetails", + _taskType.Type.Name, + hostProcessId, + Process.GetCurrentProcess().Id, + wasNewlyCreated, + _useSidecarTaskHost, + effectiveNodeReuse); + try { bool taskFinished = false; diff --git a/src/msbuild/src/Build/Resources/Strings.resx b/src/msbuild/src/Build/Resources/Strings.resx index 2a73d8bfac6f..551b617d3edf 100644 --- a/src/msbuild/src/Build/Resources/Strings.resx +++ b/src/msbuild/src/Build/Resources/Strings.resx @@ -1231,6 +1231,10 @@ MSB4216: Could not run the "{0}" task because MSBuild could not create or connect to a task host with runtime "{1}" and architecture "{2}". Please ensure that (1) the requested runtime and/or architecture are available on the machine, and (2) that the required executable "{3}" exists and can be run. {StrBegin="MSB4216: "} + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + MSB4221: Could not run the "{0}" task because MSBuild could not create or connect to a task host with runtime "{1}" and architecture "{2}". Please ensure that (1) the requested runtime and/or architecture are available on the machine, and (2) that the required executable "{3}" exists and can be run. Error {4} {5}. {StrBegin="MSB4221: "} diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.cs.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.cs.xlf index 4e6eadee7b3f..0fd3049764f1 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.cs.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.cs.xlf @@ -1033,6 +1033,11 @@ Chyby: {3} Sestavení úlohy bylo načteno z{0}, ale požadované umístění bylo{1}. + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. Úloha {0} uvolnila tento počet jader: {1}. Teď používá celkem tento počet jader: {2} diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.de.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.de.xlf index 66dadcdf8636..1008d2753e78 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.de.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.de.xlf @@ -1033,6 +1033,11 @@ Fehler: {3} Die Aufgabenassembly wurde aus „{0}“ geladen, während der gewünschte Speicherort „{1}“ war. + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. Die Aufgabe "{0}" hat {1} Kerne freigegeben und belegt jetzt insgesamt {2} Kerne. diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.es.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.es.xlf index d6dae7553284..7b7310c4a370 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.es.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.es.xlf @@ -1033,6 +1033,11 @@ Errores: {3} El ensamblado de tarea se cargó desde "{0}" mientras que la ubicación deseada era "{1}". + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. La tarea "{0}" liberó {1} núcleos y ahora retiene un total de {2} núcleos. diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.fr.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.fr.xlf index d4c5f384041c..17e5946a98f6 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.fr.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.fr.xlf @@ -1033,6 +1033,11 @@ Erreurs : {3} L’assembly de tâche a été chargé à partir de « {0} » alors que l’emplacement souhaité était « {1} ». + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. La tâche "{0}" a libéré {1} cœur. Elle détient désormais {2} cœurs au total. diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.it.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.it.xlf index a058739283e3..69c63bdb0e77 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.it.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.it.xlf @@ -1033,6 +1033,11 @@ Errori: {3} L'assembly attività è stato caricato da "{0}" mentre era "{1}" il percorso desiderato. + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. L'attività "{0}" ha rilasciato {1} core e ora contiene {2} core in totale. diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.ja.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.ja.xlf index 8871c2d34fa8..2193e9351d2e 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.ja.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.ja.xlf @@ -1033,6 +1033,11 @@ Errors: {3} タスク アセンブリは '{0}' から読み込まれましたが、必要な場所は '{1}' でした。 + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. タスク "{0}" では、{1} 個のコアを解放したため、現在合計 {2} 個のコアを保持しています。 diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.ko.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.ko.xlf index d5f8f785a966..b28e9b68b244 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.ko.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.ko.xlf @@ -1033,6 +1033,11 @@ Errors: {3} 원하는 위치가 '{1}'인 동안 '{0}'에서 작업 어셈블리를 로드했습니다. + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. "{0}" 작업에서 코어 {1}개를 해제했고 지금 총 {2}개의 코어를 보유하고 있습니다. diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.pl.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.pl.xlf index a85f582f90fd..6eb2174f4a85 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.pl.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.pl.xlf @@ -1033,6 +1033,11 @@ Błędy: {3} Zestaw zadania został załadowany z lokalizacji „{0}”, gdy żądana lokalizacja to „{1}”. + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. Zadanie „{0}” zwolniło rdzenie ({1}) i teraz jego łączna liczba rdzeni to {2}. diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.pt-BR.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.pt-BR.xlf index e01063294c43..921a8c22d880 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.pt-BR.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.pt-BR.xlf @@ -1033,6 +1033,11 @@ Erros: {3} O assembly da tarefa foi carregado de "{0}" enquanto o local desejado era "{1}". + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. A tarefa "{0}" liberou {1} núcleos e agora contém {2} núcleos no total. diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.ru.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.ru.xlf index 19f964f69db5..a9bb33d6e423 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.ru.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.ru.xlf @@ -1033,6 +1033,11 @@ Errors: {3} Сборка задачи была загружена из "{0}", а нужное расположение — "{1}". + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. Задача "{0}" освободила указанное число ядер ({1}). Теперь общее число ядер, которыми располагает задача, равно {2}. diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.tr.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.tr.xlf index fe03e0c4cd40..60e715548cc7 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.tr.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.tr.xlf @@ -1033,6 +1033,11 @@ Hatalar: {3} İstenilen konum '{1}' iken görev derlemesi '{0}'dan yüklendi. + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. "{0}" görevi {1} çekirdeği serbest bıraktı. Şu anda toplam {2} çekirdek tutuyor. diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.zh-Hans.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.zh-Hans.xlf index f57171970d0e..74ac33603059 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.zh-Hans.xlf @@ -1033,6 +1033,11 @@ Errors: {3} 已从“{0}”加载任务程序集,但所需位置为“{1}”。 + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. 任务“{0}”发布了 {1} 个核心,现总共包含 {2} 个核心。 diff --git a/src/msbuild/src/Build/Resources/xlf/Strings.zh-Hant.xlf b/src/msbuild/src/Build/Resources/xlf/Strings.zh-Hant.xlf index c5ce7a635083..e5700513cb38 100644 --- a/src/msbuild/src/Build/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/msbuild/src/Build/Resources/xlf/Strings.zh-Hant.xlf @@ -1033,6 +1033,11 @@ Errors: {3} 工作組件已從 '{0}' 載入,但 '{1}' 才是所需的位置。 + + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Task "{0}" ran in TaskHost process {1}, spawned by caller process {2}. New TaskHost spawned for this invocation: {3}. TaskHost is a reusable sidecar: {4}. Node reuse: {5}. + Diagnostic message logged from TaskHostTask. {0} task name. {1} PID of the spawned TaskHost process. {2} PID of the MSBuild process that spawned it. {3}/{4}/{5} are booleans (True/False). LOCALIZATION: Do NOT localize "TaskHost" — it is an MSBuild feature name. + Task "{0}" released {1} cores and now holds {2} cores total. 工作 "{0}" 已發行 {1} 個核心,現在共保留 {2} 個核心。 diff --git a/src/msbuild/src/Directory.Build.targets b/src/msbuild/src/Directory.Build.targets index 7e0d7109693e..e38b805791f0 100644 --- a/src/msbuild/src/Directory.Build.targets +++ b/src/msbuild/src/Directory.Build.targets @@ -43,6 +43,8 @@ $(DevDivPackagesDir) + $(DefineConstants);XUNIT_V3_4_OR_LATER + $(MSBuildThisFileDirectory)Shared\UnitTests\xunit.runner.json $(XUnitDesktopSettingsFile) diff --git a/src/msbuild/src/MSBuild/MSBuildClientApp.cs b/src/msbuild/src/MSBuild/MSBuildClientApp.cs index 33100583fe21..1282d4fb9e3f 100644 --- a/src/msbuild/src/MSBuild/MSBuildClientApp.cs +++ b/src/msbuild/src/MSBuild/MSBuildClientApp.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Globalization; using System.Threading; using Microsoft.Build.Experimental; using Microsoft.Build.Framework.Telemetry; @@ -70,7 +71,18 @@ public static MSBuildApp.ExitType Execute(string[] commandLineArgs, string msbui KnownTelemetry.PartialBuildTelemetry.ServerFallbackReason = exitResult.MSBuildClientExitType.ToString(); } - // Server is busy, fallback to old behavior. + // Surface a single user-visible message on stderr when the failure is something + // other than the well-understood "another client is racing us for the launch + // mutex" case. Without this the user sees no indication that MSBuild Server was + // requested but unavailable; previously a connection timeout would even crash + // the process (the DOTNET_CLI_USE_MSBUILD_SERVER=true regression in 10.0.300). + if (exitResult.MSBuildClientExitType != MSBuildClientExitType.ServerBusy) + { + string detail = GetServerFallbackDetail(exitResult); + Console.Error.WriteLine(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("MSBuildServerUnavailable", detail)); + } + + // Server is busy / unavailable, fallback to old behavior. return MSBuildApp.Execute(commandLineArgs); } @@ -84,5 +96,30 @@ public static MSBuildApp.ExitType Execute(string[] commandLineArgs, string msbui return MSBuildApp.ExitType.MSBuildClientFailure; } + + /// + /// Picks the most specific localized "why MSBuild server was unavailable" sub-message for + /// the user-visible fallback notice. Prefers the "server crashed immediately on launch" + /// detail over a generic connect-failure message when the launched server's exit code is + /// known. + /// + private static string GetServerFallbackDetail(MSBuildClientExitResult exitResult) + { + return exitResult.MSBuildClientExitType switch + { + MSBuildClientExitType.LaunchError => + ResourceUtilities.FormatResourceStringStripCodeAndKeyword("MSBuildServerLaunchError"), + MSBuildClientExitType.UnknownServerState => + ResourceUtilities.FormatResourceStringStripCodeAndKeyword("MSBuildServerStateUnknown"), + MSBuildClientExitType.UnableToConnect when exitResult.ServerProcessExitCode is int code => + ResourceUtilities.FormatResourceStringStripCodeAndKeyword( + "MSBuildServerCrashedOnLaunch", + code.ToString(CultureInfo.InvariantCulture)), + // Default: UnableToConnect without a known exit code, or any future MSBuildClientExitType + // value the caller forwards here. Wording is deliberately neutral about whether the + // underlying failure was a timeout or a non-timeout connect error. + _ => ResourceUtilities.FormatResourceStringStripCodeAndKeyword("MSBuildServerConnectFailed"), + }; + } } } diff --git a/src/msbuild/src/MSBuild/Resources/Strings.resx b/src/msbuild/src/MSBuild/Resources/Strings.resx index b2e5738fac1c..1eae13c5f5b9 100644 --- a/src/msbuild/src/MSBuild/Resources/Strings.resx +++ b/src/msbuild/src/MSBuild/Resources/Strings.resx @@ -1736,6 +1736,29 @@ + + + MSBuild server unavailable: {0}. Falling back to an in-process build. + LOCALIZATION: {0} is a sub-message describing why the MSBuild server could not be used. It is one of MSBuildServerCrashedOnLaunch / MSBuildServerLaunchError / MSBuildServerStateUnknown / MSBuildServerConnectFailed. The outer template supplies the trailing period after {0}; sub-messages must NOT end with a period or other sentence-final punctuation. + + + the server process exited with code {0} immediately after launch + LOCALIZATION: {0} is the integer process exit code; rendered with InvariantCulture as a decimal integer, do not localize the digits. This message is concatenated mid-sentence after a colon in MSBuildServerUnavailable: lower-case start in English is intentional and the message must NOT end with a period (the outer template adds it). + + + the server process could not be launched + LOCALIZATION: No format arguments. Concatenated mid-sentence after a colon in MSBuildServerUnavailable: lower-case start in English is intentional and the message must NOT end with a period (the outer template adds it). + + + the current server state could not be determined + LOCALIZATION: No format arguments. Concatenated mid-sentence after a colon in MSBuildServerUnavailable: lower-case start in English is intentional and the message must NOT end with a period (the outer template adds it). + + + could not connect to the server within the timeout window; the server may have failed to start + LOCALIZATION: No format arguments. Used for any connect failure when the server's exit code is not known — covers both pipe-connect timeouts and non-timeout I/O errors during the connect attempt. Concatenated mid-sentence after a colon in MSBuildServerUnavailable: lower-case start in English is intentional and the message must NOT end with a period (the outer template adds it). + + +