diff --git a/src/Build.UnitTests/BackEnd/TaskHost_Tests.cs b/src/Build.UnitTests/BackEnd/TaskHost_Tests.cs index 2e8503bf6ab..236894208af 100644 --- a/src/Build.UnitTests/BackEnd/TaskHost_Tests.cs +++ b/src/Build.UnitTests/BackEnd/TaskHost_Tests.cs @@ -546,6 +546,18 @@ public void IsRunningMultipleNodes4Nodes() Assert.True(_taskHost.IsRunningMultipleNodes); // "Expect IsRunningMultipleNodes to be true with 4 nodes" } + /// + /// Test that Yield and Reacquire can be called without throwing exceptions. + /// The mock request callback should handle these calls appropriately. + /// + [Fact] + public void TestYieldAndReacquire() + { + // Should not throw exceptions when called + _taskHost.Yield(); + _taskHost.Reacquire(); + } + #if FEATURE_CODETASKFACTORY /// /// Task logging after it's done should not crash us. diff --git a/src/Build/Instance/TaskFactories/TaskHostTask.cs b/src/Build/Instance/TaskFactories/TaskHostTask.cs index abb36afecd2..be9a73f1380 100644 --- a/src/Build/Instance/TaskFactories/TaskHostTask.cs +++ b/src/Build/Instance/TaskFactories/TaskHostTask.cs @@ -191,6 +191,8 @@ 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.TaskHostYield, TaskHostYield.FactoryForDeserialization, this); + (this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskHostReacquire, TaskHostReacquire.FactoryForDeserialization, this); _packetReceivedEvent = new AutoResetEvent(false); _receivedPackets = new ConcurrentQueue(); @@ -508,6 +510,12 @@ private void HandlePacket(INodePacket packet, out bool taskFinished) case NodePacketType.LogMessage: HandleLoggedMessage(packet as LogMessagePacket); break; + case NodePacketType.TaskHostYield: + HandleTaskHostYield(packet as TaskHostYield); + break; + case NodePacketType.TaskHostReacquire: + HandleTaskHostReacquire(packet as TaskHostReacquire); + break; default: ErrorUtilities.ThrowInternalErrorUnreachable(); break; @@ -647,6 +655,28 @@ private void HandleLoggedMessage(LogMessagePacket logMessagePacket) } } + /// + /// Handle yield notification from the task host. + /// The task host is indicating it's about to perform long-running operations + /// and the node can do other work. We acknowledge receipt immediately. + /// + private void HandleTaskHostYield(TaskHostYield yieldPacket) + { + // Send acknowledgment back to task host + _taskHostProvider.SendData(_taskHostNodeId, new TaskHostYield()); + } + + /// + /// Handle reacquire notification from the task host. + /// The task host wants to regain control after previously yielding. + /// We acknowledge receipt immediately. + /// + private void HandleTaskHostReacquire(TaskHostReacquire reacquirePacket) + { + // Send acknowledgment back to task host + _taskHostProvider.SendData(_taskHostNodeId, new TaskHostReacquire()); + } + /// /// 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 d53ecf9d743..2963b7034c7 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -100,6 +100,8 @@ + + diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index e43039e8e6c..844be4e073a 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -113,6 +113,8 @@ + + diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs index d141991394f..00e60c70a90 100644 --- a/src/MSBuild/OutOfProcTaskHostNode.cs +++ b/src/MSBuild/OutOfProcTaskHostNode.cs @@ -130,6 +130,16 @@ internal class OutOfProcTaskHostNode : /// private ManualResetEvent _taskCancelledEvent; + /// + /// The event which is set when the parent node acknowledges a yield request. + /// + private AutoResetEvent _yieldAcknowledgedEvent; + + /// + /// The event which is set when the parent node acknowledges a reacquire request. + /// + private AutoResetEvent _reacquireAcknowledgedEvent; + /// /// The thread currently executing user task in the TaskRunner /// @@ -195,6 +205,8 @@ public OutOfProcTaskHostNode() _shutdownEvent = new ManualResetEvent(false); _taskCompleteEvent = new AutoResetEvent(false); _taskCancelledEvent = new ManualResetEvent(false); + _yieldAcknowledgedEvent = new AutoResetEvent(false); + _reacquireAcknowledgedEvent = new AutoResetEvent(false); _packetFactory = new NodePacketFactory(); @@ -203,6 +215,8 @@ public OutOfProcTaskHostNode() thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostConfiguration, TaskHostConfiguration.FactoryForDeserialization, this); thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostTaskCancelled, TaskHostTaskCancelled.FactoryForDeserialization, this); thisINodePacketFactory.RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this); + thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostYield, TaskHostYield.FactoryForDeserialization, this); + thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostReacquire, TaskHostReacquire.FactoryForDeserialization, this); #if !CLR2COMPATIBILITY EngineServices = new EngineServicesImpl(this); @@ -405,21 +419,36 @@ public BuildEngineResult BuildProjectFilesInParallel(string[] projectFileNames, } /// - /// Stub implementation of IBuildEngine3.Yield. The task host does not support yielding, so just go ahead and silently - /// return, letting the task continue. + /// Implementation of IBuildEngine3.Yield. Sends a yield packet to the parent node + /// indicating that the task host is yielding control and the node can do other work + /// while the task performs long-running out-of-process operations. /// public void Yield() { - return; + if (_nodeEndpoint?.LinkStatus == LinkStatus.Active) + { + TaskHostYield yieldPacket = new TaskHostYield(); + _nodeEndpoint.SendData(yieldPacket); + + // Wait for the parent node to acknowledge the yield + _yieldAcknowledgedEvent.WaitOne(); + } } /// - /// Stub implementation of IBuildEngine3.Reacquire. The task host does not support yielding, so just go ahead and silently - /// return, letting the task continue. + /// Implementation of IBuildEngine3.Reacquire. Sends a reacquire packet to the parent node + /// indicating that the task host wants to regain control after previously yielding. /// public void Reacquire() { - return; + if (_nodeEndpoint?.LinkStatus == LinkStatus.Active) + { + TaskHostReacquire reacquirePacket = new TaskHostReacquire(); + _nodeEndpoint.SendData(reacquirePacket); + + // Wait for the parent node to acknowledge the reacquire + _reacquireAcknowledgedEvent.WaitOne(); + } } #endregion // IBuildEngine3 Implementation @@ -725,6 +754,14 @@ private void HandlePacket(INodePacket packet) case NodePacketType.NodeBuildComplete: HandleNodeBuildComplete(packet as NodeBuildComplete); break; + case NodePacketType.TaskHostYield: + // Acknowledgment from parent node + _yieldAcknowledgedEvent.Set(); + break; + case NodePacketType.TaskHostReacquire: + // Acknowledgment from parent node + _reacquireAcknowledgedEvent.Set(); + break; } } @@ -875,11 +912,15 @@ private NodeEngineShutdownReason HandleShutdown() _shutdownEvent.Close(); _taskCompleteEvent.Close(); _taskCancelledEvent.Close(); + _yieldAcknowledgedEvent.Close(); + _reacquireAcknowledgedEvent.Close(); #else _packetReceivedEvent.Dispose(); _shutdownEvent.Dispose(); _taskCompleteEvent.Dispose(); _taskCancelledEvent.Dispose(); + _yieldAcknowledgedEvent.Dispose(); + _reacquireAcknowledgedEvent.Dispose(); #endif return _shutdownReason; diff --git a/src/Shared/INodePacket.cs b/src/Shared/INodePacket.cs index 4aa870d7a09..e18ba35a90e 100644 --- a/src/Shared/INodePacket.cs +++ b/src/Shared/INodePacket.cs @@ -208,7 +208,6 @@ internal enum NodePacketType : byte /// ProcessReport, // 0x13 - /// Notifies the RAR node to set a configuration for a particular build. RarNodeEndpointConfiguration, @@ -222,6 +221,26 @@ internal enum NodePacketType : byte /// RarNodeExecuteResponse, // 0x15 + /// + /// Message sent from the task host to the parent node when a task + /// calls IBuildEngine3.Yield() to indicate it is yielding control + /// and the node can do other work. + /// + /// Contents: + /// (nothing) + /// + TaskHostYield, // 0x16 + + /// + /// Message sent from the task host to the parent node when a task + /// calls IBuildEngine3.Reacquire() to indicate it wants to regain + /// control after yielding. + /// + /// Contents: + /// (nothing) + /// + TaskHostReacquire, // 0x17 + // Reserve space for future core packet types (0x16-0x3B available for expansion) // Server command packets placed at end of safe range to maintain separation from core packets diff --git a/src/Shared/TaskHostReacquire.cs b/src/Shared/TaskHostReacquire.cs new file mode 100644 index 00000000000..3984630bac7 --- /dev/null +++ b/src/Shared/TaskHostReacquire.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace Microsoft.Build.BackEnd +{ + /// + /// TaskHostReacquire informs the parent node that the task host + /// wants to reacquire control after previously yielding via Yield(). + /// + internal class TaskHostReacquire : INodePacket + { + /// + /// Constructor + /// + public TaskHostReacquire() + { + } + + /// + /// The type of this NodePacket + /// + public NodePacketType Type + { + get { return NodePacketType.TaskHostReacquire; } + } + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(ITranslator translator) + { + // Do nothing -- this packet doesn't contain any parameters. + } + + /// + /// Factory for deserialization. + /// + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + TaskHostReacquire taskReacquire = new TaskHostReacquire(); + taskReacquire.Translate(translator); + return taskReacquire; + } + } +} diff --git a/src/Shared/TaskHostYield.cs b/src/Shared/TaskHostYield.cs new file mode 100644 index 00000000000..b100ae056e3 --- /dev/null +++ b/src/Shared/TaskHostYield.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +namespace Microsoft.Build.BackEnd +{ + /// + /// TaskHostYield informs the parent node that the task host is + /// yielding control and the node can do other work while the task + /// performs long-running out-of-process operations. + /// + internal class TaskHostYield : INodePacket + { + /// + /// Constructor + /// + public TaskHostYield() + { + } + + /// + /// The type of this NodePacket + /// + public NodePacketType Type + { + get { return NodePacketType.TaskHostYield; } + } + + /// + /// Translates the packet to/from binary form. + /// + /// The translator to use. + public void Translate(ITranslator translator) + { + // Do nothing -- this packet doesn't contain any parameters. + } + + /// + /// Factory for deserialization. + /// + internal static INodePacket FactoryForDeserialization(ITranslator translator) + { + TaskHostYield taskYield = new TaskHostYield(); + taskYield.Translate(translator); + return taskYield; + } + } +}