From 0d3ef03cc928c0b9bb12cb4ad9801ecfad16b2d1 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Thu, 29 Jan 2026 18:50:34 +0100 Subject: [PATCH 01/34] Stage 1: Packet infrastructure + IsRunningMultipleNodes callback Implements callback infrastructure for TaskHost IBuildEngine callbacks: - Add packet type enum values for TaskHost callbacks (0x20-0x27 range) - Create ITaskHostCallbackPacket interface for request/response correlation - Add TaskHostQueryRequest/Response packets for property queries - Implement IsRunningMultipleNodes forwarding in OutOfProcTaskHostNode - Handle query requests in TaskHostTask (parent side) - Add integration tests for IsRunningMultipleNodes callback This enables tasks running in TaskHost to correctly query whether the build is running with multiple nodes, which is needed for multithreaded mode (-mt) support. Partial fix for #12863 --- .../BackEnd/IsRunningMultipleNodesTask.cs | 33 +++++ .../BackEnd/TaskHostFactory_Tests.cs | 53 +++---- .../BackEnd/TaskHostQueryPacket_Tests.cs | 102 ++++++++++++++ .../Instance/TaskFactories/TaskHostTask.cs | 20 +++ src/Build/Microsoft.Build.csproj | 3 + src/MSBuild/MSBuild.csproj | 3 + src/MSBuild/OutOfProcTaskHostNode.cs | 133 +++++++++++++++++- src/MSBuild/Resources/Strings.resx | 4 + src/MSBuild/Resources/xlf/Strings.cs.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.de.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.es.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.fr.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.it.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.ja.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.ko.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.pl.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.ru.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.tr.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 6 +- src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 6 +- .../TaskHostCallback/TaskHostCallback.csproj | 15 ++ .../TestIsRunningMultipleNodes.proj | 24 ++++ .../TestIsRunningMultipleNodesTask.cs | 34 +++++ src/Shared/INodePacket.cs | 55 +++++++- src/Shared/ITaskHostCallbackPacket.cs | 28 ++++ src/Shared/TaskHostQueryRequest.cs | 57 ++++++++ src/Shared/TaskHostQueryResponse.cs | 53 +++++++ 28 files changed, 650 insertions(+), 45 deletions(-) create mode 100644 src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs create mode 100644 src/Build.UnitTests/BackEnd/TaskHostQueryPacket_Tests.cs create mode 100644 src/Samples/TaskHostCallback/TaskHostCallback.csproj create mode 100644 src/Samples/TaskHostCallback/TestIsRunningMultipleNodes.proj create mode 100644 src/Samples/TaskHostCallback/TestIsRunningMultipleNodesTask.cs create mode 100644 src/Shared/ITaskHostCallbackPacket.cs create mode 100644 src/Shared/TaskHostQueryRequest.cs create mode 100644 src/Shared/TaskHostQueryResponse.cs diff --git a/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs b/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs new file mode 100644 index 00000000000..62b999c537f --- /dev/null +++ b/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +#nullable disable + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// A simple task that queries IsRunningMultipleNodes from the build engine. + /// Used to test that IBuildEngine2 callbacks work correctly in the task host. + /// + public class IsRunningMultipleNodesTask : Task + { + [Output] + public bool IsRunningMultipleNodes { get; set; } + + public override bool Execute() + { + if (BuildEngine is IBuildEngine2 engine2) + { + IsRunningMultipleNodes = engine2.IsRunningMultipleNodes; + Log.LogMessage(MessageImportance.High, $"IsRunningMultipleNodes = {IsRunningMultipleNodes}"); + return true; + } + + Log.LogError("BuildEngine does not implement IBuildEngine2"); + return false; + } + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs index 22178bbe618..27e50d91b49 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs @@ -365,43 +365,46 @@ public void VariousParameterTypesCanBeTransmittedToAndReceivedFromTaskHost() } /// - /// Verifies that a task returning a string[] with null elements does not crash - /// when executed via TaskHostFactory. This is a regression test for - /// https://github.com/dotnet/msbuild/issues/13174 + /// Verifies that IBuildEngine2.IsRunningMultipleNodes can be queried from a task running in the task host. + /// This tests the callback infrastructure that sends queries back to the parent process. /// - [Fact] - public void StringArrayWithNullsDoesNotCrashTaskHost() + [Theory] + [InlineData(1, false)] // Single node build - should return false + [InlineData(4, true)] // Multi-node build - should return true + public void IsRunningMultipleNodesCallbackWorksInTaskHost(int maxNodeCount, bool expectedResult) { - using TestEnvironment env = TestEnvironment.Create(); + using TestEnvironment env = TestEnvironment.Create(_output); string projectContents = $@" - - - <{typeof(StringArrayWithNullsTask).Name}> - - - + + + <{nameof(IsRunningMultipleNodesTask)}> + + "; - TransientTestFile project = env.CreateFile("testProject.csproj", projectContents); - ProjectInstance projectInstance = new(project.Path); + TransientTestProjectWithFiles project = env.CreateTestProjectWithFiles(projectContents); + + BuildParameters buildParameters = new() + { + MaxNodeCount = maxNodeCount, + EnableNodeReuse = false + }; + + ProjectInstance projectInstance = new(project.ProjectFile); + BuildManager buildManager = BuildManager.DefaultBuildManager; - BuildResult buildResult = buildManager.Build(new BuildParameters(), new BuildRequestData(projectInstance, targetsToBuild: new[] { "TestTarget" })); + BuildResult buildResult = buildManager.Build( + buildParameters, + new BuildRequestData(projectInstance, targetsToBuild: ["TestIsRunningMultipleNodes"])); - // The build should succeed - nulls should be filtered, not cause a crash buildResult.OverallResult.ShouldBe(BuildResultCode.Success); - // Verify task ran out-of-process (TaskHostFactory should force this) - string taskPidStr = projectInstance.GetPropertyValue("TaskPid"); - taskPidStr.ShouldNotBeNullOrEmpty(); - int.TryParse(taskPidStr, out int taskPid).ShouldBeTrue(); - Process.GetCurrentProcess().Id.ShouldNotBe(taskPid, "Task should have run in a separate TaskHost process"); - - // Verify output items - nulls should be filtered out, leaving 3 items - var outputItems = projectInstance.GetItems("OutputItems"); - outputItems.Count.ShouldBe(3, "Null elements should be filtered from the string array"); + string result = projectInstance.GetPropertyValue("IsRunningMultipleNodes"); + result.ShouldNotBeNullOrEmpty(); + bool.Parse(result).ShouldBe(expectedResult); } } } diff --git a/src/Build.UnitTests/BackEnd/TaskHostQueryPacket_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostQueryPacket_Tests.cs new file mode 100644 index 00000000000..cf3a0d12f38 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskHostQueryPacket_Tests.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.BackEnd; +using Shouldly; +using Xunit; + +#nullable disable + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Unit tests for TaskHostQueryRequest and TaskHostQueryResponse packets. + /// + public class TaskHostQueryPacket_Tests + { + [Fact] + public void TaskHostQueryRequest_RoundTrip_Serialization() + { + var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + request.RequestId = 42; + + ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); + request.Translate(writeTranslator); + + ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); + var deserialized = (TaskHostQueryRequest)TaskHostQueryRequest.FactoryForDeserialization(readTranslator); + + deserialized.Query.ShouldBe(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + deserialized.RequestId.ShouldBe(42); + deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryRequest); + } + + [Fact] + public void TaskHostQueryRequest_DefaultRequestId_IsZero() + { + var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + request.RequestId.ShouldBe(0); + } + + [Fact] + public void TaskHostQueryResponse_RoundTrip_Serialization_True() + { + var response = new TaskHostQueryResponse(42, true); + + ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); + response.Translate(writeTranslator); + + ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); + var deserialized = (TaskHostQueryResponse)TaskHostQueryResponse.FactoryForDeserialization(readTranslator); + + deserialized.RequestId.ShouldBe(42); + deserialized.BoolResult.ShouldBeTrue(); + deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryResponse); + } + + [Fact] + public void TaskHostQueryResponse_RoundTrip_Serialization_False() + { + var response = new TaskHostQueryResponse(123, false); + + ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); + response.Translate(writeTranslator); + + ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); + var deserialized = (TaskHostQueryResponse)TaskHostQueryResponse.FactoryForDeserialization(readTranslator); + + deserialized.RequestId.ShouldBe(123); + deserialized.BoolResult.ShouldBeFalse(); + } + + [Fact] + public void TaskHostQueryRequest_ImplementsITaskHostCallbackPacket() + { + var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + request.ShouldBeAssignableTo(); + } + + [Fact] + public void TaskHostQueryResponse_ImplementsITaskHostCallbackPacket() + { + var response = new TaskHostQueryResponse(1, true); + response.ShouldBeAssignableTo(); + } + + [Fact] + public void TaskHostQueryRequest_RequestIdCanBeSet() + { + var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + request.RequestId = 999; + request.RequestId.ShouldBe(999); + } + + [Fact] + public void TaskHostQueryResponse_RequestIdCanBeSet() + { + var response = new TaskHostQueryResponse(1, true); + response.RequestId = 888; + response.RequestId.ShouldBe(888); + } + } +} diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index d677e3a2412..09e45118808 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -203,6 +203,7 @@ public TaskHostTask( (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.LogMessage, LogMessagePacket.FactoryForDeserialization, this); (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskHostTaskComplete, TaskHostTaskComplete.FactoryForDeserialization, this); (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskHostQueryRequest, TaskHostQueryRequest.FactoryForDeserialization, this); _packetReceivedEvent = new AutoResetEvent(false); _receivedPackets = new ConcurrentQueue(); @@ -509,6 +510,9 @@ private void HandlePacket(INodePacket packet, out bool taskFinished) case NodePacketType.LogMessage: HandleLoggedMessage(packet as LogMessagePacket); break; + case NodePacketType.TaskHostQueryRequest: + HandleQueryRequest(packet as TaskHostQueryRequest); + break; default: ErrorUtilities.ThrowInternalErrorUnreachable(); break; @@ -648,6 +652,22 @@ private void HandleLoggedMessage(LogMessagePacket logMessagePacket) } } + /// + /// Handle query requests from the TaskHost for simple build engine state. + /// + private void HandleQueryRequest(TaskHostQueryRequest request) + { + bool result = request.Query switch + { + TaskHostQueryRequest.QueryType.IsRunningMultipleNodes + => _buildEngine is IBuildEngine2 engine2 && engine2.IsRunningMultipleNodes, + _ => false // Unknown query type - return safe default + }; + + var response = new TaskHostQueryResponse(request.RequestId, result); + _taskHostProvider.SendData(_taskHostNodeKey, response); + } + /// /// Since we log that we weren't able to connect to the task host in a couple of different places, /// extract it out into a separate method. diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 157bc6fe2bb..688f504fc55 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -122,6 +122,9 @@ + + + diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 90a5bf4bb59..25f2f0a6ba7 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -117,6 +117,9 @@ + + + diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index aa7afd2efa4..e18a59eeae2 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -3,13 +3,22 @@ using System; using System.Collections; +#if !CLR2COMPATIBILITY +using System.Collections.Concurrent; +#endif using System.Collections.Generic; using System.Globalization; using System.IO; using System.Reflection; using System.Threading; +#if !CLR2COMPATIBILITY +using System.Threading.Tasks; +#endif using Microsoft.Build.BackEnd; using Microsoft.Build.Execution; +#if !CLR2COMPATIBILITY +using Microsoft.Build.Exceptions; +#endif using Microsoft.Build.Framework; #if !CLR2COMPATIBILITY using Microsoft.Build.Experimental.FileAccess; @@ -178,6 +187,19 @@ internal class OutOfProcTaskHostNode : private List _fileAccessData = new List(); #endif +#if !CLR2COMPATIBILITY + /// + /// Counter for generating unique request IDs for callback correlation. + /// + private int _nextCallbackRequestId; + + /// + /// Pending callback requests awaiting responses from the parent. + /// Key is the request ID, value is the TaskCompletionSource to signal when response arrives. + /// + private readonly ConcurrentDictionary> _pendingCallbackRequests = new(); +#endif + /// /// Constructor. /// @@ -204,6 +226,10 @@ public OutOfProcTaskHostNode() thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostTaskCancelled, TaskHostTaskCancelled.FactoryForDeserialization, this); thisINodePacketFactory.RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this); +#if !CLR2COMPATIBILITY + thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostQueryResponse, TaskHostQueryResponse.FactoryForDeserialization, this); +#endif + #if !CLR2COMPATIBILITY EngineServices = new EngineServicesImpl(this); #endif @@ -264,15 +290,21 @@ public string ProjectFileOfTaskNode #region IBuildEngine2 Implementation (Properties) /// - /// Stub implementation of IBuildEngine2.IsRunningMultipleNodes. The task host does not support this sort of - /// IBuildEngine callback, so error. + /// Implementation of IBuildEngine2.IsRunningMultipleNodes. + /// Queries the parent process and returns the actual value. /// public bool IsRunningMultipleNodes { get { +#if CLR2COMPATIBILITY LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); return false; +#else + var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + var response = SendCallbackRequestAndWaitForResponse(request); + return response.BoolResult; +#endif } } @@ -725,9 +757,106 @@ private void HandlePacket(INodePacket packet) case NodePacketType.NodeBuildComplete: HandleNodeBuildComplete(packet as NodeBuildComplete); break; + +#if !CLR2COMPATIBILITY + // Callback response packet - route to pending request + case NodePacketType.TaskHostQueryResponse: + HandleCallbackResponse(packet); + break; +#endif + } + } + +#if !CLR2COMPATIBILITY + /// + /// Handles a callback response packet by completing the pending request's TaskCompletionSource. + /// This is called on the main thread and unblocks the task thread waiting for the response. + /// + private void HandleCallbackResponse(INodePacket packet) + { + // Silent no-op if packet doesn't implement ITaskHostCallbackPacket or request ID unknown. + // Unknown ID can occur if request was cancelled/abandoned before response arrived. + if (packet is ITaskHostCallbackPacket callbackPacket + && _pendingCallbackRequests.TryRemove(callbackPacket.RequestId, out TaskCompletionSource tcs)) + { + tcs.TrySetResult(packet); } } + /// + /// Sends a callback request packet to the parent and waits for the corresponding response. + /// This is called from task threads and blocks until the response arrives on the main thread. + /// + /// The expected response packet type. + /// The request packet to send (must implement ITaskHostCallbackPacket). + /// The response packet. + /// If the connection is lost. + /// If the task is cancelled during the callback. + /// + /// This method is infrastructure for callback support. Used by IsRunningMultipleNodes, + /// RequestCores/ReleaseCores, BuildProjectFile, etc. + /// + private TResponse SendCallbackRequestAndWaitForResponse(ITaskHostCallbackPacket request) + where TResponse : class, INodePacket + { + int requestId = Interlocked.Increment(ref _nextCallbackRequestId); + request.RequestId = requestId; + + // Use ManualResetEvent to bridge TaskCompletionSource to WaitHandle + using var responseEvent = new ManualResetEvent(false); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + tcs.Task.ContinueWith(_ => responseEvent.Set(), TaskContinuationOptions.ExecuteSynchronously); + _pendingCallbackRequests[requestId] = tcs; + + try + { + // Send the request packet to the parent + _nodeEndpoint.SendData(request); + + // Wait for either: response arrives, task cancelled, or connection lost + // No timeout - callbacks like BuildProjectFile can legitimately take hours + WaitHandle[] waitHandles = [responseEvent, _taskCancelledEvent]; + + while (true) + { + int signaledIndex = WaitHandle.WaitAny(waitHandles, millisecondsTimeout: 1000); + + if (signaledIndex == 0) + { + // Response received + break; + } + else if (signaledIndex == 1) + { + // Task cancelled + throw new BuildAbortedException(); + } + + // Timeout - check connection status (no WaitHandle available for this) + if (_nodeEndpoint.LinkStatus != LinkStatus.Active) + { + throw new InvalidOperationException( + ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TaskHostCallbackConnectionLost")); + } + } + + INodePacket response = tcs.Task.Result; + + if (response is TResponse typedResponse) + { + return typedResponse; + } + + throw new InvalidOperationException( + $"Unexpected callback response type: expected {typeof(TResponse).Name}, got {response?.GetType().Name ?? "null"}"); + } + finally + { + _pendingCallbackRequests.TryRemove(requestId, out _); + } + } +#endif + /// /// Configure the task host according to the information received in the /// configuration packet diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index b2e5738fac1..273a416cff3 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1743,4 +1743,8 @@ Don't forget to update this comment after using the new code. --> + + MSB5025: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5025: "} + diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index 50ea8d65370..468d84c29cd 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -1,4 +1,4 @@ - + @@ -1856,6 +1856,10 @@ Když se nastaví na MessageUponIsolationViolation (nebo jeho krátký Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index 722c2d1c867..4380427c8b3 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -1,4 +1,4 @@ - + @@ -1844,6 +1844,10 @@ Hinweis: Ausführlichkeit der Dateiprotokollierungen Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index 5bf41aac41e..674f976a664 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -1,4 +1,4 @@ - + @@ -1850,6 +1850,10 @@ Esta marca es experimental y puede que no funcione según lo previsto. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index 90a849c3c37..da54ed46b7b 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -1,4 +1,4 @@ - + @@ -1844,6 +1844,10 @@ Remarque : verbosité des enregistreurs d’événements de fichiers Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 70a8c3f94f0..2ee6b60174c 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -1,4 +1,4 @@ - + @@ -1854,6 +1854,10 @@ Nota: livello di dettaglio dei logger di file Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Il parametro di output dell'attività "{0}" conteneva {1} elementi null che sono stati rimossi. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 063d6db1eab..952fca56fd6 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -1,4 +1,4 @@ - + @@ -1844,6 +1844,10 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index 377fe9430c2..17e8724fcf8 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -1,4 +1,4 @@ - + @@ -1845,6 +1845,10 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 9a830403cde..0e20a7b17fd 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -1,4 +1,4 @@ - + @@ -1853,6 +1853,10 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index 61740aaed8e..aeb5bbe60f1 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -1,4 +1,4 @@ - + @@ -1844,6 +1844,10 @@ arquivo de resposta. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index 3e17fc13b6a..739be9bc638 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -1,4 +1,4 @@ - + @@ -1842,6 +1842,10 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index be9a1a336f4..57a9a6be391 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -1,4 +1,4 @@ - + @@ -1847,6 +1847,10 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 6c10226dc85..86b7cd92995 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -1,4 +1,4 @@ - + @@ -1844,6 +1844,10 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index dd13ca9a66f..2ecb47cfcdd 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -1,4 +1,4 @@ - + @@ -1844,6 +1844,10 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} Terminal Logger was not used because it was detected that the build is running in an automated environment. diff --git a/src/Samples/TaskHostCallback/TaskHostCallback.csproj b/src/Samples/TaskHostCallback/TaskHostCallback.csproj new file mode 100644 index 00000000000..e8989a176ce --- /dev/null +++ b/src/Samples/TaskHostCallback/TaskHostCallback.csproj @@ -0,0 +1,15 @@ + + + + net472 + latest + enable + enable + + + + + + + + diff --git a/src/Samples/TaskHostCallback/TestIsRunningMultipleNodes.proj b/src/Samples/TaskHostCallback/TestIsRunningMultipleNodes.proj new file mode 100644 index 00000000000..3226f41dcc8 --- /dev/null +++ b/src/Samples/TaskHostCallback/TestIsRunningMultipleNodes.proj @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Samples/TaskHostCallback/TestIsRunningMultipleNodesTask.cs b/src/Samples/TaskHostCallback/TestIsRunningMultipleNodesTask.cs new file mode 100644 index 00000000000..00ccf830a04 --- /dev/null +++ b/src/Samples/TaskHostCallback/TestIsRunningMultipleNodesTask.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace TaskHostCallback; + +/// +/// A simple task that tests the IsRunningMultipleNodes callback from TaskHost. +/// This task uses the CLR4 runtime and x86 architecture to force it to run in a TaskHost process. +/// +public class TestIsRunningMultipleNodesTask : Microsoft.Build.Utilities.Task +{ + [Output] + public bool IsRunningMultipleNodes { get; set; } + + public override bool Execute() + { + // Access IBuildEngine2.IsRunningMultipleNodes - this should work in TaskHost + // with our callback implementation + if (BuildEngine is IBuildEngine2 buildEngine2) + { + IsRunningMultipleNodes = buildEngine2.IsRunningMultipleNodes; + Log.LogMessage(MessageImportance.High, $"IsRunningMultipleNodes = {IsRunningMultipleNodes}"); + return true; + } + else + { + Log.LogError("BuildEngine does not implement IBuildEngine2"); + return false; + } + } +} diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs index 690420ba214..a89aa975ea8 100644 --- a/src/Shared/INodePacket.cs +++ b/src/Shared/INodePacket.cs @@ -223,15 +223,60 @@ internal enum NodePacketType : byte /// RarNodeExecuteResponse, // 0x15 - // Reserve space for future core packet types (0x16-0x3B available for expansion) + /// + /// A batch of log events emitted while the RAR task is executing. + /// + RarNodeBufferedLogEvents, // 0x16 - // Server command packets placed at end of safe range to maintain separation from core packets - #region ServerNode enums + // Packet types 0x17-0x1F reserved for future core functionality + + #region TaskHost callback packets (0x20-0x27) + // These support bidirectional callbacks from TaskHost to parent for IBuildEngine implementations /// - /// A batch of log events emitted while the RAR task is executing. + /// Request from TaskHost to parent to execute BuildProjectFile* callbacks. + /// + TaskHostBuildRequest = 0x20, + + /// + /// Response from parent to TaskHost with BuildProjectFile* results. + /// + TaskHostBuildResponse = 0x21, + + /// + /// Request from TaskHost to parent for RequestCores/ReleaseCores. /// - RarNodeBufferedLogEvents, + TaskHostResourceRequest = 0x22, + + /// + /// Response from parent to TaskHost with resource allocation result. + /// + TaskHostResourceResponse = 0x23, + + /// + /// Request from TaskHost to parent for simple queries (e.g., IsRunningMultipleNodes). + /// + TaskHostQueryRequest = 0x24, + + /// + /// Response from parent to TaskHost with query result. + /// + TaskHostQueryResponse = 0x25, + + /// + /// Request from TaskHost to parent for Yield/Reacquire operations. + /// + TaskHostYieldRequest = 0x26, + + /// + /// Response from parent to TaskHost acknowledging yield/reacquire. + /// + TaskHostYieldResponse = 0x27, + + #endregion + + // Server command packets placed at end of safe range to maintain separation from core packets + #region ServerNode enums /// /// Command in form of MSBuild command line for server node - MSBuild Server. diff --git a/src/Shared/ITaskHostCallbackPacket.cs b/src/Shared/ITaskHostCallbackPacket.cs new file mode 100644 index 00000000000..ab8df6ba83d --- /dev/null +++ b/src/Shared/ITaskHostCallbackPacket.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !CLR2COMPATIBILITY + +namespace Microsoft.Build.BackEnd +{ + /// + /// Interface for TaskHost callback packets that require request/response correlation. + /// Packets implementing this interface can be matched between requests sent from TaskHost + /// and responses received from the parent process. + /// + /// + /// This interface is only used in non-CLR2 environments. The MSBuildTaskHost (CLR2) does not + /// support these callbacks. + /// + internal interface ITaskHostCallbackPacket : INodePacket + { + /// + /// Gets or sets the unique request ID for correlating requests with responses. + /// This ID is assigned by the TaskHost when sending a request and echoed back + /// by the parent in the corresponding response. + /// + int RequestId { get; set; } + } +} + +#endif diff --git a/src/Shared/TaskHostQueryRequest.cs b/src/Shared/TaskHostQueryRequest.cs new file mode 100644 index 00000000000..6cfd1a38fa2 --- /dev/null +++ b/src/Shared/TaskHostQueryRequest.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !CLR2COMPATIBILITY + +#nullable disable + +namespace Microsoft.Build.BackEnd +{ + /// + /// Packet sent from TaskHost to parent to query simple build engine state. + /// + internal class TaskHostQueryRequest : INodePacket, ITaskHostCallbackPacket + { + private QueryType _queryType; + private int _requestId; + + public TaskHostQueryRequest() + { + } + + public TaskHostQueryRequest(QueryType queryType) + { + _queryType = queryType; + } + + public NodePacketType Type => NodePacketType.TaskHostQueryRequest; + + public int RequestId + { + get => _requestId; + set => _requestId = value; + } + + public QueryType Query => _queryType; + + public void Translate(ITranslator translator) + { + translator.TranslateEnum(ref _queryType, (int)_queryType); + translator.Translate(ref _requestId); + } + + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + var packet = new TaskHostQueryRequest(); + packet.Translate(translator); + return packet; + } + + internal enum QueryType + { + IsRunningMultipleNodes = 0, + } + } +} + +#endif diff --git a/src/Shared/TaskHostQueryResponse.cs b/src/Shared/TaskHostQueryResponse.cs new file mode 100644 index 00000000000..d38cee367e9 --- /dev/null +++ b/src/Shared/TaskHostQueryResponse.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#if !CLR2COMPATIBILITY + +#nullable disable + +namespace Microsoft.Build.BackEnd +{ + /// + /// Response packet from parent to TaskHost for query requests. + /// + internal class TaskHostQueryResponse : INodePacket, ITaskHostCallbackPacket + { + private int _requestId; + private bool _boolResult; + + public TaskHostQueryResponse() + { + } + + public TaskHostQueryResponse(int requestId, bool boolResult) + { + _requestId = requestId; + _boolResult = boolResult; + } + + public NodePacketType Type => NodePacketType.TaskHostQueryResponse; + + public int RequestId + { + get => _requestId; + set => _requestId = value; + } + + public bool BoolResult => _boolResult; + + public void Translate(ITranslator translator) + { + translator.Translate(ref _requestId); + translator.Translate(ref _boolResult); + } + + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + var packet = new TaskHostQueryResponse(); + packet.Translate(translator); + return packet; + } + } +} + +#endif From 7e6177007e923e465c228fb1b2af70702f7568a5 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Fri, 30 Jan 2026 13:18:19 +0100 Subject: [PATCH 02/34] Fix MSB5025 error code collision - use MSB5027 instead MSB5025 was already used in Strings.shared.resx for solution filter JSON errors. Changed TaskHostCallbackConnectionLost to use unused MSB5027. --- src/MSBuild/Resources/Strings.resx | 4 ++-- src/MSBuild/Resources/xlf/Strings.cs.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.de.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.es.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.fr.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.it.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.ja.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.ko.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.pl.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.ru.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.tr.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 2 +- src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 2 +- 15 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index 273a416cff3..e0ad46f9c58 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1744,7 +1744,7 @@ Don't forget to update this comment after using the new code. --> - MSB5025: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5025: "} + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. + {StrBegin="MSB5027: "} diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index 468d84c29cd..10a783462fc 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -2298,4 +2298,4 @@ Když se nastaví na MessageUponIsolationViolation (nebo jeho krátký - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index 4380427c8b3..c871ea18d27 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -2286,4 +2286,4 @@ Hinweis: Ausführlichkeit der Dateiprotokollierungen - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index 674f976a664..b22794a1194 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -2290,4 +2290,4 @@ Esta marca es experimental y puede que no funcione según lo previsto. - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index da54ed46b7b..dd74d0bc081 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -2287,4 +2287,4 @@ Remarque : verbosité des enregistreurs d’événements de fichiers - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 2ee6b60174c..84726eb8b4c 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -2302,4 +2302,4 @@ Esegue la profilatura della valutazione di MSBuild e scrive - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 952fca56fd6..16e8fec66c2 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -2286,4 +2286,4 @@ - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index 17e8724fcf8..e0602e1a5f2 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -2287,4 +2287,4 @@ - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 0e20a7b17fd..36b48cc4204 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -2295,4 +2295,4 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index aeb5bbe60f1..61e346f8f78 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -2286,4 +2286,4 @@ arquivo de resposta. - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index 739be9bc638..5578fd80a25 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -2286,4 +2286,4 @@ - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index 57a9a6be391..b9b613c0f89 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -2289,4 +2289,4 @@ - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.xlf b/src/MSBuild/Resources/xlf/Strings.xlf index a16ca1ca45e..c656310ece9 100644 --- a/src/MSBuild/Resources/xlf/Strings.xlf +++ b/src/MSBuild/Resources/xlf/Strings.xlf @@ -1,4 +1,4 @@ - + @@ -1056,4 +1056,4 @@ - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 86b7cd92995..9522669bd6e 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -2289,4 +2289,4 @@ - \ No newline at end of file + diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index 2ecb47cfcdd..cce2b165bb6 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -2286,4 +2286,4 @@ - \ No newline at end of file + From a90b7fede1c5c82fabeb287c155aed49f51cc93d Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Fri, 30 Jan 2026 14:22:41 +0100 Subject: [PATCH 03/34] Add integration test for IsRunningMultipleNodes in MT mode Tests that IBuildEngine2.IsRunningMultipleNodes callback works correctly when an unmarked task is automatically ejected to TaskHost in multithreaded mode. This is the key end-to-end test for Stage 1 callback infrastructure. --- .../BackEnd/TaskRouter_IntegrationTests.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs b/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs index fa508369b3d..e830c47fa2f 100644 --- a/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs +++ b/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs @@ -14,6 +14,7 @@ using Shouldly; using Xunit; using Xunit.Abstractions; +using Microsoft.Build.UnitTests.BackEnd; #nullable disable @@ -382,6 +383,62 @@ public void ExplicitTaskHostFactory_OverridesRoutingLogic() logger.FullLog.ShouldContain("TaskWithAttribute executed"); } + /// + /// Verifies that IBuildEngine callbacks work correctly when an unmarked task + /// is automatically ejected to TaskHost in multithreaded mode. + /// This is the key integration test for Stage 1 callback infrastructure. + /// + [Theory] + [InlineData(1, false)] // Single node build - IsRunningMultipleNodes should be false + [InlineData(4, true)] // Multi-node build - IsRunningMultipleNodes should be true + public void IsRunningMultipleNodesCallback_WorksWhenTaskEjectedToTaskHost_InMultiThreadedMode(int maxNodeCount, bool expectedResult) + { + // Arrange - Use IsRunningMultipleNodesTask which is NOT marked with MSBuildMultiThreadableTask + // In MT mode, this task will be automatically ejected to TaskHost + // This tests that IBuildEngine2.IsRunningMultipleNodes callback works end-to-end + string projectContent = $@" + + + + <{nameof(IsRunningMultipleNodesTask)}> + + + +"; + + string projectFile = Path.Combine(_testProjectsDir, $"IsRunningMultipleNodes_MT_{maxNodeCount}.proj"); + File.WriteAllText(projectFile, projectContent); + + var logger = new MockLogger(_output); + var buildParameters = new BuildParameters + { + MultiThreaded = true, // Enable multithreaded mode - causes unmarked tasks to use TaskHost + MaxNodeCount = maxNodeCount, + Loggers = new[] { logger }, + DisableInProcNode = false, + EnableNodeReuse = false + }; + + var buildRequestData = new BuildRequestData( + projectFile, + new Dictionary(), + null, + new[] { "TestTarget" }, + null); + + // Act + var buildManager = BuildManager.DefaultBuildManager; + var result = buildManager.Build(buildParameters, buildRequestData); + + // Assert + result.OverallResult.ShouldBe(BuildResultCode.Success); + + // Verify task was ejected to TaskHost (because it lacks MSBuildMultiThreadableTask attribute) + TaskRouterTestHelper.AssertTaskUsedTaskHost(logger, nameof(IsRunningMultipleNodesTask)); + + // Verify the callback returned the correct value + logger.FullLog.ShouldContain($"IsRunningMultipleNodes = {expectedResult}"); + } private string CreateTestProject(string taskName, string taskClass) From bcf91d385786b6387f693c7c493e97d68b1a64b5 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Fri, 30 Jan 2026 14:27:17 +0100 Subject: [PATCH 04/34] Consolidate TaskHost callback tests into TaskHostCallback_Tests.cs - Created TaskHostCallback_Tests.cs with: - Packet serialization tests (minimized from 8 to 3) - IsRunningMultipleNodes integration tests (both explicit TaskHostFactory and MT auto-eject) - Removed TaskHostQueryPacket_Tests.cs (redundant property tests) - Removed duplicate tests from TaskHostFactory_Tests.cs and TaskRouter_IntegrationTests.cs --- .../BackEnd/TaskHostCallback_Tests.cs | 149 ++++++++++++++++++ .../BackEnd/TaskHostFactory_Tests.cs | 43 ----- .../BackEnd/TaskHostQueryPacket_Tests.cs | 102 ------------ .../BackEnd/TaskRouter_IntegrationTests.cs | 58 ------- 4 files changed, 149 insertions(+), 203 deletions(-) create mode 100644 src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs delete mode 100644 src/Build.UnitTests/BackEnd/TaskHostQueryPacket_Tests.cs diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs new file mode 100644 index 00000000000..451fe9bb412 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using Microsoft.Build.BackEnd; +using Microsoft.Build.Execution; +using Microsoft.Build.UnitTests.Shared; +using Shouldly; +using Xunit; +using Xunit.Abstractions; + +#nullable disable + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Tests for IBuildEngine callback support in TaskHost. + /// Covers packet serialization and end-to-end integration tests. + /// + public class TaskHostCallback_Tests + { + private readonly ITestOutputHelper _output; + + public TaskHostCallback_Tests(ITestOutputHelper output) + { + _output = output; + } + + #region Packet Serialization Tests + + [Fact] + public void TaskHostQueryRequest_RoundTrip_Serialization() + { + var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + request.RequestId = 42; + + ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); + request.Translate(writeTranslator); + + ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); + var deserialized = (TaskHostQueryRequest)TaskHostQueryRequest.FactoryForDeserialization(readTranslator); + + deserialized.Query.ShouldBe(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + deserialized.RequestId.ShouldBe(42); + deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryRequest); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TaskHostQueryResponse_RoundTrip_Serialization(bool boolResult) + { + var response = new TaskHostQueryResponse(123, boolResult); + + ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); + response.Translate(writeTranslator); + + ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); + var deserialized = (TaskHostQueryResponse)TaskHostQueryResponse.FactoryForDeserialization(readTranslator); + + deserialized.RequestId.ShouldBe(123); + deserialized.BoolResult.ShouldBe(boolResult); + deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryResponse); + } + + #endregion + + #region IsRunningMultipleNodes Callback Tests + + /// + /// Verifies IsRunningMultipleNodes callback works when task is explicitly run in TaskHost via TaskHostFactory. + /// + [Theory] + [InlineData(1, false)] // Single node build + [InlineData(4, true)] // Multi-node build + public void IsRunningMultipleNodes_WorksWithExplicitTaskHostFactory(int maxNodeCount, bool expectedResult) + { + using TestEnvironment env = TestEnvironment.Create(_output); + + string projectContents = $@" + + + + <{nameof(IsRunningMultipleNodesTask)}> + + + +"; + + TransientTestProjectWithFiles project = env.CreateTestProjectWithFiles(projectContents); + ProjectInstance projectInstance = new(project.ProjectFile); + + BuildResult buildResult = BuildManager.DefaultBuildManager.Build( + new BuildParameters { MaxNodeCount = maxNodeCount, EnableNodeReuse = false }, + new BuildRequestData(projectInstance, targetsToBuild: ["Test"])); + + buildResult.OverallResult.ShouldBe(BuildResultCode.Success); + bool.Parse(projectInstance.GetPropertyValue("Result")).ShouldBe(expectedResult); + } + + /// + /// Verifies IsRunningMultipleNodes callback works when unmarked task is auto-ejected to TaskHost in MT mode. + /// + [Theory] + [InlineData(1, false)] + [InlineData(4, true)] + public void IsRunningMultipleNodes_WorksWhenAutoEjectedInMultiThreadedMode(int maxNodeCount, bool expectedResult) + { + using TestEnvironment env = TestEnvironment.Create(_output); + string testDir = env.CreateFolder().Path; + + // IsRunningMultipleNodesTask lacks MSBuildMultiThreadableTask attribute, so it's auto-ejected to TaskHost in MT mode + string projectContents = $@" + + + + <{nameof(IsRunningMultipleNodesTask)}> + + + +"; + + string projectFile = Path.Combine(testDir, "Test.proj"); + File.WriteAllText(projectFile, projectContents); + + var logger = new MockLogger(_output); + BuildResult buildResult = BuildManager.DefaultBuildManager.Build( + new BuildParameters + { + MultiThreaded = true, + MaxNodeCount = maxNodeCount, + Loggers = [logger], + EnableNodeReuse = false + }, + new BuildRequestData(projectFile, new Dictionary(), null, ["Test"], null)); + + buildResult.OverallResult.ShouldBe(BuildResultCode.Success); + + // Verify task was ejected to TaskHost + logger.FullLog.ShouldContain("external task host"); + + // Verify callback returned correct value + logger.FullLog.ShouldContain($"IsRunningMultipleNodes = {expectedResult}"); + } + + #endregion + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs index 27e50d91b49..9b80c023c94 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs @@ -363,48 +363,5 @@ public void VariousParameterTypesCanBeTransmittedToAndReceivedFromTaskHost() projectInstance.GetPropertyValue("CustomStructOutput").ShouldBe(TaskBuilderTestTask.s_customStruct.ToString(CultureInfo.InvariantCulture)); projectInstance.GetPropertyValue("EnumOutput").ShouldBe(TargetBuiltReason.BeforeTargets.ToString()); } - - /// - /// Verifies that IBuildEngine2.IsRunningMultipleNodes can be queried from a task running in the task host. - /// This tests the callback infrastructure that sends queries back to the parent process. - /// - [Theory] - [InlineData(1, false)] // Single node build - should return false - [InlineData(4, true)] // Multi-node build - should return true - public void IsRunningMultipleNodesCallbackWorksInTaskHost(int maxNodeCount, bool expectedResult) - { - using TestEnvironment env = TestEnvironment.Create(_output); - - string projectContents = $@" - - - - <{nameof(IsRunningMultipleNodesTask)}> - - - -"; - - TransientTestProjectWithFiles project = env.CreateTestProjectWithFiles(projectContents); - - BuildParameters buildParameters = new() - { - MaxNodeCount = maxNodeCount, - EnableNodeReuse = false - }; - - ProjectInstance projectInstance = new(project.ProjectFile); - - BuildManager buildManager = BuildManager.DefaultBuildManager; - BuildResult buildResult = buildManager.Build( - buildParameters, - new BuildRequestData(projectInstance, targetsToBuild: ["TestIsRunningMultipleNodes"])); - - buildResult.OverallResult.ShouldBe(BuildResultCode.Success); - - string result = projectInstance.GetPropertyValue("IsRunningMultipleNodes"); - result.ShouldNotBeNullOrEmpty(); - bool.Parse(result).ShouldBe(expectedResult); - } } } diff --git a/src/Build.UnitTests/BackEnd/TaskHostQueryPacket_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostQueryPacket_Tests.cs deleted file mode 100644 index cf3a0d12f38..00000000000 --- a/src/Build.UnitTests/BackEnd/TaskHostQueryPacket_Tests.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Build.BackEnd; -using Shouldly; -using Xunit; - -#nullable disable - -namespace Microsoft.Build.UnitTests.BackEnd -{ - /// - /// Unit tests for TaskHostQueryRequest and TaskHostQueryResponse packets. - /// - public class TaskHostQueryPacket_Tests - { - [Fact] - public void TaskHostQueryRequest_RoundTrip_Serialization() - { - var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); - request.RequestId = 42; - - ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); - request.Translate(writeTranslator); - - ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); - var deserialized = (TaskHostQueryRequest)TaskHostQueryRequest.FactoryForDeserialization(readTranslator); - - deserialized.Query.ShouldBe(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); - deserialized.RequestId.ShouldBe(42); - deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryRequest); - } - - [Fact] - public void TaskHostQueryRequest_DefaultRequestId_IsZero() - { - var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); - request.RequestId.ShouldBe(0); - } - - [Fact] - public void TaskHostQueryResponse_RoundTrip_Serialization_True() - { - var response = new TaskHostQueryResponse(42, true); - - ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); - response.Translate(writeTranslator); - - ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); - var deserialized = (TaskHostQueryResponse)TaskHostQueryResponse.FactoryForDeserialization(readTranslator); - - deserialized.RequestId.ShouldBe(42); - deserialized.BoolResult.ShouldBeTrue(); - deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryResponse); - } - - [Fact] - public void TaskHostQueryResponse_RoundTrip_Serialization_False() - { - var response = new TaskHostQueryResponse(123, false); - - ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); - response.Translate(writeTranslator); - - ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); - var deserialized = (TaskHostQueryResponse)TaskHostQueryResponse.FactoryForDeserialization(readTranslator); - - deserialized.RequestId.ShouldBe(123); - deserialized.BoolResult.ShouldBeFalse(); - } - - [Fact] - public void TaskHostQueryRequest_ImplementsITaskHostCallbackPacket() - { - var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); - request.ShouldBeAssignableTo(); - } - - [Fact] - public void TaskHostQueryResponse_ImplementsITaskHostCallbackPacket() - { - var response = new TaskHostQueryResponse(1, true); - response.ShouldBeAssignableTo(); - } - - [Fact] - public void TaskHostQueryRequest_RequestIdCanBeSet() - { - var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); - request.RequestId = 999; - request.RequestId.ShouldBe(999); - } - - [Fact] - public void TaskHostQueryResponse_RequestIdCanBeSet() - { - var response = new TaskHostQueryResponse(1, true); - response.RequestId = 888; - response.RequestId.ShouldBe(888); - } - } -} diff --git a/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs b/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs index e830c47fa2f..f86aada207c 100644 --- a/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs +++ b/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs @@ -14,7 +14,6 @@ using Shouldly; using Xunit; using Xunit.Abstractions; -using Microsoft.Build.UnitTests.BackEnd; #nullable disable @@ -383,63 +382,6 @@ public void ExplicitTaskHostFactory_OverridesRoutingLogic() logger.FullLog.ShouldContain("TaskWithAttribute executed"); } - /// - /// Verifies that IBuildEngine callbacks work correctly when an unmarked task - /// is automatically ejected to TaskHost in multithreaded mode. - /// This is the key integration test for Stage 1 callback infrastructure. - /// - [Theory] - [InlineData(1, false)] // Single node build - IsRunningMultipleNodes should be false - [InlineData(4, true)] // Multi-node build - IsRunningMultipleNodes should be true - public void IsRunningMultipleNodesCallback_WorksWhenTaskEjectedToTaskHost_InMultiThreadedMode(int maxNodeCount, bool expectedResult) - { - // Arrange - Use IsRunningMultipleNodesTask which is NOT marked with MSBuildMultiThreadableTask - // In MT mode, this task will be automatically ejected to TaskHost - // This tests that IBuildEngine2.IsRunningMultipleNodes callback works end-to-end - string projectContent = $@" - - - - <{nameof(IsRunningMultipleNodesTask)}> - - - -"; - - string projectFile = Path.Combine(_testProjectsDir, $"IsRunningMultipleNodes_MT_{maxNodeCount}.proj"); - File.WriteAllText(projectFile, projectContent); - - var logger = new MockLogger(_output); - var buildParameters = new BuildParameters - { - MultiThreaded = true, // Enable multithreaded mode - causes unmarked tasks to use TaskHost - MaxNodeCount = maxNodeCount, - Loggers = new[] { logger }, - DisableInProcNode = false, - EnableNodeReuse = false - }; - - var buildRequestData = new BuildRequestData( - projectFile, - new Dictionary(), - null, - new[] { "TestTarget" }, - null); - - // Act - var buildManager = BuildManager.DefaultBuildManager; - var result = buildManager.Build(buildParameters, buildRequestData); - - // Assert - result.OverallResult.ShouldBe(BuildResultCode.Success); - - // Verify task was ejected to TaskHost (because it lacks MSBuildMultiThreadableTask attribute) - TaskRouterTestHelper.AssertTaskUsedTaskHost(logger, nameof(IsRunningMultipleNodesTask)); - - // Verify the callback returned the correct value - logger.FullLog.ShouldContain($"IsRunningMultipleNodes = {expectedResult}"); - } - private string CreateTestProject(string taskName, string taskClass) { From 8bf2412147729e22439ffb7a7e13dd3cc6537d8a Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 9 Feb 2026 11:01:54 +0100 Subject: [PATCH 05/34] Make new code files nullable clean Remove #nullable disable from: - TaskHostQueryRequest.cs - TaskHostQueryResponse.cs - IsRunningMultipleNodesTask.cs - TaskHostCallback_Tests.cs --- src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs | 2 -- src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs | 4 +--- src/MSBuild/Resources/xlf/Strings.cs.xlf | 1 + src/MSBuild/Resources/xlf/Strings.de.xlf | 1 + src/MSBuild/Resources/xlf/Strings.es.xlf | 1 + src/MSBuild/Resources/xlf/Strings.fr.xlf | 1 + src/MSBuild/Resources/xlf/Strings.it.xlf | 1 + src/MSBuild/Resources/xlf/Strings.ja.xlf | 1 + src/MSBuild/Resources/xlf/Strings.ko.xlf | 1 + src/MSBuild/Resources/xlf/Strings.pl.xlf | 1 + src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 1 + src/MSBuild/Resources/xlf/Strings.ru.xlf | 1 + src/MSBuild/Resources/xlf/Strings.tr.xlf | 1 + src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 1 + src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 1 + src/Shared/TaskHostQueryRequest.cs | 2 -- src/Shared/TaskHostQueryResponse.cs | 2 -- 17 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs b/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs index 62b999c537f..09d14959f2e 100644 --- a/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs +++ b/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs @@ -4,8 +4,6 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; -#nullable disable - namespace Microsoft.Build.UnitTests.BackEnd { /// diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs index 451fe9bb412..ce8e36102f2 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs @@ -10,8 +10,6 @@ using Xunit; using Xunit.Abstractions; -#nullable disable - namespace Microsoft.Build.UnitTests.BackEnd { /// @@ -133,7 +131,7 @@ public void IsRunningMultipleNodes_WorksWhenAutoEjectedInMultiThreadedMode(int m Loggers = [logger], EnableNodeReuse = false }, - new BuildRequestData(projectFile, new Dictionary(), null, ["Test"], null)); + new BuildRequestData(projectFile, new Dictionary(), null, ["Test"], null)); buildResult.OverallResult.ShouldBe(BuildResultCode.Success); diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index 10a783462fc..81d69076e79 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -1856,6 +1856,7 @@ Když se nastaví na MessageUponIsolationViolation (nebo jeho krátký Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index c871ea18d27..d2beef196a6 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -1844,6 +1844,7 @@ Hinweis: Ausführlichkeit der Dateiprotokollierungen Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index b22794a1194..abfb40d7ebd 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -1850,6 +1850,7 @@ Esta marca es experimental y puede que no funcione según lo previsto. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index dd74d0bc081..5758923e5e6 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -1844,6 +1844,7 @@ Remarque : verbosité des enregistreurs d’événements de fichiers Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 84726eb8b4c..911f7f85045 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -1854,6 +1854,7 @@ Nota: livello di dettaglio dei logger di file Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Il parametro di output dell'attività "{0}" conteneva {1} elementi null che sono stati rimossi. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 16e8fec66c2..e761ab0600c 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -1844,6 +1844,7 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index e0602e1a5f2..bb3238e22e6 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -1845,6 +1845,7 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 36b48cc4204..1903f66dbea 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -1853,6 +1853,7 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index 61e346f8f78..c493ee20327 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -1844,6 +1844,7 @@ arquivo de resposta. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index 5578fd80a25..4f267159bc8 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -1842,6 +1842,7 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index b9b613c0f89..07f49900326 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -1847,6 +1847,7 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 9522669bd6e..9ff29f50dbf 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -1844,6 +1844,7 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index cce2b165bb6..006de177f9b 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -1844,6 +1844,7 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. + MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. diff --git a/src/Shared/TaskHostQueryRequest.cs b/src/Shared/TaskHostQueryRequest.cs index 6cfd1a38fa2..22c1f45a976 100644 --- a/src/Shared/TaskHostQueryRequest.cs +++ b/src/Shared/TaskHostQueryRequest.cs @@ -3,8 +3,6 @@ #if !CLR2COMPATIBILITY -#nullable disable - namespace Microsoft.Build.BackEnd { /// diff --git a/src/Shared/TaskHostQueryResponse.cs b/src/Shared/TaskHostQueryResponse.cs index d38cee367e9..22c8476a616 100644 --- a/src/Shared/TaskHostQueryResponse.cs +++ b/src/Shared/TaskHostQueryResponse.cs @@ -3,8 +3,6 @@ #if !CLR2COMPATIBILITY -#nullable disable - namespace Microsoft.Build.BackEnd { /// From 2a67efba83563789bbff067b87b818834b910d35 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 9 Feb 2026 11:34:10 +0100 Subject: [PATCH 06/34] Remove redundant TaskHostCallback sample The sample duplicates IsRunningMultipleNodesTask which is already in the test project, and proper integration tests exist in TaskHostCallback_Tests.cs. --- .../TaskHostCallback/TaskHostCallback.csproj | 15 -------- .../TestIsRunningMultipleNodes.proj | 24 ------------- .../TestIsRunningMultipleNodesTask.cs | 34 ------------------- 3 files changed, 73 deletions(-) delete mode 100644 src/Samples/TaskHostCallback/TaskHostCallback.csproj delete mode 100644 src/Samples/TaskHostCallback/TestIsRunningMultipleNodes.proj delete mode 100644 src/Samples/TaskHostCallback/TestIsRunningMultipleNodesTask.cs diff --git a/src/Samples/TaskHostCallback/TaskHostCallback.csproj b/src/Samples/TaskHostCallback/TaskHostCallback.csproj deleted file mode 100644 index e8989a176ce..00000000000 --- a/src/Samples/TaskHostCallback/TaskHostCallback.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - net472 - latest - enable - enable - - - - - - - - diff --git a/src/Samples/TaskHostCallback/TestIsRunningMultipleNodes.proj b/src/Samples/TaskHostCallback/TestIsRunningMultipleNodes.proj deleted file mode 100644 index 3226f41dcc8..00000000000 --- a/src/Samples/TaskHostCallback/TestIsRunningMultipleNodes.proj +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/Samples/TaskHostCallback/TestIsRunningMultipleNodesTask.cs b/src/Samples/TaskHostCallback/TestIsRunningMultipleNodesTask.cs deleted file mode 100644 index 00ccf830a04..00000000000 --- a/src/Samples/TaskHostCallback/TestIsRunningMultipleNodesTask.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace TaskHostCallback; - -/// -/// A simple task that tests the IsRunningMultipleNodes callback from TaskHost. -/// This task uses the CLR4 runtime and x86 architecture to force it to run in a TaskHost process. -/// -public class TestIsRunningMultipleNodesTask : Microsoft.Build.Utilities.Task -{ - [Output] - public bool IsRunningMultipleNodes { get; set; } - - public override bool Execute() - { - // Access IBuildEngine2.IsRunningMultipleNodes - this should work in TaskHost - // with our callback implementation - if (BuildEngine is IBuildEngine2 buildEngine2) - { - IsRunningMultipleNodes = buildEngine2.IsRunningMultipleNodes; - Log.LogMessage(MessageImportance.High, $"IsRunningMultipleNodes = {IsRunningMultipleNodes}"); - return true; - } - else - { - Log.LogError("BuildEngine does not implement IBuildEngine2"); - return false; - } - } -} From aabe5b1633300035feb74f8af15c48ff4e9141eb Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 9 Feb 2026 11:44:27 +0100 Subject: [PATCH 07/34] Remove unnecessary #if !CLR2COMPATIBILITY guards These files are not included in MSBuildTaskHost.csproj, so the CLR2 guards are not needed. --- src/Shared/ITaskHostCallbackPacket.cs | 8 -------- src/Shared/TaskHostQueryRequest.cs | 4 ---- src/Shared/TaskHostQueryResponse.cs | 4 ---- 3 files changed, 16 deletions(-) diff --git a/src/Shared/ITaskHostCallbackPacket.cs b/src/Shared/ITaskHostCallbackPacket.cs index ab8df6ba83d..c8ad9cb9bb2 100644 --- a/src/Shared/ITaskHostCallbackPacket.cs +++ b/src/Shared/ITaskHostCallbackPacket.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if !CLR2COMPATIBILITY - namespace Microsoft.Build.BackEnd { /// @@ -10,10 +8,6 @@ namespace Microsoft.Build.BackEnd /// Packets implementing this interface can be matched between requests sent from TaskHost /// and responses received from the parent process. /// - /// - /// This interface is only used in non-CLR2 environments. The MSBuildTaskHost (CLR2) does not - /// support these callbacks. - /// internal interface ITaskHostCallbackPacket : INodePacket { /// @@ -24,5 +18,3 @@ internal interface ITaskHostCallbackPacket : INodePacket int RequestId { get; set; } } } - -#endif diff --git a/src/Shared/TaskHostQueryRequest.cs b/src/Shared/TaskHostQueryRequest.cs index 22c1f45a976..450d84ba113 100644 --- a/src/Shared/TaskHostQueryRequest.cs +++ b/src/Shared/TaskHostQueryRequest.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if !CLR2COMPATIBILITY - namespace Microsoft.Build.BackEnd { /// @@ -51,5 +49,3 @@ internal enum QueryType } } } - -#endif diff --git a/src/Shared/TaskHostQueryResponse.cs b/src/Shared/TaskHostQueryResponse.cs index 22c8476a616..edc91652750 100644 --- a/src/Shared/TaskHostQueryResponse.cs +++ b/src/Shared/TaskHostQueryResponse.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if !CLR2COMPATIBILITY - namespace Microsoft.Build.BackEnd { /// @@ -47,5 +45,3 @@ internal static INodePacket FactoryForDeserialization(ITranslator translator) } } } - -#endif From 7d847c569c4c9d39d13984655ff073aac64ae73b Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 9 Feb 2026 11:50:30 +0100 Subject: [PATCH 08/34] Remove localized TaskHostCallbackConnectionLost error This error can only occur in TaskHost when the connection to parent is lost, meaning there's no way to log it anyway. Use a simple hardcoded string instead of a localized resource. --- src/MSBuild/OutOfProcTaskHostNode.cs | 4 +++- src/MSBuild/Resources/Strings.resx | 4 ---- src/MSBuild/Resources/xlf/Strings.cs.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.de.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.es.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.fr.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.it.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.ja.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.ko.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.pl.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.ru.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.tr.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 5 ----- src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 5 ----- 15 files changed, 3 insertions(+), 70 deletions(-) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index e18a59eeae2..7c2ec5f3933 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -835,8 +835,10 @@ private TResponse SendCallbackRequestAndWaitForResponse(ITaskHostCall // Timeout - check connection status (no WaitHandle available for this) if (_nodeEndpoint.LinkStatus != LinkStatus.Active) { + // Connection lost - no way to log this since the pipe is broken. + // This is extremely rare (parent killed without killing TaskHost). throw new InvalidOperationException( - ResourceUtilities.FormatResourceStringStripCodeAndKeyword("TaskHostCallbackConnectionLost")); + "TaskHost lost connection to parent process during callback."); } } diff --git a/src/MSBuild/Resources/Strings.resx b/src/MSBuild/Resources/Strings.resx index e0ad46f9c58..b2e5738fac1 100644 --- a/src/MSBuild/Resources/Strings.resx +++ b/src/MSBuild/Resources/Strings.resx @@ -1743,8 +1743,4 @@ Don't forget to update this comment after using the new code. --> - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index 81d69076e79..19d1acc6ea1 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -1857,11 +1857,6 @@ Když se nastaví na MessageUponIsolationViolation (nebo jeho krátký Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. Terminálový protokolovací nástroj nebyl použit, protože bylo zjištěno, že sestavení běží v automatizovaném prostředí. diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index d2beef196a6..25bdb347caf 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -1845,11 +1845,6 @@ Hinweis: Ausführlichkeit der Dateiprotokollierungen Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. Die Terminalprotokollierung wurde nicht verwendet, da erkannt wurde, dass der Build in einer automatisierten Umgebung ausgeführt wird. diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index abfb40d7ebd..b6ed0ce9a27 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -1851,11 +1851,6 @@ Esta marca es experimental y puede que no funcione según lo previsto. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. No se usó el registrador de terminal porque se detectó que la compilación se está ejecutando en un entorno automatizado. diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index 5758923e5e6..773c1dc2853 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -1845,11 +1845,6 @@ Remarque : verbosité des enregistreurs d’événements de fichiers Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. L'enregistreur de terminal n'a pas été utilisé, car il a été détecté que la build s'exécute dans un environnement automatisé. diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index 911f7f85045..b65ffc21167 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -1855,11 +1855,6 @@ Nota: livello di dettaglio dei logger di file Il parametro di output dell'attività "{0}" conteneva {1} elementi null che sono stati rimossi. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. Il logger del terminale non è stato utilizzato perché è stato rilevato che la compilazione è in esecuzione in un ambiente automatizzato. diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index e761ab0600c..8dfb50fdce9 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -1845,11 +1845,6 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. ビルドが自動化された環境で実行されていることが検出されたため、ターミナル ロガーは使用されませんでした。 diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index bb3238e22e6..dc4985de1af 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -1846,11 +1846,6 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. 자동화된 환경에서 빌드가 실행 중임을 감지하여 터미널 로거를 사용하지 않았습니다. diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index 1903f66dbea..d2e256d68b5 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -1854,11 +1854,6 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. Rejestrator terminali nie został użyty, ponieważ wykryto, że kompilacja jest uruchomiona w zautomatyzowanym środowisku. diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index c493ee20327..a1504c8c111 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -1845,11 +1845,6 @@ arquivo de resposta. Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. O Agente de Terminal não foi usado porque foi detectado que a compilação está em execução em um ambiente automatizado. diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index 4f267159bc8..95099967cf5 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -1843,11 +1843,6 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. Средство ведения журнала терминалов не использовалось, так как было обнаружено, что сборка выполняется в автоматизированной среде. diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index 07f49900326..4804911e58d 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -1848,11 +1848,6 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. Terminal Günlükçüsü, derlemenin otomatik bir ortamda çalıştığı tespit edildiğinden kullanılmadı. diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 9ff29f50dbf..320f4ec8c49 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -1845,11 +1845,6 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. 未使用终端记录器,因为检测到生成正在自动化环境中运行。 diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index 006de177f9b..5ae82cd4b7b 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -1845,11 +1845,6 @@ Task output parameter "{0}" contained {1} null element(s) which have been filtered out. LOCALIZATION: {0} is the name of the task output parameter, {1} is the count of null elements filtered. - - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - MSB5027: TaskHost lost connection to parent process during callback. The build may have been cancelled. - {StrBegin="MSB5027: "} - Terminal Logger was not used because it was detected that the build is running in an automated environment. 檢測到此組建正在自動化環境中執行,因此未使用終端機記錄器。 From 54c55bc42305d81fb700a24f91e5d1b59af02a21 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 9 Feb 2026 14:05:29 +0100 Subject: [PATCH 09/34] Fail on invalid packet type in HandleCallbackResponse Throw InternalError if HandleCallbackResponse receives a packet that doesn't implement ITaskHostCallbackPacket (programming error). Unknown request ID is still silently ignored since it's expected when a request was cancelled before the response arrived. --- src/MSBuild/OutOfProcTaskHostNode.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 7c2ec5f3933..4014c4958b2 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -774,10 +774,15 @@ private void HandlePacket(INodePacket packet) /// private void HandleCallbackResponse(INodePacket packet) { - // Silent no-op if packet doesn't implement ITaskHostCallbackPacket or request ID unknown. - // Unknown ID can occur if request was cancelled/abandoned before response arrived. - if (packet is ITaskHostCallbackPacket callbackPacket - && _pendingCallbackRequests.TryRemove(callbackPacket.RequestId, out TaskCompletionSource tcs)) + if (packet is not ITaskHostCallbackPacket callbackPacket) + { + ErrorUtilities.ThrowInternalError("HandleCallbackResponse called with non-callback packet type: {0}", packet.GetType().Name); + return; + } + + // Request ID not found is expected if the request was cancelled before response arrived. + // The task thread already threw BuildAbortedException and cleaned up. + if (_pendingCallbackRequests.TryRemove(callbackPacket.RequestId, out TaskCompletionSource tcs)) { tcs.TrySetResult(packet); } From 0afb492f1da8c02bd43b97de1061994ca1f68818 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 9 Feb 2026 14:08:42 +0100 Subject: [PATCH 10/34] Throw NotImplementedException for unknown query types Unknown query type is a programming bug, not a runtime condition. Fail fast to make bugs immediately visible rather than returning a silent false that would cause subtle, hard-to-debug behavior. --- src/Build/Instance/TaskFactories/TaskHostTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 09e45118808..3ccead0ecc7 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -661,7 +661,7 @@ private void HandleQueryRequest(TaskHostQueryRequest request) { TaskHostQueryRequest.QueryType.IsRunningMultipleNodes => _buildEngine is IBuildEngine2 engine2 && engine2.IsRunningMultipleNodes, - _ => false // Unknown query type - return safe default + _ => throw new System.NotImplementedException($"Unknown TaskHost query type: {request.Query}") }; var response = new TaskHostQueryResponse(request.RequestId, result); From ae179c6e6a44e9a72404a2d252df612d02abd7f8 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 9 Feb 2026 17:44:41 +0100 Subject: [PATCH 11/34] Align callback cancellation with in-process mode: remove _taskCancelledEvent from wait In regular (non-TaskHost) mode, IBuildEngine callbacks are direct method calls that always complete - cancel never interrupts a callback mid-flight. Instead, cancellation causes the work behind the callback (e.g. child build in BuildProjectFile) to fail fast, and the callback returns normally. Align TaskHost to match: the parent continues processing callback requests even after sending TaskHostTaskCancelled, so the response always arrives. Cooperative cancellation via ICancelableTask.Cancel() handles the rest. Only InvalidOperationException on connection loss remains (parent process killed), detected via 1000ms LinkStatus polling. --- src/MSBuild/OutOfProcTaskHostNode.cs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 4014c4958b2..9f397511649 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -780,8 +780,8 @@ private void HandleCallbackResponse(INodePacket packet) return; } - // Request ID not found is expected if the request was cancelled before response arrived. - // The task thread already threw BuildAbortedException and cleaned up. + // Request ID not found is expected if the connection was lost and the task thread + // already cleaned up via the finally block in SendCallbackRequestAndWaitForResponse. if (_pendingCallbackRequests.TryRemove(callbackPacket.RequestId, out TaskCompletionSource tcs)) { tcs.TrySetResult(packet); @@ -796,10 +796,15 @@ private void HandleCallbackResponse(INodePacket packet) /// The request packet to send (must implement ITaskHostCallbackPacket). /// The response packet. /// If the connection is lost. - /// If the task is cancelled during the callback. /// /// This method is infrastructure for callback support. Used by IsRunningMultipleNodes, /// RequestCores/ReleaseCores, BuildProjectFile, etc. + /// + /// We intentionally do NOT check _taskCancelledEvent here. This aligns with in-process + /// mode where IBuildEngine callbacks are direct method calls that complete regardless of + /// cancellation state. The parent continues processing callback requests even after + /// sending TaskHostTaskCancelled, so the response will arrive. Cancellation is handled + /// cooperatively via ICancelableTask.Cancel() on the task itself. /// private TResponse SendCallbackRequestAndWaitForResponse(ITaskHostCallbackPacket request) where TResponse : class, INodePacket @@ -818,24 +823,17 @@ private TResponse SendCallbackRequestAndWaitForResponse(ITaskHostCall // Send the request packet to the parent _nodeEndpoint.SendData(request); - // Wait for either: response arrives, task cancelled, or connection lost - // No timeout - callbacks like BuildProjectFile can legitimately take hours - WaitHandle[] waitHandles = [responseEvent, _taskCancelledEvent]; - + // Wait for the response or connection loss. + // No timeout - callbacks like BuildProjectFile can legitimately take hours. while (true) { - int signaledIndex = WaitHandle.WaitAny(waitHandles, millisecondsTimeout: 1000); + int signaledIndex = WaitHandle.WaitAny([responseEvent], millisecondsTimeout: 1000); if (signaledIndex == 0) { // Response received break; } - else if (signaledIndex == 1) - { - // Task cancelled - throw new BuildAbortedException(); - } // Timeout - check connection status (no WaitHandle available for this) if (_nodeEndpoint.LinkStatus != LinkStatus.Active) From e441126f54a05c764fc15097171383706a637a14 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 9 Feb 2026 17:58:07 +0100 Subject: [PATCH 12/34] Add TaskHost threading model documentation --- .../multithreading/taskhost-threading.md | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 documentation/specs/multithreading/taskhost-threading.md diff --git a/documentation/specs/multithreading/taskhost-threading.md b/documentation/specs/multithreading/taskhost-threading.md new file mode 100644 index 00000000000..aaeefdd14a4 --- /dev/null +++ b/documentation/specs/multithreading/taskhost-threading.md @@ -0,0 +1,89 @@ +# Threading in TaskHost Processes + +MSBuild can run tasks in a separate process called a **TaskHost** (`OutOfProcTaskHostNode`). This happens when a task requires a different runtime, architecture, or when multithreaded mode (`-mt`) ejects a non-thread-safe task out of the worker node. The TaskHost process communicates with the parent worker node over a named pipe. + +## Thread Model + +The TaskHost has two threads: + +### Main Thread (Communication Thread) + +The main thread runs `OutOfProcTaskHostNode.Run()`, a `WaitHandle.WaitAny` loop that services four events: + +| Index | Event | Handler | +|-------|-------|---------| +| 0 | `_shutdownEvent` | `HandleShutdown()` — joins the task thread, cleans up, exits | +| 1 | `_packetReceivedEvent` | `HandlePacket()` — dispatches incoming IPC packets | +| 2 | `_taskCompleteEvent` | `CompleteTask()` — sends `TaskHostTaskComplete` to parent | +| 3 | `_taskCancelledEvent` | `CancelTask()` — calls `ICancelableTask.Cancel()` on the task | + +This thread is responsible for all IPC: receiving packets from the parent (task configuration, cancellation, callback responses) and sending packets back (log messages, task completion, callback requests). + +### Task Runner Thread + +When the main thread receives a `TaskHostConfiguration` packet, it spawns the task runner thread (`RunTask`). This thread: + +1. Sets up the environment (working directory, env vars, culture) +2. Loads the task assembly and instantiates the task +3. Sets task parameters via reflection +4. Calls `task.Execute()` +5. Collects output parameters +6. Packages the result into `TaskHostTaskComplete` and signals `_taskCompleteEvent` + +The task runner thread is where user task code runs. Any `IBuildEngine` calls from the task (logging, property queries, building other projects) are serviced on this thread. + +## IBuildEngine Callback Flow (added in Stage 1) + +Before callback support, the two threads had a simple lifecycle: the main thread spawned the task thread, waited for completion, and sent the result. Communication was one-directional (parent → TaskHost for configuration/cancellation, TaskHost → parent for logs/completion). + +With callback support, the task can query the parent for information it doesn't have locally (e.g., `IsRunningMultipleNodes`, and in future stages: `RequestCores`, `BuildProjectFile`). This introduces **bidirectional IPC** between the threads: + +``` +Task Runner Thread Main Thread Parent Process + | | | + |-- IBuildEngine.Foo() ------------>| | + | (sends request packet) |-- request packet ----------------->| + | (blocks on responseEvent) | | + | | (processes request) | + | |<-- response packet ----------------| + | |-- HandleCallbackResponse() | + | | (sets TCS result) | + |<-- responseEvent signaled --------| | + | (callback returns) | | +``` + +### How It Works + +1. **Task thread** calls an `IBuildEngine` method (e.g., `IsRunningMultipleNodes`). +2. This calls `SendCallbackRequestAndWaitForResponse()`, which: + - Assigns a unique request ID + - Registers a `TaskCompletionSource` in `_pendingCallbackRequests` + - Sends the request packet via `_nodeEndpoint.SendData()` + - Blocks on a `ManualResetEvent` bridged to the TCS, polling every 1000ms for connection loss +3. **Main thread** receives the response packet from the parent, looks up the TCS by request ID, and calls `TrySetResult()`, which signals the event. +4. **Task thread** wakes up, retrieves the typed response, and returns it to the caller. + +### Cancellation Semantics + +The callback wait intentionally does **not** check `_taskCancelledEvent`. This aligns with how in-process `TaskHost` (regular worker node mode) handles callbacks: + +- In regular mode, `IBuildEngine` callbacks are direct method calls that always complete. Cancellation never interrupts a callback mid-flight. Instead, cancellation causes the *work behind* the callback to fail fast (e.g., the scheduler cancels a child build started by `BuildProjectFile`), and the callback returns normally with a failure result. +- In TaskHost mode, the parent continues processing callback requests even after sending `TaskHostTaskCancelled`. The response is **guaranteed** to arrive because the parent's packet loop only exits upon receiving `TaskHostTaskComplete`, which cannot be sent until the task finishes, which cannot happen until the callback returns. + +Cancellation is handled cooperatively: after the callback returns, the task checks its cancellation state (set by `ICancelableTask.Cancel()`) and exits. + +The only exception path is connection loss (parent process killed), detected by polling `_nodeEndpoint.LinkStatus` every 1000ms. This throws `InvalidOperationException`. + +### Response Guarantee (Why the Callback Cannot Deadlock) + +There is a causal dependency chain that prevents deadlock: + +``` +Parent sends callback response + → TaskHost callback returns + → task finishes Execute() + → TaskHost sends TaskHostTaskComplete + → parent exits packet loop +``` + +The parent cannot exit its packet loop without first receiving `TaskHostTaskComplete`. But `TaskHostTaskComplete` cannot be sent until the task finishes. And the task cannot finish while it is blocked waiting for a callback response. Therefore, the parent **must** process the callback request and send the response before it can ever stop. From f24da9d64b538a9794b5c6e2243119ec31af3f59 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 16 Feb 2026 15:08:40 +0100 Subject: [PATCH 13/34] Gate TaskHost callbacks behind version check + Traits escape hatch Address AR-May's cross-version compatibility concern: a new .NET TaskHost connecting to an old MSBuild node would send callback packets the old node doesn't understand. - Add EnableTaskHostCallbacks to Traits.cs (MSBUILDENABLETASKHOSTCALLBACKS env var) - Store parentPacketVersion in OutOfProcTaskHostNode, add CallbacksSupported check - Callbacks enabled when: negotiated version >= 3 OR env var is set - PacketVersion stays at 2 until all callback stages are complete - IsRunningMultipleNodes returns false (safe default) when callbacks not supported - Add test verifying graceful fallback when callbacks disabled - Existing tests set MSBUILDENABLETASKHOSTCALLBACKS=1 to enable callbacks - Restore accidentally-removed StringArrayWithNullsDoesNotCrashTaskHost test --- .../BackEnd/TaskHostCallback_Tests.cs | 36 +++++++++++++++++ .../BackEnd/TaskHostFactory_Tests.cs | 40 +++++++++++++++++++ .../BackEnd/TaskRouter_IntegrationTests.cs | 1 + src/Framework/Traits.cs | 6 +++ src/MSBuild/OutOfProcTaskHostNode.cs | 27 +++++++++++++ 5 files changed, 110 insertions(+) diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs index ce8e36102f2..dc9c80dcb4b 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs @@ -75,6 +75,7 @@ public void TaskHostQueryResponse_RoundTrip_Serialization(bool boolResult) public void IsRunningMultipleNodes_WorksWithExplicitTaskHostFactory(int maxNodeCount, bool expectedResult) { using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS", "1"); string projectContents = $@" @@ -106,6 +107,7 @@ public void IsRunningMultipleNodes_WorksWithExplicitTaskHostFactory(int maxNodeC public void IsRunningMultipleNodes_WorksWhenAutoEjectedInMultiThreadedMode(int maxNodeCount, bool expectedResult) { using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS", "1"); string testDir = env.CreateFolder().Path; // IsRunningMultipleNodesTask lacks MSBuildMultiThreadableTask attribute, so it's auto-ejected to TaskHost in MT mode @@ -142,6 +144,40 @@ public void IsRunningMultipleNodes_WorksWhenAutoEjectedInMultiThreadedMode(int m logger.FullLog.ShouldContain($"IsRunningMultipleNodes = {expectedResult}"); } + /// + /// Verifies IsRunningMultipleNodes returns false (safe default) when callbacks are not enabled. + /// This simulates the cross-version scenario where a new TaskHost connects to an old parent. + /// + [Fact] + public void IsRunningMultipleNodes_ReturnsFalseWhenCallbacksNotSupported() + { + using TestEnvironment env = TestEnvironment.Create(_output); + + // Explicitly do NOT set MSBUILDENABLETASKHOSTCALLBACKS — callbacks should be disabled + string projectContents = $@" + + + + <{nameof(IsRunningMultipleNodesTask)}> + + + +"; + + TransientTestProjectWithFiles project = env.CreateTestProjectWithFiles(projectContents); + ProjectInstance projectInstance = new(project.ProjectFile); + + BuildResult buildResult = BuildManager.DefaultBuildManager.Build( + new BuildParameters { MaxNodeCount = 4, EnableNodeReuse = false }, + new BuildRequestData(projectInstance, targetsToBuild: ["Test"])); + + // Build should succeed — callbacks gracefully return safe defaults + buildResult.OverallResult.ShouldBe(BuildResultCode.Success); + + // IsRunningMultipleNodes should return false (safe default) even though MaxNodeCount=4 + bool.Parse(projectInstance.GetPropertyValue("Result")).ShouldBe(false); + } + #endregion } } diff --git a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs index 9b80c023c94..22178bbe618 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostFactory_Tests.cs @@ -363,5 +363,45 @@ public void VariousParameterTypesCanBeTransmittedToAndReceivedFromTaskHost() projectInstance.GetPropertyValue("CustomStructOutput").ShouldBe(TaskBuilderTestTask.s_customStruct.ToString(CultureInfo.InvariantCulture)); projectInstance.GetPropertyValue("EnumOutput").ShouldBe(TargetBuiltReason.BeforeTargets.ToString()); } + + /// + /// Verifies that a task returning a string[] with null elements does not crash + /// when executed via TaskHostFactory. This is a regression test for + /// https://github.com/dotnet/msbuild/issues/13174 + /// + [Fact] + public void StringArrayWithNullsDoesNotCrashTaskHost() + { + using TestEnvironment env = TestEnvironment.Create(); + + string projectContents = $@" + + + + <{typeof(StringArrayWithNullsTask).Name}> + + + + +"; + + TransientTestFile project = env.CreateFile("testProject.csproj", projectContents); + ProjectInstance projectInstance = new(project.Path); + BuildManager buildManager = BuildManager.DefaultBuildManager; + BuildResult buildResult = buildManager.Build(new BuildParameters(), new BuildRequestData(projectInstance, targetsToBuild: new[] { "TestTarget" })); + + // The build should succeed - nulls should be filtered, not cause a crash + buildResult.OverallResult.ShouldBe(BuildResultCode.Success); + + // Verify task ran out-of-process (TaskHostFactory should force this) + string taskPidStr = projectInstance.GetPropertyValue("TaskPid"); + taskPidStr.ShouldNotBeNullOrEmpty(); + int.TryParse(taskPidStr, out int taskPid).ShouldBeTrue(); + Process.GetCurrentProcess().Id.ShouldNotBe(taskPid, "Task should have run in a separate TaskHost process"); + + // Verify output items - nulls should be filtered out, leaving 3 items + var outputItems = projectInstance.GetItems("OutputItems"); + outputItems.Count.ShouldBe(3, "Null elements should be filtered from the string array"); + } } } diff --git a/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs b/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs index f86aada207c..fa508369b3d 100644 --- a/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs +++ b/src/Build.UnitTests/BackEnd/TaskRouter_IntegrationTests.cs @@ -383,6 +383,7 @@ public void ExplicitTaskHostFactory_OverridesRoutingLogic() } + private string CreateTestProject(string taskName, string taskClass) { return $@" diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index f14208e145a..7ecd440249b 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -130,6 +130,12 @@ public Traits() /// TODO: Replace with command line flag when feature is completed. The environment variable is intented to avoid exposing the flag early. public readonly bool EnableRarNode = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildRarNode")); + /// + /// Enable IBuildEngine callbacks in the TaskHost process. + /// Temporary escape hatch until all callback stages are complete and PacketVersion is bumped to 3. + /// + public readonly bool EnableTaskHostCallbacks = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS")); + /// /// Name of environment variables used to enable MSBuild server. /// diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 9f397511649..fb0c40973b7 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -198,6 +198,26 @@ internal class OutOfProcTaskHostNode : /// Key is the request ID, value is the TaskCompletionSource to signal when response arrives. /// private readonly ConcurrentDictionary> _pendingCallbackRequests = new(); + + /// + /// The packet version negotiated with the parent process. + /// Used to determine if the parent supports callback packets. + /// + private byte _parentPacketVersion; + + /// + /// Minimum packet version required for IBuildEngine callback support. + /// When all callback stages are complete, PacketVersion will be bumped to this value. + /// + private const byte CallbacksMinPacketVersion = 3; + + /// + /// Whether the parent supports IBuildEngine callbacks. + /// True if the parent's packet version is high enough, or if the feature is force-enabled via env var. + /// + private bool CallbacksSupported => + _parentPacketVersion >= CallbacksMinPacketVersion + || Traits.Instance.EnableTaskHostCallbacks; #endif /// @@ -292,6 +312,7 @@ public string ProjectFileOfTaskNode /// /// Implementation of IBuildEngine2.IsRunningMultipleNodes. /// Queries the parent process and returns the actual value. + /// Returns false if the parent doesn't support callbacks (cross-version scenario). /// public bool IsRunningMultipleNodes { @@ -301,6 +322,11 @@ public bool IsRunningMultipleNodes LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); return false; #else + if (!CallbacksSupported) + { + return false; + } + var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); var response = SendCallbackRequestAndWaitForResponse(request); return response.BoolResult; @@ -680,6 +706,7 @@ public NodeEngineShutdownReason Run(out Exception shutdownException, bool nodeRe { #if !CLR2COMPATIBILITY _registeredTaskObjectCache = new RegisteredTaskObjectCacheBase(); + _parentPacketVersion = parentPacketVersion; #endif shutdownException = null; From 9af9103b17cd8f929ba74c3d602465057d1c07ab Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 16 Feb 2026 15:13:10 +0100 Subject: [PATCH 14/34] Revert accidental xlf whitespace changes --- src/MSBuild/Resources/xlf/Strings.cs.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.de.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.es.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.fr.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.it.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.ja.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.ko.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.pl.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.pt-BR.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.ru.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.tr.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf | 4 ++-- src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf | 4 ++-- 14 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/MSBuild/Resources/xlf/Strings.cs.xlf b/src/MSBuild/Resources/xlf/Strings.cs.xlf index 19d1acc6ea1..50ea8d65370 100644 --- a/src/MSBuild/Resources/xlf/Strings.cs.xlf +++ b/src/MSBuild/Resources/xlf/Strings.cs.xlf @@ -1,4 +1,4 @@ - + @@ -2294,4 +2294,4 @@ Když se nastaví na MessageUponIsolationViolation (nebo jeho krátký - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.de.xlf b/src/MSBuild/Resources/xlf/Strings.de.xlf index 25bdb347caf..722c2d1c867 100644 --- a/src/MSBuild/Resources/xlf/Strings.de.xlf +++ b/src/MSBuild/Resources/xlf/Strings.de.xlf @@ -1,4 +1,4 @@ - + @@ -2282,4 +2282,4 @@ Hinweis: Ausführlichkeit der Dateiprotokollierungen - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.es.xlf b/src/MSBuild/Resources/xlf/Strings.es.xlf index b6ed0ce9a27..5bf41aac41e 100644 --- a/src/MSBuild/Resources/xlf/Strings.es.xlf +++ b/src/MSBuild/Resources/xlf/Strings.es.xlf @@ -1,4 +1,4 @@ - + @@ -2286,4 +2286,4 @@ Esta marca es experimental y puede que no funcione según lo previsto. - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.fr.xlf b/src/MSBuild/Resources/xlf/Strings.fr.xlf index 773c1dc2853..90a849c3c37 100644 --- a/src/MSBuild/Resources/xlf/Strings.fr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.fr.xlf @@ -1,4 +1,4 @@ - + @@ -2283,4 +2283,4 @@ Remarque : verbosité des enregistreurs d’événements de fichiers - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.it.xlf b/src/MSBuild/Resources/xlf/Strings.it.xlf index b65ffc21167..70a8c3f94f0 100644 --- a/src/MSBuild/Resources/xlf/Strings.it.xlf +++ b/src/MSBuild/Resources/xlf/Strings.it.xlf @@ -1,4 +1,4 @@ - + @@ -2298,4 +2298,4 @@ Esegue la profilatura della valutazione di MSBuild e scrive - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.ja.xlf b/src/MSBuild/Resources/xlf/Strings.ja.xlf index 8dfb50fdce9..063d6db1eab 100644 --- a/src/MSBuild/Resources/xlf/Strings.ja.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ja.xlf @@ -1,4 +1,4 @@ - + @@ -2282,4 +2282,4 @@ - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.ko.xlf b/src/MSBuild/Resources/xlf/Strings.ko.xlf index dc4985de1af..377fe9430c2 100644 --- a/src/MSBuild/Resources/xlf/Strings.ko.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ko.xlf @@ -1,4 +1,4 @@ - + @@ -2283,4 +2283,4 @@ - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.pl.xlf b/src/MSBuild/Resources/xlf/Strings.pl.xlf index d2e256d68b5..9a830403cde 100644 --- a/src/MSBuild/Resources/xlf/Strings.pl.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pl.xlf @@ -1,4 +1,4 @@ - + @@ -2291,4 +2291,4 @@ Ta flaga jest eksperymentalna i może nie działać zgodnie z oczekiwaniami. - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf index a1504c8c111..61740aaed8e 100644 --- a/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf +++ b/src/MSBuild/Resources/xlf/Strings.pt-BR.xlf @@ -1,4 +1,4 @@ - + @@ -2282,4 +2282,4 @@ arquivo de resposta. - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.ru.xlf b/src/MSBuild/Resources/xlf/Strings.ru.xlf index 95099967cf5..3e17fc13b6a 100644 --- a/src/MSBuild/Resources/xlf/Strings.ru.xlf +++ b/src/MSBuild/Resources/xlf/Strings.ru.xlf @@ -1,4 +1,4 @@ - + @@ -2282,4 +2282,4 @@ - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.tr.xlf b/src/MSBuild/Resources/xlf/Strings.tr.xlf index 4804911e58d..be9a1a336f4 100644 --- a/src/MSBuild/Resources/xlf/Strings.tr.xlf +++ b/src/MSBuild/Resources/xlf/Strings.tr.xlf @@ -1,4 +1,4 @@ - + @@ -2285,4 +2285,4 @@ - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.xlf b/src/MSBuild/Resources/xlf/Strings.xlf index c656310ece9..a16ca1ca45e 100644 --- a/src/MSBuild/Resources/xlf/Strings.xlf +++ b/src/MSBuild/Resources/xlf/Strings.xlf @@ -1,4 +1,4 @@ - + @@ -1056,4 +1056,4 @@ - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf index 320f4ec8c49..6c10226dc85 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hans.xlf @@ -1,4 +1,4 @@ - + @@ -2285,4 +2285,4 @@ - + \ No newline at end of file diff --git a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf index 5ae82cd4b7b..dd13ca9a66f 100644 --- a/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf +++ b/src/MSBuild/Resources/xlf/Strings.zh-Hant.xlf @@ -1,4 +1,4 @@ - + @@ -2282,4 +2282,4 @@ - + \ No newline at end of file From c9caf0ecf536016ff9e233a7077a6484ce66c869 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Tue, 17 Feb 2026 11:32:36 +0100 Subject: [PATCH 15/34] Add .NET Core TaskHost E2E test for callback support Tests the cross-runtime scenario where .NET Framework MSBuild.exe spawns a .NET Core TaskHost with Runtime='NET' and TaskHostFactory. Uses bootstrap dotnet to ensure TaskHost loads locally-built MSBuild.dll. --- .../Microsoft.Build.Engine.UnitTests.csproj | 3 ++ src/Build.UnitTests/NetTaskHost_E2E_Tests.cs | 27 +++++++++++++++++ .../ExampleTask/CallbackTestTask.cs | 30 +++++++++++++++++++ .../TestNetTaskCallback.csproj | 25 ++++++++++++++++ .../TestNetTaskCallback/global.json | 9 ++++++ 5 files changed, 94 insertions(+) create mode 100644 src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs create mode 100644 src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/TestNetTaskCallback.csproj create mode 100644 src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/global.json diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 25ce6ea2727..420872194a2 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -145,6 +145,9 @@ PreserveNewest + + PreserveNewest + diff --git a/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs b/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs index a0b35a6ed39..c63588fcd02 100644 --- a/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs +++ b/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs @@ -135,6 +135,33 @@ public void MSBuildTaskInNetHostTest() testTaskOutput.ShouldContain($"Hello TEST"); } + [WindowsFullFrameworkOnlyFact] + public void NetTaskHost_CallbackIsRunningMultipleNodesTest() + { + using TestEnvironment env = TestEnvironment.Create(_output); + env.SetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS", "1"); + + // Point dotnet resolution to the bootstrap layout so the .NET Core TaskHost + // uses the locally-built MSBuild.dll (with callback support) instead of the system SDK. + string bootstrapCorePath = Path.Combine(RunnerUtilities.BootstrapRootPath, "core"); + string bootstrapDotnet = Path.Combine(bootstrapCorePath, "dotnet.exe"); + env.SetEnvironmentVariable("DOTNET_HOST_PATH", bootstrapDotnet); + env.SetEnvironmentVariable("DOTNET_ROOT", bootstrapCorePath); + env.SetEnvironmentVariable("DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR", bootstrapCorePath); + + string testProjectPath = Path.Combine(TestAssetsRootPath, "ExampleNetTask", "TestNetTaskCallback", "TestNetTaskCallback.csproj"); + + string testTaskOutput = RunnerUtilities.ExecBootstrapedMSBuild($"{testProjectPath} -restore -v:n -p:LatestDotNetCoreForMSBuild={RunnerUtilities.LatestDotNetCoreForMSBuild}", out bool successTestTask); + + if (!successTestTask) + { + _output.WriteLine(testTaskOutput); + } + + successTestTask.ShouldBeTrue(); + testTaskOutput.ShouldContain("CallbackResult: IsRunningMultipleNodes = False"); + } + [WindowsFullFrameworkOnlyFact] public void NetTaskWithImplicitHostParamsTest() { diff --git a/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs b/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs new file mode 100644 index 00000000000..b7b62ae96db --- /dev/null +++ b/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.Framework; + +namespace NetTask +{ + /// + /// A simple task that queries IsRunningMultipleNodes via IBuildEngine2 callback. + /// Used to test that TaskHost callbacks work in the .NET Core TaskHost spawned from .NET Framework parent. + /// + public class CallbackTestTask : Microsoft.Build.Utilities.Task + { + [Output] + public bool IsRunningMultipleNodes { get; set; } + + public override bool Execute() + { + if (BuildEngine is IBuildEngine2 engine2) + { + IsRunningMultipleNodes = engine2.IsRunningMultipleNodes; + Log.LogMessage(MessageImportance.High, $"IsRunningMultipleNodes = {IsRunningMultipleNodes}"); + return true; + } + + Log.LogError("BuildEngine does not implement IBuildEngine2"); + return false; + } + } +} diff --git a/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/TestNetTaskCallback.csproj b/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/TestNetTaskCallback.csproj new file mode 100644 index 00000000000..f74bbdd71a7 --- /dev/null +++ b/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/TestNetTaskCallback.csproj @@ -0,0 +1,25 @@ + + + + $(LatestDotNetCoreForMSBuild) + + + + $([System.IO.Path]::GetFullPath('$([System.IO.Path]::Combine('$(MSBuildThisFileDirectory)', '..', '..', '..', '..'))')) + $([System.IO.Path]::Combine('$(TestProjectFolder)', '$(TargetFramework)', 'ExampleTask.dll')) + + + + + + + + + + + + diff --git a/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/global.json b/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/global.json new file mode 100644 index 00000000000..611555aea5f --- /dev/null +++ b/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/global.json @@ -0,0 +1,9 @@ +{ + "sdk": { + // This global.json is needed to prevent builds running in tests using the bootstrap layout from walking + // up the repo tree and resolving our sdk.paths, instead of the bootstrap layout's SDK. + // See https://github.com/dotnet/runtime/issues/118488 for details. + "allowPrerelease": true, + "rollForward": "latestMajor" + } +} From 4c99f40fed9d5d779f3a59ec1bccc7c3f8a225e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 23 Feb 2026 12:03:18 +0100 Subject: [PATCH 16/34] Apply suggestion from @JanProvaznik --- src/MSBuild/OutOfProcTaskHostNode.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index fb0c40973b7..fdfe4dce700 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -324,6 +324,7 @@ public bool IsRunningMultipleNodes #else if (!CallbacksSupported) { + LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported"); return false; } From 9109e0c30d4c9085ff2d623405c54790042cc7ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Provazn=C3=ADk?= Date: Mon, 23 Feb 2026 14:38:40 +0100 Subject: [PATCH 17/34] Update documentation/specs/multithreading/taskhost-threading.md Co-authored-by: Rainer Sigwald --- .../multithreading/taskhost-threading.md | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/documentation/specs/multithreading/taskhost-threading.md b/documentation/specs/multithreading/taskhost-threading.md index aaeefdd14a4..f2804f2010f 100644 --- a/documentation/specs/multithreading/taskhost-threading.md +++ b/documentation/specs/multithreading/taskhost-threading.md @@ -38,18 +38,23 @@ Before callback support, the two threads had a simple lifecycle: the main thread With callback support, the task can query the parent for information it doesn't have locally (e.g., `IsRunningMultipleNodes`, and in future stages: `RequestCores`, `BuildProjectFile`). This introduces **bidirectional IPC** between the threads: -``` -Task Runner Thread Main Thread Parent Process - | | | - |-- IBuildEngine.Foo() ------------>| | - | (sends request packet) |-- request packet ----------------->| - | (blocks on responseEvent) | | - | | (processes request) | - | |<-- response packet ----------------| - | |-- HandleCallbackResponse() | - | | (sets TCS result) | - |<-- responseEvent signaled --------| | - | (callback returns) | | +```mermaid +sequenceDiagram + participant TR as Task Runner Thread + participant MT as Main Thread + participant PP as Scheduler Process + + TR->>MT: IBuildEngine.Foo()
(sends request packet, blocks) + activate TR + + MT->>PP: request packet + Note over PP: (processes request) + + PP-->>MT: response packet + MT->>MT: HandleCallbackResponse()
(sets TCS result) + + MT-->>TR: responseEvent signaled + deactivate TR ``` ### How It Works From 3c9243e5068f450e699b9ea6de1a5fc51f8179b7 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 16:00:49 +0100 Subject: [PATCH 18/34] Simplify callback wait: replace polling with fail-on-disconnect Remove ManualResetEvent + ContinueWith bridge and 1s polling loop. Block on tcs.Task.GetAwaiter().GetResult() directly. Connection loss is handled by OnLinkStatusChanged failing all pending TCS immediately. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../multithreading/taskhost-threading.md | 6 +-- src/MSBuild/OutOfProcTaskHostNode.cs | 45 +++++++++---------- 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/documentation/specs/multithreading/taskhost-threading.md b/documentation/specs/multithreading/taskhost-threading.md index f2804f2010f..d0090f21894 100644 --- a/documentation/specs/multithreading/taskhost-threading.md +++ b/documentation/specs/multithreading/taskhost-threading.md @@ -64,8 +64,8 @@ sequenceDiagram - Assigns a unique request ID - Registers a `TaskCompletionSource` in `_pendingCallbackRequests` - Sends the request packet via `_nodeEndpoint.SendData()` - - Blocks on a `ManualResetEvent` bridged to the TCS, polling every 1000ms for connection loss -3. **Main thread** receives the response packet from the parent, looks up the TCS by request ID, and calls `TrySetResult()`, which signals the event. + - Blocks on `tcs.Task.GetAwaiter().GetResult()` until the TCS is completed +3. **Main thread** receives the response packet from the parent, looks up the TCS by request ID, and calls `TrySetResult()`. 4. **Task thread** wakes up, retrieves the typed response, and returns it to the caller. ### Cancellation Semantics @@ -77,7 +77,7 @@ The callback wait intentionally does **not** check `_taskCancelledEvent`. This a Cancellation is handled cooperatively: after the callback returns, the task checks its cancellation state (set by `ICancelableTask.Cancel()`) and exits. -The only exception path is connection loss (parent process killed), detected by polling `_nodeEndpoint.LinkStatus` every 1000ms. This throws `InvalidOperationException`. +The only exception path is connection loss (parent process killed), detected by `OnLinkStatusChanged` which fails all pending `TaskCompletionSource` entries with `InvalidOperationException`. This unblocks task threads immediately. ### Response Guarantee (Why the Callback Cannot Deadlock) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index fdfe4dce700..b86526d7dd3 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -833,6 +833,9 @@ private void HandleCallbackResponse(INodePacket packet) /// cancellation state. The parent continues processing callback requests even after /// sending TaskHostTaskCancelled, so the response will arrive. Cancellation is handled /// cooperatively via ICancelableTask.Cancel() on the task itself. + /// + /// Connection loss is handled by OnLinkStatusChanged, which fails all pending TCS + /// with InvalidOperationException, causing this method to throw immediately. /// private TResponse SendCallbackRequestAndWaitForResponse(ITaskHostCallbackPacket request) where TResponse : class, INodePacket @@ -840,10 +843,7 @@ private TResponse SendCallbackRequestAndWaitForResponse(ITaskHostCall int requestId = Interlocked.Increment(ref _nextCallbackRequestId); request.RequestId = requestId; - // Use ManualResetEvent to bridge TaskCompletionSource to WaitHandle - using var responseEvent = new ManualResetEvent(false); var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - tcs.Task.ContinueWith(_ => responseEvent.Set(), TaskContinuationOptions.ExecuteSynchronously); _pendingCallbackRequests[requestId] = tcs; try @@ -851,29 +851,10 @@ private TResponse SendCallbackRequestAndWaitForResponse(ITaskHostCall // Send the request packet to the parent _nodeEndpoint.SendData(request); - // Wait for the response or connection loss. + // Block until the response arrives (via HandleCallbackResponse → TCS.SetResult) + // or the connection is lost (via OnLinkStatusChanged → TCS.TrySetException). // No timeout - callbacks like BuildProjectFile can legitimately take hours. - while (true) - { - int signaledIndex = WaitHandle.WaitAny([responseEvent], millisecondsTimeout: 1000); - - if (signaledIndex == 0) - { - // Response received - break; - } - - // Timeout - check connection status (no WaitHandle available for this) - if (_nodeEndpoint.LinkStatus != LinkStatus.Active) - { - // Connection lost - no way to log this since the pipe is broken. - // This is extremely rare (parent killed without killing TaskHost). - throw new InvalidOperationException( - "TaskHost lost connection to parent process during callback."); - } - } - - INodePacket response = tcs.Task.Result; + INodePacket response = tcs.Task.GetAwaiter().GetResult(); if (response is TResponse typedResponse) { @@ -1057,6 +1038,20 @@ private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) case LinkStatus.ConnectionFailed: case LinkStatus.Failed: _shutdownReason = NodeEngineShutdownReason.ConnectionFailed; + +#if !CLR2COMPATIBILITY + // Fail all pending callback requests so task threads unblock immediately + // instead of waiting indefinitely for responses that will never arrive. + foreach (var kvp in _pendingCallbackRequests) + { + if (_pendingCallbackRequests.TryRemove(kvp.Key, out TaskCompletionSource tcs)) + { + tcs.TrySetException(new InvalidOperationException( + ResourceUtilities.GetResourceString("TaskHostCallbackConnectionLost"))); + } + } +#endif + _shutdownEvent.Set(); break; From 5f92f5a59d75b5f8c3b6d7a854a71d0ca973993c Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 17:24:21 +0100 Subject: [PATCH 19/34] Use plain string for connection-lost exception (not a user-facing message) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/MSBuild/OutOfProcTaskHostNode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index b86526d7dd3..b750beb920c 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -1047,7 +1047,7 @@ private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) if (_pendingCallbackRequests.TryRemove(kvp.Key, out TaskCompletionSource tcs)) { tcs.TrySetException(new InvalidOperationException( - ResourceUtilities.GetResourceString("TaskHostCallbackConnectionLost"))); + "TaskHost lost connection to parent process during callback.")); } } #endif From b2b3f5ee5971d3ddc00600a6c18a56f9208ef42b Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 17:31:21 +0100 Subject: [PATCH 20/34] Replace 'parent' terminology with 'owning worker node' per review Update all comments and documentation in OutOfProcTaskHostNode.cs, TaskHostConfiguration.cs, TaskHostTaskComplete.cs, ITaskHostCallbackPacket.cs, TaskHostQueryRequest.cs, TaskHostQueryResponse.cs, TaskHostCallback_Tests.cs, and taskhost-threading.md to use 'owning worker node' instead of 'parent' to avoid confusion with the many tree/graph structures in MSBuild. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../plan.md | 82 +++++++++++++++++++ .../multithreading/taskhost-threading.md | 26 +++--- .../BackEnd/TaskHostCallback_Tests.cs | 2 +- src/MSBuild/OutOfProcTaskHostNode.cs | 58 ++++++------- src/Shared/ITaskHostCallbackPacket.cs | 4 +- src/Shared/TaskHostConfiguration.cs | 2 +- src/Shared/TaskHostQueryRequest.cs | 2 +- src/Shared/TaskHostQueryResponse.cs | 2 +- src/Shared/TaskHostTaskComplete.cs | 2 +- 9 files changed, 131 insertions(+), 49 deletions(-) create mode 100644 .copilot/session-state/30d6c980-aa15-4be2-ab99-d518a7b1a36a/plan.md diff --git a/.copilot/session-state/30d6c980-aa15-4be2-ab99-d518a7b1a36a/plan.md b/.copilot/session-state/30d6c980-aa15-4be2-ab99-d518a7b1a36a/plan.md new file mode 100644 index 00000000000..8acd4167417 --- /dev/null +++ b/.copilot/session-state/30d6c980-aa15-4be2-ab99-d518a7b1a36a/plan.md @@ -0,0 +1,82 @@ +# AR-May's Cross-Version Compatibility — Option B Implementation Plan + +## Problem +New .NET task host could send callback packets to old MSBuild node that doesn't understand them. + +## Solution: Option B with Traits.cs +- `PacketVersion` stays at `2` (safe for shipping) +- Callbacks gated by: `parentPacketVersion >= CallbacksMinVersion(3) || Traits.Instance.EnableTaskHostCallbacks` +- `Traits.cs` reads `MSBUILDENABLETASKHOSTCALLBACKS` env var (follows existing pattern) +- Tests set env var → `Traits` picks it up (auto-refreshed per test via `BuildEnvironmentState.s_runningTests`) +- When all stages are done: bump PacketVersion to 3, remove `Traits` escape hatch + +## Why Traits.cs +- **Established pattern**: Same approach as `EnableRarNode`, `ForceAllTasksOutOfProcToTaskHost`, etc. +- **Test-friendly**: `Traits.Instance` re-creates per test when `s_runningTests` is set, so env var changes are picked up automatically +- **Shared across assemblies**: `Traits` is in `Microsoft.Build.Framework`, accessible from both `MSBuild.exe` and `Microsoft.Build.dll` +- **Discoverable**: All MSBuild feature toggles live in one place + +## Implementation Details + +### 1. Traits.cs +**File**: `src/Framework/Traits.cs` + +Add to `Traits` class (next to `EnableRarNode`): +```csharp +/// +/// Enable IBuildEngine callbacks in the TaskHost process. +/// Temporary escape hatch until all callback stages are complete and PacketVersion is bumped. +/// +public readonly bool EnableTaskHostCallbacks = !string.IsNullOrEmpty( + Environment.GetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS")); +``` + +### 2. OutOfProcTaskHostNode changes +**File**: `src/MSBuild/OutOfProcTaskHostNode.cs` + +Add field + store in `Run()`: +```csharp +private byte _parentPacketVersion; +private const byte CallbacksMinPacketVersion = 3; +``` + +Add helper: +```csharp +private bool CallbacksSupported => + _parentPacketVersion >= CallbacksMinPacketVersion + || Traits.Instance.EnableTaskHostCallbacks; +``` + +Guard `IsRunningMultipleNodes`: +```csharp +if (!CallbacksSupported) return false; +``` + +### 3. Test changes +**File**: `src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs` + +Existing integration tests: Add `env.SetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS", "1")` + +New test: `IsRunningMultipleNodes_ReturnsFalseWhenCallbacksNotSupported` — no env var, verify `false` default + +### 4. Safe defaults when callbacks disabled +| Callback | Default | Rationale | +|----------|---------|-----------| +| `IsRunningMultipleNodes` | `false` | Same as pre-callback behavior | +| `RequestCores` (future) | `1` | Single core | +| `ReleaseCores` (future) | no-op | Nothing to release | +| `BuildProjectFile` (future) | throw | Task depends on result | + +## Workplan + +- [ ] Add `EnableTaskHostCallbacks` to `Traits.cs` +- [ ] Add `_parentPacketVersion` field and `CallbacksMinPacketVersion` const +- [ ] Add `CallbacksSupported` property +- [ ] Guard `IsRunningMultipleNodes` with `CallbacksSupported` check +- [ ] Update existing integration tests to set env var +- [ ] Add new test for graceful fallback (no env var → returns false) +- [ ] Restore `StringArrayWithNullsDoesNotCrashTaskHost` test (done locally) +- [ ] Commit and push +- [ ] Reply to AR-May's review comment explaining the approach + + diff --git a/documentation/specs/multithreading/taskhost-threading.md b/documentation/specs/multithreading/taskhost-threading.md index d0090f21894..91df5b21758 100644 --- a/documentation/specs/multithreading/taskhost-threading.md +++ b/documentation/specs/multithreading/taskhost-threading.md @@ -1,6 +1,6 @@ # Threading in TaskHost Processes -MSBuild can run tasks in a separate process called a **TaskHost** (`OutOfProcTaskHostNode`). This happens when a task requires a different runtime, architecture, or when multithreaded mode (`-mt`) ejects a non-thread-safe task out of the worker node. The TaskHost process communicates with the parent worker node over a named pipe. +MSBuild can run tasks in a separate process called a **TaskHost** (`OutOfProcTaskHostNode`). This happens when a task requires a different runtime, architecture, or when multithreaded mode (`-mt`) ejects a non-thread-safe task out of the worker node. The TaskHost process communicates with the owning worker node over a named pipe. ## Thread Model @@ -14,10 +14,10 @@ The main thread runs `OutOfProcTaskHostNode.Run()`, a `WaitHandle.WaitAny` loop |-------|-------|---------| | 0 | `_shutdownEvent` | `HandleShutdown()` — joins the task thread, cleans up, exits | | 1 | `_packetReceivedEvent` | `HandlePacket()` — dispatches incoming IPC packets | -| 2 | `_taskCompleteEvent` | `CompleteTask()` — sends `TaskHostTaskComplete` to parent | +| 2 | `_taskCompleteEvent` | `CompleteTask()` — sends `TaskHostTaskComplete` to owning worker node | | 3 | `_taskCancelledEvent` | `CancelTask()` — calls `ICancelableTask.Cancel()` on the task | -This thread is responsible for all IPC: receiving packets from the parent (task configuration, cancellation, callback responses) and sending packets back (log messages, task completion, callback requests). +This thread is responsible for all IPC: receiving packets from the owning worker node (task configuration, cancellation, callback responses) and sending packets back (log messages, task completion, callback requests). ### Task Runner Thread @@ -34,15 +34,15 @@ The task runner thread is where user task code runs. Any `IBuildEngine` calls fr ## IBuildEngine Callback Flow (added in Stage 1) -Before callback support, the two threads had a simple lifecycle: the main thread spawned the task thread, waited for completion, and sent the result. Communication was one-directional (parent → TaskHost for configuration/cancellation, TaskHost → parent for logs/completion). +Before callback support, the two threads had a simple lifecycle: the main thread spawned the task thread, waited for completion, and sent the result. Communication was one-directional (worker node → TaskHost for configuration/cancellation, TaskHost → worker node for logs/completion). -With callback support, the task can query the parent for information it doesn't have locally (e.g., `IsRunningMultipleNodes`, and in future stages: `RequestCores`, `BuildProjectFile`). This introduces **bidirectional IPC** between the threads: +With callback support, the task can query the owning worker node for information it doesn't have locally (e.g., `IsRunningMultipleNodes`, and in future stages: `RequestCores`, `BuildProjectFile`). This introduces **bidirectional IPC** between the threads: ```mermaid sequenceDiagram participant TR as Task Runner Thread participant MT as Main Thread - participant PP as Scheduler Process + participant PP as Owning Worker Node TR->>MT: IBuildEngine.Foo()
(sends request packet, blocks) activate TR @@ -53,7 +53,7 @@ sequenceDiagram PP-->>MT: response packet MT->>MT: HandleCallbackResponse()
(sets TCS result) - MT-->>TR: responseEvent signaled + MT-->>TR: TCS unblocks deactivate TR ``` @@ -65,7 +65,7 @@ sequenceDiagram - Registers a `TaskCompletionSource` in `_pendingCallbackRequests` - Sends the request packet via `_nodeEndpoint.SendData()` - Blocks on `tcs.Task.GetAwaiter().GetResult()` until the TCS is completed -3. **Main thread** receives the response packet from the parent, looks up the TCS by request ID, and calls `TrySetResult()`. +3. **Main thread** receives the response packet from the owning worker node, looks up the TCS by request ID, and calls `TrySetResult()`. 4. **Task thread** wakes up, retrieves the typed response, and returns it to the caller. ### Cancellation Semantics @@ -73,22 +73,22 @@ sequenceDiagram The callback wait intentionally does **not** check `_taskCancelledEvent`. This aligns with how in-process `TaskHost` (regular worker node mode) handles callbacks: - In regular mode, `IBuildEngine` callbacks are direct method calls that always complete. Cancellation never interrupts a callback mid-flight. Instead, cancellation causes the *work behind* the callback to fail fast (e.g., the scheduler cancels a child build started by `BuildProjectFile`), and the callback returns normally with a failure result. -- In TaskHost mode, the parent continues processing callback requests even after sending `TaskHostTaskCancelled`. The response is **guaranteed** to arrive because the parent's packet loop only exits upon receiving `TaskHostTaskComplete`, which cannot be sent until the task finishes, which cannot happen until the callback returns. +- In TaskHost mode, the owning worker node continues processing callback requests even after sending `TaskHostTaskCancelled`. The response is **guaranteed** to arrive because the worker node's packet loop only exits upon receiving `TaskHostTaskComplete`, which cannot be sent until the task finishes, which cannot happen until the callback returns. Cancellation is handled cooperatively: after the callback returns, the task checks its cancellation state (set by `ICancelableTask.Cancel()`) and exits. -The only exception path is connection loss (parent process killed), detected by `OnLinkStatusChanged` which fails all pending `TaskCompletionSource` entries with `InvalidOperationException`. This unblocks task threads immediately. +The only exception path is connection loss (owning worker node killed), detected by `OnLinkStatusChanged` which fails all pending `TaskCompletionSource` entries with `InvalidOperationException`. This unblocks task threads immediately. ### Response Guarantee (Why the Callback Cannot Deadlock) There is a causal dependency chain that prevents deadlock: ``` -Parent sends callback response +Worker node sends callback response → TaskHost callback returns → task finishes Execute() → TaskHost sends TaskHostTaskComplete - → parent exits packet loop + → worker node exits packet loop ``` -The parent cannot exit its packet loop without first receiving `TaskHostTaskComplete`. But `TaskHostTaskComplete` cannot be sent until the task finishes. And the task cannot finish while it is blocked waiting for a callback response. Therefore, the parent **must** process the callback request and send the response before it can ever stop. +The worker node cannot exit its packet loop without first receiving `TaskHostTaskComplete`. But `TaskHostTaskComplete` cannot be sent until the task finishes. And the task cannot finish while it is blocked waiting for a callback response. Therefore, the worker node **must** process the callback request and send the response before it can ever stop. diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs index dc9c80dcb4b..7cc503aff3c 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs @@ -146,7 +146,7 @@ public void IsRunningMultipleNodes_WorksWhenAutoEjectedInMultiThreadedMode(int m /// /// Verifies IsRunningMultipleNodes returns false (safe default) when callbacks are not enabled. - /// This simulates the cross-version scenario where a new TaskHost connects to an old parent. + /// This simulates the cross-version scenario where a new TaskHost connects to an old worker node. /// [Fact] public void IsRunningMultipleNodes_ReturnsFalseWhenCallbacksNotSupported() diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index b750beb920c..6d97d1dbc25 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -55,10 +55,10 @@ internal class OutOfProcTaskHostNode : /// process. Those are the variables that this dictionary should store. /// /// - The key into the dictionary is the name of the environment variable. - /// - The Key of the KeyValuePair is the value of the variable in the parent process -- the value that we + /// - The Key of the KeyValuePair is the value of the variable in the owning worker node process -- the value that we /// wish to ensure is replaced by whatever the correct value in our current process is. /// - The Value of the KeyValuePair is the value of the variable in the current process -- the value that - /// we wish to replay the Key value with in the environment that we receive from the parent before + /// we wish to replay the Key value with in the environment that we receive from the owning worker node before /// applying it to the current process. /// /// Note that either value in the KeyValuePair can be null, as it is completely possible to have an @@ -194,14 +194,14 @@ internal class OutOfProcTaskHostNode : private int _nextCallbackRequestId; /// - /// Pending callback requests awaiting responses from the parent. + /// Pending callback requests awaiting responses from the owning worker node. /// Key is the request ID, value is the TaskCompletionSource to signal when response arrives. /// private readonly ConcurrentDictionary> _pendingCallbackRequests = new(); /// - /// The packet version negotiated with the parent process. - /// Used to determine if the parent supports callback packets. + /// The packet version negotiated with the owning worker node. + /// Used to determine if the worker node supports callback packets. /// private byte _parentPacketVersion; @@ -212,8 +212,8 @@ internal class OutOfProcTaskHostNode : private const byte CallbacksMinPacketVersion = 3; /// - /// Whether the parent supports IBuildEngine callbacks. - /// True if the parent's packet version is high enough, or if the feature is force-enabled via env var. + /// Whether the owning worker node supports IBuildEngine callbacks. + /// True if the worker node's packet version is high enough, or if the feature is force-enabled via env var. /// private bool CallbacksSupported => _parentPacketVersion >= CallbacksMinPacketVersion @@ -311,8 +311,8 @@ public string ProjectFileOfTaskNode /// /// Implementation of IBuildEngine2.IsRunningMultipleNodes. - /// Queries the parent process and returns the actual value. - /// Returns false if the parent doesn't support callbacks (cross-version scenario). + /// Queries the owning worker node and returns the actual value. + /// Returns false if the worker node doesn't support callbacks (cross-version scenario). /// public bool IsRunningMultipleNodes { @@ -376,9 +376,9 @@ private bool WarningAsErrorNotOverriden(string warningCode) #region IBuildEngine Implementation (Methods) /// - /// Sends the provided error back to the parent node to be logged, tagging it with - /// the parent node's ID so that, as far as anyone is concerned, it might as well have - /// just come from the parent node to begin with. + /// Sends the provided error back to the owning worker node to be logged, tagging it with + /// the worker node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the worker node to begin with. /// public void LogErrorEvent(BuildErrorEventArgs e) { @@ -386,9 +386,9 @@ public void LogErrorEvent(BuildErrorEventArgs e) } /// - /// Sends the provided warning back to the parent node to be logged, tagging it with - /// the parent node's ID so that, as far as anyone is concerned, it might as well have - /// just come from the parent node to begin with. + /// Sends the provided warning back to the owning worker node to be logged, tagging it with + /// the worker node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the worker node to begin with. /// public void LogWarningEvent(BuildWarningEventArgs e) { @@ -396,9 +396,9 @@ public void LogWarningEvent(BuildWarningEventArgs e) } /// - /// Sends the provided message back to the parent node to be logged, tagging it with - /// the parent node's ID so that, as far as anyone is concerned, it might as well have - /// just come from the parent node to begin with. + /// Sends the provided message back to the owning worker node to be logged, tagging it with + /// the worker node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the worker node to begin with. /// public void LogMessageEvent(BuildMessageEventArgs e) { @@ -406,9 +406,9 @@ public void LogMessageEvent(BuildMessageEventArgs e) } /// - /// Sends the provided custom event back to the parent node to be logged, tagging it with - /// the parent node's ID so that, as far as anyone is concerned, it might as well have - /// just come from the parent node to begin with. + /// Sends the provided custom event back to the owning worker node to be logged, tagging it with + /// the worker node's ID so that, as far as anyone is concerned, it might as well have + /// just come from the worker node to begin with. /// public void LogCustomEvent(CustomBuildEventArgs e) { @@ -817,7 +817,7 @@ private void HandleCallbackResponse(INodePacket packet) } /// - /// Sends a callback request packet to the parent and waits for the corresponding response. + /// Sends a callback request packet to the owning worker node and waits for the corresponding response. /// This is called from task threads and blocks until the response arrives on the main thread. /// /// The expected response packet type. @@ -830,7 +830,7 @@ private void HandleCallbackResponse(INodePacket packet) /// /// We intentionally do NOT check _taskCancelledEvent here. This aligns with in-process /// mode where IBuildEngine callbacks are direct method calls that complete regardless of - /// cancellation state. The parent continues processing callback requests even after + /// cancellation state. The owning worker node continues processing callback requests even after /// sending TaskHostTaskCancelled, so the response will arrive. Cancellation is handled /// cooperatively via ICancelableTask.Cancel() on the task itself. /// @@ -848,7 +848,7 @@ private TResponse SendCallbackRequestAndWaitForResponse(ITaskHostCall try { - // Send the request packet to the parent + // Send the request packet to the owning worker node _nodeEndpoint.SendData(request); // Block until the response arrives (via HandleCallbackResponse → TCS.SetResult) @@ -1047,7 +1047,7 @@ private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) if (_pendingCallbackRequests.TryRemove(kvp.Key, out TaskCompletionSource tcs)) { tcs.TrySetException(new InvalidOperationException( - "TaskHost lost connection to parent process during callback.")); + "TaskHost lost connection to owning worker node during callback.")); } } #endif @@ -1073,7 +1073,7 @@ private void RunTask(object state) TaskHostConfiguration taskConfiguration = state as TaskHostConfiguration; IDictionary taskParams = taskConfiguration.TaskParameters; - // We only really know the values of these variables for sure once we see what we received from our parent + // We only really know the values of these variables for sure once we see what we received from the owning worker node // environment -- otherwise if this was a completely new build, we could lose out on expected environment // variables. _debugCommunications = taskConfiguration.BuildProcessEnvironment.ContainsValueAndIsEqual("MSBUILDDEBUGCOMM", "1", StringComparison.OrdinalIgnoreCase); @@ -1197,7 +1197,7 @@ private void RunTask(object state) /// /// Set the environment for the task host -- includes possibly munging the given /// environment somewhat to account for expected environment differences between, - /// e.g. parent processes and task hosts of different bitnesses. + /// e.g. worker node processes and task hosts of different bitnesses. /// private void SetTaskHostEnvironment(IDictionary environment) { @@ -1271,9 +1271,9 @@ private IDictionary UpdateEnvironmentForMainNode(IDictionary> variable in s_mismatchedEnvironmentValues) { - // Since this is munging the property list for returning to the parent process, + // Since this is munging the property list for returning to the owning worker node process, // then the value we wish to replace is the one that is in this process, and the - // replacement value is the one that originally came from the parent process, + // replacement value is the one that originally came from the worker node process, // instead of the other way around. string oldValue = variable.Value.Value; string newValue = variable.Value.Key; diff --git a/src/Shared/ITaskHostCallbackPacket.cs b/src/Shared/ITaskHostCallbackPacket.cs index c8ad9cb9bb2..0e240509f9e 100644 --- a/src/Shared/ITaskHostCallbackPacket.cs +++ b/src/Shared/ITaskHostCallbackPacket.cs @@ -6,14 +6,14 @@ namespace Microsoft.Build.BackEnd /// /// Interface for TaskHost callback packets that require request/response correlation. /// Packets implementing this interface can be matched between requests sent from TaskHost - /// and responses received from the parent process. + /// and responses received from the owning worker node. /// internal interface ITaskHostCallbackPacket : INodePacket { /// /// Gets or sets the unique request ID for correlating requests with responses. /// This ID is assigned by the TaskHost when sending a request and echoed back - /// by the parent in the corresponding response. + /// by the owning worker node in the corresponding response. /// int RequestId { get; set; } } diff --git a/src/Shared/TaskHostConfiguration.cs b/src/Shared/TaskHostConfiguration.cs index 31732e30bba..168648efb56 100644 --- a/src/Shared/TaskHostConfiguration.cs +++ b/src/Shared/TaskHostConfiguration.cs @@ -19,7 +19,7 @@ namespace Microsoft.Build.BackEnd internal class TaskHostConfiguration : INodePacket { /// - /// The node id (of the parent node, to make the logging work out) + /// The node id (of the owning worker node, to make the logging work out) /// private int _nodeId; diff --git a/src/Shared/TaskHostQueryRequest.cs b/src/Shared/TaskHostQueryRequest.cs index 450d84ba113..9ffc9c5a519 100644 --- a/src/Shared/TaskHostQueryRequest.cs +++ b/src/Shared/TaskHostQueryRequest.cs @@ -4,7 +4,7 @@ namespace Microsoft.Build.BackEnd { /// - /// Packet sent from TaskHost to parent to query simple build engine state. + /// Packet sent from TaskHost to owning worker node to query simple build engine state. /// internal class TaskHostQueryRequest : INodePacket, ITaskHostCallbackPacket { diff --git a/src/Shared/TaskHostQueryResponse.cs b/src/Shared/TaskHostQueryResponse.cs index edc91652750..c1e95884e21 100644 --- a/src/Shared/TaskHostQueryResponse.cs +++ b/src/Shared/TaskHostQueryResponse.cs @@ -4,7 +4,7 @@ namespace Microsoft.Build.BackEnd { /// - /// Response packet from parent to TaskHost for query requests. + /// Response packet from owning worker node to TaskHost for query requests. /// internal class TaskHostQueryResponse : INodePacket, ITaskHostCallbackPacket { diff --git a/src/Shared/TaskHostTaskComplete.cs b/src/Shared/TaskHostTaskComplete.cs index 6bded722522..8255ca19865 100644 --- a/src/Shared/TaskHostTaskComplete.cs +++ b/src/Shared/TaskHostTaskComplete.cs @@ -47,7 +47,7 @@ internal enum TaskCompleteType } /// - /// TaskHostTaskComplete contains all the information the parent node + /// TaskHostTaskComplete contains all the information the owning worker node /// needs from the task host on completion of task execution. /// internal class TaskHostTaskComplete : INodePacket From a1aed7736e61b42a7ba50d387150987bc35f30ec Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 17:33:54 +0100 Subject: [PATCH 21/34] Remove accidentally committed session state --- .../plan.md | 82 ------------------- 1 file changed, 82 deletions(-) delete mode 100644 .copilot/session-state/30d6c980-aa15-4be2-ab99-d518a7b1a36a/plan.md diff --git a/.copilot/session-state/30d6c980-aa15-4be2-ab99-d518a7b1a36a/plan.md b/.copilot/session-state/30d6c980-aa15-4be2-ab99-d518a7b1a36a/plan.md deleted file mode 100644 index 8acd4167417..00000000000 --- a/.copilot/session-state/30d6c980-aa15-4be2-ab99-d518a7b1a36a/plan.md +++ /dev/null @@ -1,82 +0,0 @@ -# AR-May's Cross-Version Compatibility — Option B Implementation Plan - -## Problem -New .NET task host could send callback packets to old MSBuild node that doesn't understand them. - -## Solution: Option B with Traits.cs -- `PacketVersion` stays at `2` (safe for shipping) -- Callbacks gated by: `parentPacketVersion >= CallbacksMinVersion(3) || Traits.Instance.EnableTaskHostCallbacks` -- `Traits.cs` reads `MSBUILDENABLETASKHOSTCALLBACKS` env var (follows existing pattern) -- Tests set env var → `Traits` picks it up (auto-refreshed per test via `BuildEnvironmentState.s_runningTests`) -- When all stages are done: bump PacketVersion to 3, remove `Traits` escape hatch - -## Why Traits.cs -- **Established pattern**: Same approach as `EnableRarNode`, `ForceAllTasksOutOfProcToTaskHost`, etc. -- **Test-friendly**: `Traits.Instance` re-creates per test when `s_runningTests` is set, so env var changes are picked up automatically -- **Shared across assemblies**: `Traits` is in `Microsoft.Build.Framework`, accessible from both `MSBuild.exe` and `Microsoft.Build.dll` -- **Discoverable**: All MSBuild feature toggles live in one place - -## Implementation Details - -### 1. Traits.cs -**File**: `src/Framework/Traits.cs` - -Add to `Traits` class (next to `EnableRarNode`): -```csharp -/// -/// Enable IBuildEngine callbacks in the TaskHost process. -/// Temporary escape hatch until all callback stages are complete and PacketVersion is bumped. -/// -public readonly bool EnableTaskHostCallbacks = !string.IsNullOrEmpty( - Environment.GetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS")); -``` - -### 2. OutOfProcTaskHostNode changes -**File**: `src/MSBuild/OutOfProcTaskHostNode.cs` - -Add field + store in `Run()`: -```csharp -private byte _parentPacketVersion; -private const byte CallbacksMinPacketVersion = 3; -``` - -Add helper: -```csharp -private bool CallbacksSupported => - _parentPacketVersion >= CallbacksMinPacketVersion - || Traits.Instance.EnableTaskHostCallbacks; -``` - -Guard `IsRunningMultipleNodes`: -```csharp -if (!CallbacksSupported) return false; -``` - -### 3. Test changes -**File**: `src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs` - -Existing integration tests: Add `env.SetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS", "1")` - -New test: `IsRunningMultipleNodes_ReturnsFalseWhenCallbacksNotSupported` — no env var, verify `false` default - -### 4. Safe defaults when callbacks disabled -| Callback | Default | Rationale | -|----------|---------|-----------| -| `IsRunningMultipleNodes` | `false` | Same as pre-callback behavior | -| `RequestCores` (future) | `1` | Single core | -| `ReleaseCores` (future) | no-op | Nothing to release | -| `BuildProjectFile` (future) | throw | Task depends on result | - -## Workplan - -- [ ] Add `EnableTaskHostCallbacks` to `Traits.cs` -- [ ] Add `_parentPacketVersion` field and `CallbacksMinPacketVersion` const -- [ ] Add `CallbacksSupported` property -- [ ] Guard `IsRunningMultipleNodes` with `CallbacksSupported` check -- [ ] Update existing integration tests to set env var -- [ ] Add new test for graceful fallback (no env var → returns false) -- [ ] Restore `StringArrayWithNullsDoesNotCrashTaskHost` test (done locally) -- [ ] Commit and push -- [ ] Reply to AR-May's review comment explaining the approach - - From 49168b316a5cdc910873690f8406a580f02d5c16 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 17:42:02 +0100 Subject: [PATCH 22/34] Document TaskHost lifecycle: task reuse, state, and shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 'TaskHost Lifecycle' section to taskhost-threading.md covering: - Event loop cycle (idle → task → idle) - State reset vs persistence between tasks - Shutdown vs reuse decision (sidecar vs regular) - No idle timeout behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../multithreading/taskhost-threading.md | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/documentation/specs/multithreading/taskhost-threading.md b/documentation/specs/multithreading/taskhost-threading.md index 91df5b21758..688be27bd97 100644 --- a/documentation/specs/multithreading/taskhost-threading.md +++ b/documentation/specs/multithreading/taskhost-threading.md @@ -92,3 +92,45 @@ Worker node sends callback response ``` The worker node cannot exit its packet loop without first receiving `TaskHostTaskComplete`. But `TaskHostTaskComplete` cannot be sent until the task finishes. And the task cannot finish while it is blocked waiting for a callback response. Therefore, the worker node **must** process the callback request and send the response before it can ever stop. + +## TaskHost Lifecycle + +The TaskHost process can execute multiple tasks sequentially. After finishing one task, it returns to an idle state and waits for either a new task or a shutdown signal. + +### Event Loop Cycle + +```mermaid +stateDiagram-v2 + [*] --> Idle: Process starts, endpoint connects + Idle --> Running: TaskHostConfiguration packet arrives + Running --> Idle: CompleteTask() sends result, clears config + Idle --> Shutdown: NodeBuildComplete or connection loss + Running --> Shutdown: _taskCancelledEvent during idle transition + Shutdown --> [*]: HandleShutdown() exits +``` + +1. **Idle**: `WaitAny()` blocks on the four wait handles. No task thread exists. `_currentConfiguration` is null. +2. **TaskHostConfiguration arrives**: `HandleTaskHostConfiguration()` stores the config and spawns `_taskRunnerThread` to call `RunTask()`. The main thread immediately returns to `WaitAny()`. +3. **Task executes**: `RunTask()` sets up the environment, loads the task assembly, calls `task.Execute()`, collects output parameters, and packages the result into `_taskCompletePacket`. On completion (success or failure), it signals `_taskCompleteEvent`. +4. **CompleteTask()**: The main thread wakes on index 2, sends `_taskCompletePacket` to the owning worker node, and sets `_currentConfiguration = null`. The node is now idle again. +5. **Back to step 1**: The main thread loops back to `WaitAny()`, ready for another `TaskHostConfiguration` or a `NodeBuildComplete`. + +### State Between Tasks + +Each new `TaskHostConfiguration` carries a full environment snapshot, task parameters, and warning settings. The task runner thread resets per-task state at the start of `RunTask()`: + +**Reset per task:** `_isTaskExecuting`, `_currentConfiguration`, `_debugCommunications`, `_updateEnvironment`, `WarningsAsErrors`/`WarningsNotAsErrors`/`WarningsAsMessages`, `_fileAccessData` + +**Persists across tasks:** +- `s_mismatchedEnvironmentValues` (static) — environment variable fixups for bitness differences, computed once +- `_registeredTaskObjectCache` — task object cache with `Build` lifetime scope, disposed only at shutdown +- `_pendingCallbackRequests` / `_nextCallbackRequestId` — callback tracking (should be empty between tasks) + +### Shutdown vs. Reuse + +When the owning worker node sends `NodeBuildComplete`, `HandleNodeBuildComplete()` decides whether to exit or stay alive: + +- **Sidecar TaskHost** (`_nodeReuse = true`): Always sets `BuildCompleteReuse`. The sidecar process persists across builds, re-entering the `Run()` outer loop to accept new connections. +- **Regular TaskHost** (`_nodeReuse = false`): Sets `BuildCompleteReuse` only if `buildComplete.PrepareForReuse` is true **and** `Traits.Instance.EscapeHatches.ReuseTaskHostNodes` is enabled. Otherwise sets `BuildComplete` and the process exits. This avoids holding assembly locks on custom task DLLs between builds. + +There is **no idle timeout**. The `WaitAny()` call has no timeout parameter — the TaskHost waits indefinitely until it receives a shutdown signal or the connection drops. From 5e849a78b08671ba5503bcbf2b61982f64ebfa0e Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 17:44:20 +0100 Subject: [PATCH 23/34] Fix docs: task object cache is disposed per build, not per process HandleShutdown() runs DisposeCacheObjects(Build) on every Run() exit, including BuildCompleteReuse. A fresh cache is created on the next Run(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- documentation/specs/multithreading/taskhost-threading.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/specs/multithreading/taskhost-threading.md b/documentation/specs/multithreading/taskhost-threading.md index 688be27bd97..38debe81b51 100644 --- a/documentation/specs/multithreading/taskhost-threading.md +++ b/documentation/specs/multithreading/taskhost-threading.md @@ -121,9 +121,9 @@ Each new `TaskHostConfiguration` carries a full environment snapshot, task param **Reset per task:** `_isTaskExecuting`, `_currentConfiguration`, `_debugCommunications`, `_updateEnvironment`, `WarningsAsErrors`/`WarningsNotAsErrors`/`WarningsAsMessages`, `_fileAccessData` -**Persists across tasks:** -- `s_mismatchedEnvironmentValues` (static) — environment variable fixups for bitness differences, computed once -- `_registeredTaskObjectCache` — task object cache with `Build` lifetime scope, disposed only at shutdown +**Persists across tasks (within a single build):** +- `s_mismatchedEnvironmentValues` (static) — environment variable fixups for bitness differences, computed once per process +- `_registeredTaskObjectCache` — task object cache with `Build` lifetime scope, disposed at end of each build (in `HandleShutdown()`), recreated fresh on the next `Run()` call - `_pendingCallbackRequests` / `_nextCallbackRequestId` — callback tracking (should be empty between tasks) ### Shutdown vs. Reuse From a8cf5f3e5a2af5cdcfd700ab7f219b30d47b5362 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 17:46:32 +0100 Subject: [PATCH 24/34] Call out cancellation-aware callbacks as future opportunity Per review feedback: the IPC mechanism could support interrupting callbacks on cancellation (unlike in-process direct method calls). Document this as a conscious design choice, not an oversight. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- documentation/specs/multithreading/taskhost-threading.md | 2 ++ src/MSBuild/OutOfProcTaskHostNode.cs | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/documentation/specs/multithreading/taskhost-threading.md b/documentation/specs/multithreading/taskhost-threading.md index 38debe81b51..37116f3de74 100644 --- a/documentation/specs/multithreading/taskhost-threading.md +++ b/documentation/specs/multithreading/taskhost-threading.md @@ -77,6 +77,8 @@ The callback wait intentionally does **not** check `_taskCancelledEvent`. This a Cancellation is handled cooperatively: after the callback returns, the task checks its cancellation state (set by `ICancelableTask.Cancel()`) and exits. +> **Future opportunity:** Unlike in-process mode where callbacks are direct method calls that cannot be interrupted, the IPC-based callback mechanism *could* support cancellation-aware callbacks — for example, by failing the pending `TaskCompletionSource` when `_taskCancelledEvent` is signaled. This would let long-running callbacks like `BuildProjectFile` abort immediately on cancellation rather than waiting for the worker node to process and respond. This is not implemented today for consistency with in-process behavior, but the mechanism is in place if needed. + The only exception path is connection loss (owning worker node killed), detected by `OnLinkStatusChanged` which fails all pending `TaskCompletionSource` entries with `InvalidOperationException`. This unblocks task threads immediately. ### Response Guarantee (Why the Callback Cannot Deadlock) diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 6d97d1dbc25..5d8375abfe8 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -834,6 +834,11 @@ private void HandleCallbackResponse(INodePacket packet) /// sending TaskHostTaskCancelled, so the response will arrive. Cancellation is handled /// cooperatively via ICancelableTask.Cancel() on the task itself. /// + /// NOTE: Unlike in-process mode, the IPC mechanism here *could* support cancellation-aware + /// callbacks by failing the TCS when _taskCancelledEvent is signaled. This is a future + /// opportunity if we need to abort long-running callbacks (e.g. BuildProjectFile) immediately + /// on cancellation rather than waiting for the worker node to respond. + /// /// Connection loss is handled by OnLinkStatusChanged, which fails all pending TCS /// with InvalidOperationException, causing this method to throw immediately. /// From 7475320902cae2a95dd9db1e48c9f91eab9318fb Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 17:50:16 +0100 Subject: [PATCH 25/34] Split packet serialization tests into separate class Move pure unit tests (no I/O, no BuildManager) for TaskHostQueryRequest and TaskHostQueryResponse round-trip serialization into TaskHostCallbackPacket_Tests. Keep integration tests in TaskHostCallback_Tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/TaskHostCallbackPacket_Tests.cs | 51 +++++++++++++++++++ .../BackEnd/TaskHostCallback_Tests.cs | 48 ++--------------- 2 files changed, 54 insertions(+), 45 deletions(-) create mode 100644 src/Build.UnitTests/BackEnd/TaskHostCallbackPacket_Tests.cs diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallbackPacket_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallbackPacket_Tests.cs new file mode 100644 index 00000000000..ae21764df79 --- /dev/null +++ b/src/Build.UnitTests/BackEnd/TaskHostCallbackPacket_Tests.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Build.BackEnd; +using Shouldly; +using Xunit; + +namespace Microsoft.Build.UnitTests.BackEnd +{ + /// + /// Pure unit tests for TaskHost callback packet serialization. + /// No I/O or BuildManager — just round-trip translation. + /// + public class TaskHostCallbackPacket_Tests + { + [Fact] + public void TaskHostQueryRequest_RoundTrip_Serialization() + { + var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + request.RequestId = 42; + + ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); + request.Translate(writeTranslator); + + ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); + var deserialized = (TaskHostQueryRequest)TaskHostQueryRequest.FactoryForDeserialization(readTranslator); + + deserialized.Query.ShouldBe(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + deserialized.RequestId.ShouldBe(42); + deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryRequest); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TaskHostQueryResponse_RoundTrip_Serialization(bool boolResult) + { + var response = new TaskHostQueryResponse(123, boolResult); + + ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); + response.Translate(writeTranslator); + + ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); + var deserialized = (TaskHostQueryResponse)TaskHostQueryResponse.FactoryForDeserialization(readTranslator); + + deserialized.RequestId.ShouldBe(123); + deserialized.BoolResult.ShouldBe(boolResult); + deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryResponse); + } + } +} diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs index 7cc503aff3c..2512a96606c 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs @@ -13,8 +13,9 @@ namespace Microsoft.Build.UnitTests.BackEnd { /// - /// Tests for IBuildEngine callback support in TaskHost. - /// Covers packet serialization and end-to-end integration tests. + /// Integration tests for IBuildEngine callback support in TaskHost. + /// These tests use BuildManager to run real builds with TaskHostFactory. + /// For packet serialization tests, see . /// public class TaskHostCallback_Tests { @@ -25,47 +26,6 @@ public TaskHostCallback_Tests(ITestOutputHelper output) _output = output; } - #region Packet Serialization Tests - - [Fact] - public void TaskHostQueryRequest_RoundTrip_Serialization() - { - var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); - request.RequestId = 42; - - ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); - request.Translate(writeTranslator); - - ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); - var deserialized = (TaskHostQueryRequest)TaskHostQueryRequest.FactoryForDeserialization(readTranslator); - - deserialized.Query.ShouldBe(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); - deserialized.RequestId.ShouldBe(42); - deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryRequest); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void TaskHostQueryResponse_RoundTrip_Serialization(bool boolResult) - { - var response = new TaskHostQueryResponse(123, boolResult); - - ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); - response.Translate(writeTranslator); - - ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); - var deserialized = (TaskHostQueryResponse)TaskHostQueryResponse.FactoryForDeserialization(readTranslator); - - deserialized.RequestId.ShouldBe(123); - deserialized.BoolResult.ShouldBe(boolResult); - deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryResponse); - } - - #endregion - - #region IsRunningMultipleNodes Callback Tests - /// /// Verifies IsRunningMultipleNodes callback works when task is explicitly run in TaskHost via TaskHostFactory. /// @@ -177,7 +137,5 @@ public void IsRunningMultipleNodes_ReturnsFalseWhenCallbacksNotSupported() // IsRunningMultipleNodes should return false (safe default) even though MaxNodeCount=4 bool.Parse(projectInstance.GetPropertyValue("Result")).ShouldBe(false); } - - #endregion } } From 992f0fe828649db1029a51e2b9c12e547b7bf16c Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 17:52:27 +0100 Subject: [PATCH 26/34] Clarify IsRunningMultipleNodes is config-based, not runtime Add doc comments explaining that IsRunningMultipleNodes returns MaxNodeCount > 1, regardless of how many nodes are actually running. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs index 2512a96606c..6cf01cd80c4 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs @@ -28,10 +28,12 @@ public TaskHostCallback_Tests(ITestOutputHelper output) /// /// Verifies IsRunningMultipleNodes callback works when task is explicitly run in TaskHost via TaskHostFactory. + /// IsRunningMultipleNodes is configuration-based (MaxNodeCount > 1), not based on actual running nodes. + /// See TaskHost.IsRunningMultipleNodes: returns _host.BuildParameters.MaxNodeCount > 1 || _disableInprocNode. /// [Theory] - [InlineData(1, false)] // Single node build - [InlineData(4, true)] // Multi-node build + [InlineData(1, false)] // MaxNodeCount=1 → IsRunningMultipleNodes=false + [InlineData(4, true)] // MaxNodeCount=4 → IsRunningMultipleNodes=true (even with one project) public void IsRunningMultipleNodes_WorksWithExplicitTaskHostFactory(int maxNodeCount, bool expectedResult) { using TestEnvironment env = TestEnvironment.Create(_output); From 5f410a0a9e55d0fe62fd3f160d35f169dbf58b4b Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 18:24:27 +0100 Subject: [PATCH 27/34] Fix fallback test: assert MSB5022 error via logger, not OverallResult The test now uses MockLogger to verify the error is logged when callbacks are disabled, without relying on OverallResult (which doesn't reflect logged errors when the task returns true). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/TaskHostCallback_Tests.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs index 6cf01cd80c4..ebe1cebb8d0 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostCallback_Tests.cs @@ -107,11 +107,13 @@ public void IsRunningMultipleNodes_WorksWhenAutoEjectedInMultiThreadedMode(int m } /// - /// Verifies IsRunningMultipleNodes returns false (safe default) when callbacks are not enabled. - /// This simulates the cross-version scenario where a new TaskHost connects to an old worker node. + /// Verifies that accessing IsRunningMultipleNodes when callbacks are disabled + /// logs error MSB5022 (BuildEngineCallbacksInTaskHostUnsupported). + /// This preserves the pre-callback behavior where unsupported IBuildEngine + /// methods in TaskHost log an error. /// [Fact] - public void IsRunningMultipleNodes_ReturnsFalseWhenCallbacksNotSupported() + public void IsRunningMultipleNodes_LogsErrorWhenCallbacksNotSupported() { using TestEnvironment env = TestEnvironment.Create(_output); @@ -129,15 +131,14 @@ public void IsRunningMultipleNodes_ReturnsFalseWhenCallbacksNotSupported() TransientTestProjectWithFiles project = env.CreateTestProjectWithFiles(projectContents); ProjectInstance projectInstance = new(project.ProjectFile); + var logger = new MockLogger(_output); BuildResult buildResult = BuildManager.DefaultBuildManager.Build( - new BuildParameters { MaxNodeCount = 4, EnableNodeReuse = false }, + new BuildParameters { MaxNodeCount = 4, EnableNodeReuse = false, Loggers = [logger] }, new BuildRequestData(projectInstance, targetsToBuild: ["Test"])); - // Build should succeed — callbacks gracefully return safe defaults - buildResult.OverallResult.ShouldBe(BuildResultCode.Success); - - // IsRunningMultipleNodes should return false (safe default) even though MaxNodeCount=4 - bool.Parse(projectInstance.GetPropertyValue("Result")).ShouldBe(false); + // MSB5022 error should be logged — the callback was not forwarded + logger.ErrorCount.ShouldBeGreaterThan(0); + logger.FullLog.ShouldContain("MSB5022"); } } } From 20cd8635f36da4b6656fb088f75cfe806c6f7f8f Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 18:26:04 +0100 Subject: [PATCH 28/34] Cross-reference duplicate test tasks with explanatory comments Both exist because E2E tests need a separate .NET Core assembly while integration tests use the task compiled into the test DLL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs | 3 ++- .../ExampleNetTask/ExampleTask/CallbackTestTask.cs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs b/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs index 09d14959f2e..5e1382653ec 100644 --- a/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs +++ b/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs @@ -8,7 +8,8 @@ namespace Microsoft.Build.UnitTests.BackEnd { /// /// A simple task that queries IsRunningMultipleNodes from the build engine. - /// Used to test that IBuildEngine2 callbacks work correctly in the task host. + /// Used by TaskHostCallback_Tests for in-process integration tests. + /// See also CallbackTestTask in ExampleNetTask for cross-runtime E2E tests. /// public class IsRunningMultipleNodesTask : Task { diff --git a/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs b/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs index b7b62ae96db..bebdff64bfe 100644 --- a/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs +++ b/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs @@ -7,7 +7,10 @@ namespace NetTask { /// /// A simple task that queries IsRunningMultipleNodes via IBuildEngine2 callback. - /// Used to test that TaskHost callbacks work in the .NET Core TaskHost spawned from .NET Framework parent. + /// Used by NetTaskHost_E2E_Tests to test callbacks in the .NET Core TaskHost + /// spawned from a .NET Framework parent. + /// Functionally identical to IsRunningMultipleNodesTask in the test DLL, but must + /// be a separate assembly targeting .NET Core for cross-runtime E2E testing. /// public class CallbackTestTask : Microsoft.Build.Utilities.Task { From 2d182bb92cb0a1023332ae1732964fd1d670af49 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 18:35:14 +0100 Subject: [PATCH 29/34] Deduplicate test task: link IsRunningMultipleNodesTask into ExampleTask Remove CallbackTestTask.cs and link IsRunningMultipleNodesTask.cs into ExampleTask.csproj instead. Set ReferenceOutputAssembly=false on the ProjectReference to avoid CS0436. Remove redundant EmbeddedResource entry for global.json (already covered by TestAssets\**\* glob). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BackEnd/IsRunningMultipleNodesTask.cs | 4 +-- .../Microsoft.Build.Engine.UnitTests.csproj | 5 +-- .../ExampleTask/CallbackTestTask.cs | 33 ------------------- .../ExampleTask/ExampleTask.csproj | 4 +++ .../TestNetTaskCallback.csproj | 6 ++-- 5 files changed, 10 insertions(+), 42 deletions(-) delete mode 100644 src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs diff --git a/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs b/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs index 5e1382653ec..c15a09e2739 100644 --- a/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs +++ b/src/Build.UnitTests/BackEnd/IsRunningMultipleNodesTask.cs @@ -8,8 +8,8 @@ namespace Microsoft.Build.UnitTests.BackEnd { /// /// A simple task that queries IsRunningMultipleNodes from the build engine. - /// Used by TaskHostCallback_Tests for in-process integration tests. - /// See also CallbackTestTask in ExampleNetTask for cross-runtime E2E tests. + /// Used by TaskHostCallback_Tests (in-process) and NetTaskHost_E2E_Tests (cross-runtime). + /// The E2E project includes this file via linked compile to avoid duplication. /// public class IsRunningMultipleNodesTask : Task { diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 420872194a2..82278b9403b 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -127,7 +127,7 @@ - + @@ -145,9 +145,6 @@ PreserveNewest - - PreserveNewest - diff --git a/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs b/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs deleted file mode 100644 index bebdff64bfe..00000000000 --- a/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/CallbackTestTask.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Build.Framework; - -namespace NetTask -{ - /// - /// A simple task that queries IsRunningMultipleNodes via IBuildEngine2 callback. - /// Used by NetTaskHost_E2E_Tests to test callbacks in the .NET Core TaskHost - /// spawned from a .NET Framework parent. - /// Functionally identical to IsRunningMultipleNodesTask in the test DLL, but must - /// be a separate assembly targeting .NET Core for cross-runtime E2E testing. - /// - public class CallbackTestTask : Microsoft.Build.Utilities.Task - { - [Output] - public bool IsRunningMultipleNodes { get; set; } - - public override bool Execute() - { - if (BuildEngine is IBuildEngine2 engine2) - { - IsRunningMultipleNodes = engine2.IsRunningMultipleNodes; - Log.LogMessage(MessageImportance.High, $"IsRunningMultipleNodes = {IsRunningMultipleNodes}"); - return true; - } - - Log.LogError("BuildEngine does not implement IBuildEngine2"); - return false; - } - } -} diff --git a/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/ExampleTask.csproj b/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/ExampleTask.csproj index 7a5976334e9..d6b3ee0abeb 100644 --- a/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/ExampleTask.csproj +++ b/src/Build.UnitTests/TestAssets/ExampleNetTask/ExampleTask/ExampleTask.csproj @@ -14,4 +14,8 @@ + + + + diff --git a/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/TestNetTaskCallback.csproj b/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/TestNetTaskCallback.csproj index f74bbdd71a7..18d77820c79 100644 --- a/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/TestNetTaskCallback.csproj +++ b/src/Build.UnitTests/TestAssets/ExampleNetTask/TestNetTaskCallback/TestNetTaskCallback.csproj @@ -10,15 +10,15 @@ - + - + From c4988a5ed83a0d47e30910967425c9c526a17cec Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 18:35:57 +0100 Subject: [PATCH 30/34] E2E test: target TestTask directly, skip restore Per review: minimize 'real project build' stuff we aren't testing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build.UnitTests/NetTaskHost_E2E_Tests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs b/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs index c63588fcd02..2d5a69ac393 100644 --- a/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs +++ b/src/Build.UnitTests/NetTaskHost_E2E_Tests.cs @@ -151,7 +151,7 @@ public void NetTaskHost_CallbackIsRunningMultipleNodesTest() string testProjectPath = Path.Combine(TestAssetsRootPath, "ExampleNetTask", "TestNetTaskCallback", "TestNetTaskCallback.csproj"); - string testTaskOutput = RunnerUtilities.ExecBootstrapedMSBuild($"{testProjectPath} -restore -v:n -p:LatestDotNetCoreForMSBuild={RunnerUtilities.LatestDotNetCoreForMSBuild}", out bool successTestTask); + string testTaskOutput = RunnerUtilities.ExecBootstrapedMSBuild($"{testProjectPath} -v:n -p:LatestDotNetCoreForMSBuild={RunnerUtilities.LatestDotNetCoreForMSBuild} -t:TestTask", out bool successTestTask); if (!successTestTask) { From 3124da5d553ff95da9a5e0684a7957d2e9d834a4 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 18:51:37 +0100 Subject: [PATCH 31/34] Use InternalErrorException for unknown query type Surfaces as 'report this as an MSBuild bug' via IsCriticalException path, which is appropriate since unknown query types indicate an internal version mismatch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build/Instance/TaskFactories/TaskHostTask.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 3ccead0ecc7..632582d6569 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -661,7 +661,7 @@ private void HandleQueryRequest(TaskHostQueryRequest request) { TaskHostQueryRequest.QueryType.IsRunningMultipleNodes => _buildEngine is IBuildEngine2 engine2 && engine2.IsRunningMultipleNodes, - _ => throw new System.NotImplementedException($"Unknown TaskHost query type: {request.Query}") + _ => throw new InternalErrorException($"Unknown TaskHost query type: {request.Query}") }; var response = new TaskHostQueryResponse(request.RequestId, result); From e81359f18b2f64c7a5201b7d5e9ecee56113feb1 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Mon, 23 Feb 2026 19:30:06 +0100 Subject: [PATCH 32/34] Fix TaskHostLifecycle test: restore ExampleTask copy to output ReferenceOutputAssembly=false prevented ExampleTask.dll from being copied to the test output directory, breaking TaskHostLifecycle E2E tests that locate ExampleTask.dll relative to the test assembly. Revert to normal ProjectReference and suppress CS0436 instead (the warning is harmless since both types come from the same linked source). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj index 82278b9403b..9cad26fc463 100644 --- a/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj +++ b/src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj @@ -14,7 +14,7 @@ true - $(NoWarn);MSB3270 + $(NoWarn);MSB3270;CS0436 @@ -127,7 +127,7 @@ - + From efa132d4642248571719fab884b93121b89bd014 Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Tue, 24 Feb 2026 10:22:15 +0100 Subject: [PATCH 33/34] simplify: the only query is IsRunningMultipleNodes --- .../BackEnd/TaskHostCallbackPacket_Tests.cs | 19 +++++++-------- .../Instance/TaskFactories/TaskHostTask.cs | 20 ++++++---------- src/Build/Microsoft.Build.csproj | 4 ++-- src/MSBuild/MSBuild.csproj | 4 ++-- src/MSBuild/OutOfProcTaskHostNode.cs | 10 ++++---- src/Shared/INodePacket.cs | 8 +++---- ... TaskHostIsRunningMultipleNodesRequest.cs} | 24 ++++--------------- ...TaskHostIsRunningMultipleNodesResponse.cs} | 20 ++++++++-------- 8 files changed, 44 insertions(+), 65 deletions(-) rename src/Shared/{TaskHostQueryRequest.cs => TaskHostIsRunningMultipleNodesRequest.cs} (54%) rename src/Shared/{TaskHostQueryResponse.cs => TaskHostIsRunningMultipleNodesResponse.cs} (50%) diff --git a/src/Build.UnitTests/BackEnd/TaskHostCallbackPacket_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHostCallbackPacket_Tests.cs index ae21764df79..0cab3ab82a3 100644 --- a/src/Build.UnitTests/BackEnd/TaskHostCallbackPacket_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHostCallbackPacket_Tests.cs @@ -14,38 +14,37 @@ namespace Microsoft.Build.UnitTests.BackEnd public class TaskHostCallbackPacket_Tests { [Fact] - public void TaskHostQueryRequest_RoundTrip_Serialization() + public void TaskHostIsRunningMultipleNodesRequest_RoundTrip_Serialization() { - var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); + var request = new TaskHostIsRunningMultipleNodesRequest(); request.RequestId = 42; ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); request.Translate(writeTranslator); ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); - var deserialized = (TaskHostQueryRequest)TaskHostQueryRequest.FactoryForDeserialization(readTranslator); + var deserialized = (TaskHostIsRunningMultipleNodesRequest)TaskHostIsRunningMultipleNodesRequest.FactoryForDeserialization(readTranslator); - deserialized.Query.ShouldBe(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); deserialized.RequestId.ShouldBe(42); - deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryRequest); + deserialized.Type.ShouldBe(NodePacketType.TaskHostIsRunningMultipleNodesRequest); } [Theory] [InlineData(true)] [InlineData(false)] - public void TaskHostQueryResponse_RoundTrip_Serialization(bool boolResult) + public void TaskHostIsRunningMultipleNodesResponse_RoundTrip_Serialization(bool isRunningMultipleNodes) { - var response = new TaskHostQueryResponse(123, boolResult); + var response = new TaskHostIsRunningMultipleNodesResponse(123, isRunningMultipleNodes); ITranslator writeTranslator = TranslationHelpers.GetWriteTranslator(); response.Translate(writeTranslator); ITranslator readTranslator = TranslationHelpers.GetReadTranslator(); - var deserialized = (TaskHostQueryResponse)TaskHostQueryResponse.FactoryForDeserialization(readTranslator); + var deserialized = (TaskHostIsRunningMultipleNodesResponse)TaskHostIsRunningMultipleNodesResponse.FactoryForDeserialization(readTranslator); deserialized.RequestId.ShouldBe(123); - deserialized.BoolResult.ShouldBe(boolResult); - deserialized.Type.ShouldBe(NodePacketType.TaskHostQueryResponse); + deserialized.IsRunningMultipleNodes.ShouldBe(isRunningMultipleNodes); + deserialized.Type.ShouldBe(NodePacketType.TaskHostIsRunningMultipleNodesResponse); } } } diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index 632582d6569..e4589fa3fad 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -203,7 +203,7 @@ public TaskHostTask( (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.LogMessage, LogMessagePacket.FactoryForDeserialization, this); (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskHostTaskComplete, TaskHostTaskComplete.FactoryForDeserialization, this); (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, this); - (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskHostQueryRequest, TaskHostQueryRequest.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskHostIsRunningMultipleNodesRequest, TaskHostIsRunningMultipleNodesRequest.FactoryForDeserialization, this); _packetReceivedEvent = new AutoResetEvent(false); _receivedPackets = new ConcurrentQueue(); @@ -510,8 +510,8 @@ private void HandlePacket(INodePacket packet, out bool taskFinished) case NodePacketType.LogMessage: HandleLoggedMessage(packet as LogMessagePacket); break; - case NodePacketType.TaskHostQueryRequest: - HandleQueryRequest(packet as TaskHostQueryRequest); + case NodePacketType.TaskHostIsRunningMultipleNodesRequest: + HandleIsRunningMultipleNodesRequest(packet as TaskHostIsRunningMultipleNodesRequest); break; default: ErrorUtilities.ThrowInternalErrorUnreachable(); @@ -653,18 +653,12 @@ private void HandleLoggedMessage(LogMessagePacket logMessagePacket) } /// - /// Handle query requests from the TaskHost for simple build engine state. + /// Handle IsRunningMultipleNodes request from the TaskHost. /// - private void HandleQueryRequest(TaskHostQueryRequest request) + private void HandleIsRunningMultipleNodesRequest(TaskHostIsRunningMultipleNodesRequest request) { - bool result = request.Query switch - { - TaskHostQueryRequest.QueryType.IsRunningMultipleNodes - => _buildEngine is IBuildEngine2 engine2 && engine2.IsRunningMultipleNodes, - _ => throw new InternalErrorException($"Unknown TaskHost query type: {request.Query}") - }; - - var response = new TaskHostQueryResponse(request.RequestId, result); + bool result = _buildEngine is IBuildEngine2 engine2 && engine2.IsRunningMultipleNodes; + var response = new TaskHostIsRunningMultipleNodesResponse(request.RequestId, result); _taskHostProvider.SendData(_taskHostNodeKey, response); } diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 688f504fc55..a9812801ba5 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -123,8 +123,8 @@ - - + + diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index 25f2f0a6ba7..69617e9152b 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -118,8 +118,8 @@ - - + + diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index 5d8375abfe8..6d16f0abc37 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -247,7 +247,7 @@ public OutOfProcTaskHostNode() thisINodePacketFactory.RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this); #if !CLR2COMPATIBILITY - thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostQueryResponse, TaskHostQueryResponse.FactoryForDeserialization, this); + thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostIsRunningMultipleNodesResponse, TaskHostIsRunningMultipleNodesResponse.FactoryForDeserialization, this); #endif #if !CLR2COMPATIBILITY @@ -328,9 +328,9 @@ public bool IsRunningMultipleNodes return false; } - var request = new TaskHostQueryRequest(TaskHostQueryRequest.QueryType.IsRunningMultipleNodes); - var response = SendCallbackRequestAndWaitForResponse(request); - return response.BoolResult; + var request = new TaskHostIsRunningMultipleNodesRequest(); + var response = SendCallbackRequestAndWaitForResponse(request); + return response.IsRunningMultipleNodes; #endif } } @@ -788,7 +788,7 @@ private void HandlePacket(INodePacket packet) #if !CLR2COMPATIBILITY // Callback response packet - route to pending request - case NodePacketType.TaskHostQueryResponse: + case NodePacketType.TaskHostIsRunningMultipleNodesResponse: HandleCallbackResponse(packet); break; #endif diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs index a89aa975ea8..56a9f856af8 100644 --- a/src/Shared/INodePacket.cs +++ b/src/Shared/INodePacket.cs @@ -254,14 +254,14 @@ internal enum NodePacketType : byte TaskHostResourceResponse = 0x23, /// - /// Request from TaskHost to parent for simple queries (e.g., IsRunningMultipleNodes). + /// Request from TaskHost to owning worker node for IsRunningMultipleNodes. /// - TaskHostQueryRequest = 0x24, + TaskHostIsRunningMultipleNodesRequest = 0x24, /// - /// Response from parent to TaskHost with query result. + /// Response from owning worker node to TaskHost with IsRunningMultipleNodes value. /// - TaskHostQueryResponse = 0x25, + TaskHostIsRunningMultipleNodesResponse = 0x25, /// /// Request from TaskHost to parent for Yield/Reacquire operations. diff --git a/src/Shared/TaskHostQueryRequest.cs b/src/Shared/TaskHostIsRunningMultipleNodesRequest.cs similarity index 54% rename from src/Shared/TaskHostQueryRequest.cs rename to src/Shared/TaskHostIsRunningMultipleNodesRequest.cs index 9ffc9c5a519..164335f29e0 100644 --- a/src/Shared/TaskHostQueryRequest.cs +++ b/src/Shared/TaskHostIsRunningMultipleNodesRequest.cs @@ -4,23 +4,17 @@ namespace Microsoft.Build.BackEnd { /// - /// Packet sent from TaskHost to owning worker node to query simple build engine state. + /// Packet sent from TaskHost to owning worker node to query IsRunningMultipleNodes. /// - internal class TaskHostQueryRequest : INodePacket, ITaskHostCallbackPacket + internal class TaskHostIsRunningMultipleNodesRequest : INodePacket, ITaskHostCallbackPacket { - private QueryType _queryType; private int _requestId; - public TaskHostQueryRequest() + public TaskHostIsRunningMultipleNodesRequest() { } - public TaskHostQueryRequest(QueryType queryType) - { - _queryType = queryType; - } - - public NodePacketType Type => NodePacketType.TaskHostQueryRequest; + public NodePacketType Type => NodePacketType.TaskHostIsRunningMultipleNodesRequest; public int RequestId { @@ -28,24 +22,16 @@ public int RequestId set => _requestId = value; } - public QueryType Query => _queryType; - public void Translate(ITranslator translator) { - translator.TranslateEnum(ref _queryType, (int)_queryType); translator.Translate(ref _requestId); } internal static INodePacket FactoryForDeserialization(ITranslator translator) { - var packet = new TaskHostQueryRequest(); + var packet = new TaskHostIsRunningMultipleNodesRequest(); packet.Translate(translator); return packet; } - - internal enum QueryType - { - IsRunningMultipleNodes = 0, - } } } diff --git a/src/Shared/TaskHostQueryResponse.cs b/src/Shared/TaskHostIsRunningMultipleNodesResponse.cs similarity index 50% rename from src/Shared/TaskHostQueryResponse.cs rename to src/Shared/TaskHostIsRunningMultipleNodesResponse.cs index c1e95884e21..47695d53a33 100644 --- a/src/Shared/TaskHostQueryResponse.cs +++ b/src/Shared/TaskHostIsRunningMultipleNodesResponse.cs @@ -4,24 +4,24 @@ namespace Microsoft.Build.BackEnd { /// - /// Response packet from owning worker node to TaskHost for query requests. + /// Response packet from owning worker node to TaskHost with the IsRunningMultipleNodes value. /// - internal class TaskHostQueryResponse : INodePacket, ITaskHostCallbackPacket + internal class TaskHostIsRunningMultipleNodesResponse : INodePacket, ITaskHostCallbackPacket { private int _requestId; - private bool _boolResult; + private bool _isRunningMultipleNodes; - public TaskHostQueryResponse() + public TaskHostIsRunningMultipleNodesResponse() { } - public TaskHostQueryResponse(int requestId, bool boolResult) + public TaskHostIsRunningMultipleNodesResponse(int requestId, bool isRunningMultipleNodes) { _requestId = requestId; - _boolResult = boolResult; + _isRunningMultipleNodes = isRunningMultipleNodes; } - public NodePacketType Type => NodePacketType.TaskHostQueryResponse; + public NodePacketType Type => NodePacketType.TaskHostIsRunningMultipleNodesResponse; public int RequestId { @@ -29,17 +29,17 @@ public int RequestId set => _requestId = value; } - public bool BoolResult => _boolResult; + public bool IsRunningMultipleNodes => _isRunningMultipleNodes; public void Translate(ITranslator translator) { translator.Translate(ref _requestId); - translator.Translate(ref _boolResult); + translator.Translate(ref _isRunningMultipleNodes); } internal static INodePacket FactoryForDeserialization(ITranslator translator) { - var packet = new TaskHostQueryResponse(); + var packet = new TaskHostIsRunningMultipleNodesResponse(); packet.Translate(translator); return packet; } From cf07e2b26c06cc795699b874d366f9c9bde4cfea Mon Sep 17 00:00:00 2001 From: Jan Provaznik Date: Tue, 24 Feb 2026 10:30:18 +0100 Subject: [PATCH 34/34] Use == '1' pattern for MSBUILDENABLETASKHOSTCALLBACKS Avoids footgun where setting the env var to '0' would still enable callbacks due to !string.IsNullOrEmpty check. Aligns with the == '1' pattern used by ForceAllTasksOutOfProcToTaskHost, InProcNodeDisabled, ReuseTaskHostNodes and other similar Traits flags. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Framework/Traits.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Framework/Traits.cs b/src/Framework/Traits.cs index 7ecd440249b..84fde7c36b5 100644 --- a/src/Framework/Traits.cs +++ b/src/Framework/Traits.cs @@ -134,7 +134,7 @@ public Traits() /// Enable IBuildEngine callbacks in the TaskHost process. /// Temporary escape hatch until all callback stages are complete and PacketVersion is bumped to 3. /// - public readonly bool EnableTaskHostCallbacks = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS")); + public readonly bool EnableTaskHostCallbacks = Environment.GetEnvironmentVariable("MSBUILDENABLETASKHOSTCALLBACKS") == "1"; /// /// Name of environment variables used to enable MSBuild server.