diff --git a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs
index d0856898473..b38a385928b 100644
--- a/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs
+++ b/src/Build/BackEnd/Components/Communications/NodeProviderOutOfProcTaskHost.cs
@@ -661,9 +661,6 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN
HandshakeOptions hostContext = nodeKey.HandshakeOptions;
- // If runtime host path is null it means we don't have MSBuild.dll path resolved and there is no need to include it in the command line arguments.
- string commandLineArgsPlaceholder = "\"{0}\" /nologo /nodemode:2 /nodereuse:{1} /low:{2} /parentpacketversion:{3} ";
-
// Generate a unique node ID for communication purposes using atomic increment.
int communicationNodeId = Interlocked.Increment(ref _nextNodeId);
@@ -682,10 +679,14 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN
var handshake = new Handshake(hostContext, predefinedToolsDirectory: msbuildAssemblyPath);
+ bool nodeReuse = NodeReuseIsEnabled(hostContext);
+ // nodemode:2 = Regular TaskHost (short-lived), nodemode:4 = Sidecar TaskHost (long-lived, with callback support)
+ int nodeMode = nodeReuse ? 4 : 2;
+
// There is always one task host per host context so we always create just 1 one task host node here.
nodeContexts = GetNodes(
runtimeHostPath,
- string.Format(commandLineArgsPlaceholder, Path.Combine(msbuildAssemblyPath, Constants.MSBuildAssemblyName), NodeReuseIsEnabled(hostContext), ComponentHost.BuildParameters.LowPriority, NodePacketTypeExtensions.PacketVersion),
+ $"\"{Path.Combine(msbuildAssemblyPath, Constants.MSBuildAssemblyName)}\" /nologo /nodemode:{nodeMode} /nodereuse:{nodeReuse} /low:{ComponentHost.BuildParameters.LowPriority} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion} ",
communicationNodeId,
this,
handshake,
@@ -707,9 +708,13 @@ internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, IN
CommunicationsUtilities.Trace("For a host context of {0}, spawning executable from {1}.", hostContext.ToString(), msbuildLocation ?? Constants.MSBuildExecutableName);
+ bool nodeReuseNonNet = NodeReuseIsEnabled(hostContext);
+ // nodemode:2 = Regular TaskHost (short-lived), nodemode:4 = Sidecar TaskHost (long-lived, with callback support)
+ int nodeModeNonNet = nodeReuseNonNet ? 4 : 2;
+
nodeContexts = GetNodes(
msbuildLocation,
- string.Format(commandLineArgsPlaceholder, string.Empty, NodeReuseIsEnabled(hostContext), ComponentHost.BuildParameters.LowPriority, NodePacketTypeExtensions.PacketVersion),
+ $"/nologo /nodemode:{nodeModeNonNet} /nodereuse:{nodeReuseNonNet} /low:{ComponentHost.BuildParameters.LowPriority} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion} ",
communicationNodeId,
this,
new Handshake(hostContext),
diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj
index 781af0730a3..4af23689b80 100644
--- a/src/MSBuild/MSBuild.csproj
+++ b/src/MSBuild/MSBuild.csproj
@@ -144,7 +144,9 @@
+
+
diff --git a/src/MSBuild/OutOfProcTaskHostNode.cs b/src/MSBuild/OutOfProcTaskHostNode.cs
index aa7afd2efa4..45dd54a936d 100644
--- a/src/MSBuild/OutOfProcTaskHostNode.cs
+++ b/src/MSBuild/OutOfProcTaskHostNode.cs
@@ -4,21 +4,8 @@
using System;
using System.Collections;
using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Reflection;
-using System.Threading;
using Microsoft.Build.BackEnd;
-using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
-#if !CLR2COMPATIBILITY
-using Microsoft.Build.Experimental.FileAccess;
-#endif
-using Microsoft.Build.Internal;
-using Microsoft.Build.Shared;
-#if FEATURE_APPDOMAIN
-using System.Runtime.Remoting;
-#endif
#nullable disable
@@ -26,248 +13,26 @@ namespace Microsoft.Build.CommandLine
{
///
/// This class represents an implementation of INode for out-of-proc node for hosting tasks.
+ /// This is the regular TaskHostFactory taskhost that does not support IBuildEngine callbacks.
+ /// For sidecar taskhosts with callback support, see .
///
- internal class OutOfProcTaskHostNode :
-#if FEATURE_APPDOMAIN
- MarshalByRefObject,
-#endif
- INodePacketFactory, INodePacketHandler,
-#if CLR2COMPATIBILITY
- IBuildEngine3
-#else
- IBuildEngine10
-#endif
+ internal sealed class OutOfProcTaskHostNode : OutOfProcTaskHostNodeBase
{
- ///
- /// Keeps a record of all environment variables that, on startup of the task host, have a different
- /// value from those that are passed to the task host in the configuration packet for the first task.
- /// These environments are assumed to be effectively identical, so the only difference between the
- /// two sets of values should be any environment variables that differ between e.g. a 32-bit and a 64-bit
- /// 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
- /// 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
- /// applying it to the current process.
- ///
- /// Note that either value in the KeyValuePair can be null, as it is completely possible to have an
- /// environment variable that is set in 32-bit processes but not in 64-bit, or vice versa.
- ///
- /// This dictionary must be static because otherwise, if a node is sitting around waiting for reuse, it will
- /// have inherited the environment from the previous build, and any differences between the two will be seen
- /// as "legitimate". There is no way for us to know what the differences between the startup environment of
- /// the previous build and the environment of the first task run in the task host in this build -- so we
- /// must assume that the 4ish system environment variables that this is really meant to catch haven't
- /// somehow magically changed between two builds spaced no more than 15 minutes apart.
- ///
- private static IDictionary> s_mismatchedEnvironmentValues;
-
- ///
- /// The endpoint used to talk to the host.
- ///
- private NodeEndpointOutOfProcTaskHost _nodeEndpoint;
-
- ///
- /// The packet factory.
- ///
- private NodePacketFactory _packetFactory;
-
- ///
- /// The event which is set when we receive packets.
- ///
- private AutoResetEvent _packetReceivedEvent;
-
- ///
- /// The queue of packets we have received but which have not yet been processed.
- ///
- private Queue _receivedPackets;
-
- ///
- /// The current configuration for this task host.
- ///
- private TaskHostConfiguration _currentConfiguration;
-
- ///
- /// The saved environment for the process.
- ///
- private IDictionary _savedEnvironment;
-
- ///
- /// The event which is set when we should shut down.
- ///
- private ManualResetEvent _shutdownEvent;
-
- ///
- /// The reason we are shutting down.
- ///
- private NodeEngineShutdownReason _shutdownReason;
-
- ///
- /// We set this flag to track a currently executing task
- ///
- private bool _isTaskExecuting;
-
- ///
- /// The event which is set when a task has completed.
- ///
- private AutoResetEvent _taskCompleteEvent;
-
- ///
- /// Packet containing all the information relating to the
- /// completed state of the task.
- ///
- private TaskHostTaskComplete _taskCompletePacket;
-
- ///
- /// Object used to synchronize access to taskCompletePacket
- ///
- private LockType _taskCompleteLock = new();
-
- ///
- /// The event which is set when a task is cancelled
- ///
- private ManualResetEvent _taskCancelledEvent;
-
- ///
- /// The thread currently executing user task in the TaskRunner
- ///
- private Thread _taskRunnerThread;
-
- ///
- /// This is the wrapper for the user task to be executed.
- /// We are providing a wrapper to create a possibility of executing the task in a separate AppDomain
- ///
- private OutOfProcTaskAppDomainWrapper _taskWrapper;
-
- ///
- /// Flag indicating if we should debug communications or not.
- ///
- private bool _debugCommunications;
-
- ///
- /// Flag indicating whether we should modify the environment based on any differences we find between that of the
- /// task host at startup and the environment passed to us in our initial task configuration packet.
- ///
- private bool _updateEnvironment;
-
- ///
- /// An interim step between MSBuildTaskHostDoNotUpdateEnvironment=1 and the default update behavior: go ahead and
- /// do all the updates that we would otherwise have done by default, but log any updates that are made (at low
- /// importance) so that the user is aware.
- ///
- private bool _updateEnvironmentAndLog;
-
- ///
- /// setting this to true means we're running a long-lived sidecar node.
- ///
- private bool _nodeReuse;
-
-#if !CLR2COMPATIBILITY
- ///
- /// The task object cache.
- ///
- private RegisteredTaskObjectCacheBase _registeredTaskObjectCache;
-#endif
-
-#if FEATURE_REPORTFILEACCESSES
- ///
- /// The file accesses reported by the most recently completed task.
- ///
- private List _fileAccessData = new List();
-#endif
-
///
/// Constructor.
///
public OutOfProcTaskHostNode()
+ : base()
{
- // We don't know what the current build thinks this variable should be until RunTask(), but as a fallback in case there are
- // communications before we get the configuration set up, just go with what was already in the environment from when this node
- // was initially launched.
- _debugCommunications = Traits.Instance.DebugNodeCommunication;
-
- _receivedPackets = new Queue();
-
- // These WaitHandles are disposed in HandleShutDown()
- _packetReceivedEvent = new AutoResetEvent(false);
- _shutdownEvent = new ManualResetEvent(false);
- _taskCompleteEvent = new AutoResetEvent(false);
- _taskCancelledEvent = new ManualResetEvent(false);
-
- _packetFactory = new NodePacketFactory();
-
- INodePacketFactory thisINodePacketFactory = (INodePacketFactory)this;
-
- thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostConfiguration, TaskHostConfiguration.FactoryForDeserialization, this);
- thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostTaskCancelled, TaskHostTaskCancelled.FactoryForDeserialization, this);
- thisINodePacketFactory.RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this);
-
-#if !CLR2COMPATIBILITY
- EngineServices = new EngineServicesImpl(this);
-#endif
- }
-
- #region IBuildEngine Implementation (Properties)
-
- ///
- /// Returns the value of ContinueOnError for the currently executing task.
- ///
- public bool ContinueOnError
- {
- get
- {
- ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
- return _currentConfiguration.ContinueOnError;
- }
- }
-
- ///
- /// Returns the line number of the location in the project file of the currently executing task.
- ///
- public int LineNumberOfTaskNode
- {
- get
- {
- ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
- return _currentConfiguration.LineNumberOfTask;
- }
- }
-
- ///
- /// Returns the column number of the location in the project file of the currently executing task.
- ///
- public int ColumnNumberOfTaskNode
- {
- get
- {
- ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
- return _currentConfiguration.ColumnNumberOfTask;
- }
}
- ///
- /// Returns the project file of the currently executing task.
- ///
- public string ProjectFileOfTaskNode
- {
- get
- {
- ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
- return _currentConfiguration.ProjectFileOfTask;
- }
- }
-
- #endregion // IBuildEngine Implementation (Properties)
-
#region IBuildEngine2 Implementation (Properties)
///
- /// Stub implementation of IBuildEngine2.IsRunningMultipleNodes. The task host does not support this sort of
- /// IBuildEngine callback, so error.
+ /// Stub implementation of IBuildEngine2.IsRunningMultipleNodes.
+ /// The regular task host does not support this callback, so log an error.
///
- public bool IsRunningMultipleNodes
+ public override bool IsRunningMultipleNodes
{
get
{
@@ -278,89 +43,13 @@ public bool IsRunningMultipleNodes
#endregion // IBuildEngine2 Implementation (Properties)
- #region IBuildEngine7 Implementation
- ///
- /// Enables or disables emitting a default error when a task fails without logging errors
- ///
- public bool AllowFailureWithoutError { get; set; } = false;
- #endregion
-
- #region IBuildEngine8 Implementation
-
- ///
- /// Contains all warnings that should be logged as errors.
- /// Non-null empty set when all warnings should be treated as errors.
- ///
- private ICollection WarningsAsErrors { get; set; }
-
- private ICollection WarningsNotAsErrors { get; set; }
-
- private ICollection WarningsAsMessages { get; set; }
-
- public bool ShouldTreatWarningAsError(string warningCode)
- {
- // Warnings as messages overrides warnings as errors.
- if (WarningsAsErrors == null || WarningsAsMessages?.Contains(warningCode) == true)
- {
- return false;
- }
-
- return (WarningsAsErrors.Count == 0 && WarningAsErrorNotOverriden(warningCode)) || WarningsAsMessages.Contains(warningCode);
- }
-
- private bool WarningAsErrorNotOverriden(string warningCode)
- {
- return WarningsNotAsErrors?.Contains(warningCode) != true;
- }
- #endregion
-
#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.
- ///
- public void LogErrorEvent(BuildErrorEventArgs e)
- {
- SendBuildEvent(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.
- ///
- public void LogWarningEvent(BuildWarningEventArgs e)
- {
- SendBuildEvent(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.
- ///
- public void LogMessageEvent(BuildMessageEventArgs e)
- {
- SendBuildEvent(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.
- ///
- public void LogCustomEvent(CustomBuildEventArgs e)
- {
- SendBuildEvent(e);
- }
-
- ///
- /// Stub implementation of IBuildEngine.BuildProjectFile. The task host does not support IBuildEngine
- /// callbacks for the purposes of building projects, so error.
+ /// Stub implementation of IBuildEngine.BuildProjectFile.
+ /// The regular task host does not support IBuildEngine callbacks, so log an error.
///
- public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs)
+ public override bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs)
{
LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
return false;
@@ -371,20 +60,20 @@ public bool BuildProjectFile(string projectFileName, string[] targetNames, IDict
#region IBuildEngine2 Implementation (Methods)
///
- /// Stub implementation of IBuildEngine2.BuildProjectFile. The task host does not support IBuildEngine
- /// callbacks for the purposes of building projects, so error.
+ /// Stub implementation of IBuildEngine2.BuildProjectFile.
+ /// The regular task host does not support IBuildEngine callbacks, so log an error.
///
- public bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion)
+ public override bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion)
{
LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
return false;
}
///
- /// Stub implementation of IBuildEngine2.BuildProjectFilesInParallel. The task host does not support IBuildEngine
- /// callbacks for the purposes of building projects, so error.
+ /// Stub implementation of IBuildEngine2.BuildProjectFilesInParallel.
+ /// The regular task host does not support IBuildEngine callbacks, so log an error.
///
- public bool BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IDictionary[] targetOutputsPerProject, string[] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion)
+ public override bool BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IDictionary[] targetOutputsPerProject, string[] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion)
{
LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
return false;
@@ -395,29 +84,29 @@ public bool BuildProjectFilesInParallel(string[] projectFileNames, string[] targ
#region IBuildEngine3 Implementation
///
- /// Stub implementation of IBuildEngine3.BuildProjectFilesInParallel. The task host does not support IBuildEngine
- /// callbacks for the purposes of building projects, so error.
+ /// Stub implementation of IBuildEngine3.BuildProjectFilesInParallel.
+ /// The regular task host does not support IBuildEngine callbacks, so log an error.
///
- public BuildEngineResult BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IList[] removeGlobalProperties, string[] toolsVersion, bool returnTargetOutputs)
+ public override BuildEngineResult BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IList[] removeGlobalProperties, string[] toolsVersion, bool returnTargetOutputs)
{
LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
return new BuildEngineResult(false, null);
}
///
- /// Stub implementation of IBuildEngine3.Yield. The task host does not support yielding, so just go ahead and silently
- /// return, letting the task continue.
+ /// Stub implementation of IBuildEngine3.Yield.
+ /// The regular task host does not support yielding, so silently return.
///
- public void Yield()
+ public override void Yield()
{
return;
}
///
- /// Stub implementation of IBuildEngine3.Reacquire. The task host does not support yielding, so just go ahead and silently
- /// return, letting the task continue.
+ /// Stub implementation of IBuildEngine3.Reacquire.
+ /// The regular task host does not support yielding, so silently return.
///
- public void Reacquire()
+ public override void Reacquire()
{
return;
}
@@ -425,872 +114,25 @@ public void Reacquire()
#endregion // IBuildEngine3 Implementation
#if !CLR2COMPATIBILITY
- #region IBuildEngine4 Implementation
-
- ///
- /// Registers an object with the system that will be disposed of at some specified time
- /// in the future.
- ///
- /// The key used to retrieve the object.
- /// The object to be held for later disposal.
- /// The lifetime of the object.
- /// The object may be disposed earlier that the requested time if
- /// MSBuild needs to reclaim memory.
- public void RegisterTaskObject(object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection)
- {
- _registeredTaskObjectCache.RegisterTaskObject(key, obj, lifetime, allowEarlyCollection);
- }
-
- ///
- /// Retrieves a previously registered task object stored with the specified key.
- ///
- /// The key used to retrieve the object.
- /// The lifetime of the object.
- ///
- /// The registered object, or null is there is no object registered under that key or the object
- /// has been discarded through early collection.
- ///
- public object GetRegisteredTaskObject(object key, RegisteredTaskObjectLifetime lifetime)
- {
- return _registeredTaskObjectCache.GetRegisteredTaskObject(key, lifetime);
- }
-
- ///
- /// Unregisters a previously-registered task object.
- ///
- /// The key used to retrieve the object.
- /// The lifetime of the object.
- ///
- /// The registered object, or null is there is no object registered under that key or the object
- /// has been discarded through early collection.
- ///
- public object UnregisterTaskObject(object key, RegisteredTaskObjectLifetime lifetime)
- {
- return _registeredTaskObjectCache.UnregisterTaskObject(key, lifetime);
- }
-
- #endregion
-
- #region IBuildEngine5 Implementation
-
- ///
- /// Logs a telemetry event.
- ///
- /// The event name.
- /// The list of properties associated with the event.
- public void LogTelemetry(string eventName, IDictionary properties)
- {
- SendBuildEvent(new TelemetryEventArgs
- {
- EventName = eventName,
- Properties = properties == null ? new Dictionary() : new Dictionary(properties),
- });
- }
-
- #endregion
-
- #region IBuildEngine6 Implementation
-
- ///
- /// Gets the global properties for the current project.
- ///
- /// An containing the global properties of the current project.
- public IReadOnlyDictionary GetGlobalProperties()
- {
- return new Dictionary(_currentConfiguration.GlobalProperties);
- }
-
- #endregion
-
#region IBuildEngine9 Implementation
- public int RequestCores(int requestedCores)
- {
- // No resource management in OOP nodes
- throw new NotImplementedException();
- }
-
- public void ReleaseCores(int coresToRelease)
- {
- // No resource management in OOP nodes
- throw new NotImplementedException();
- }
-
- #endregion
-
- #region IBuildEngine10 Members
-
- [Serializable]
- private sealed class EngineServicesImpl : EngineServices
- {
- private readonly OutOfProcTaskHostNode _taskHost;
-
- internal EngineServicesImpl(OutOfProcTaskHostNode taskHost)
- {
- _taskHost = taskHost;
- }
-
- ///
- /// No logging verbosity optimization in OOP nodes.
- ///
- public override bool LogsMessagesOfImportance(MessageImportance importance) => true;
-
- ///
- public override bool IsTaskInputLoggingEnabled
- {
- get
- {
- ErrorUtilities.VerifyThrow(_taskHost._currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
- return _taskHost._currentConfiguration.IsTaskInputLoggingEnabled;
- }
- }
-
-#if FEATURE_REPORTFILEACCESSES
- ///
- /// Reports a file access from a task.
- ///
- /// The file access to report.
- public void ReportFileAccess(FileAccessData fileAccessData)
- {
- _taskHost._fileAccessData.Add(fileAccessData);
- }
-#endif
- }
-
- public EngineServices EngineServices { get; }
-
- #endregion
-
-#endif
-
- #region INodePacketFactory Members
-
- ///
- /// Registers the specified handler for a particular packet type.
- ///
- /// The packet type.
- /// The factory for packets of the specified type.
- /// The handler to be called when packets of the specified type are received.
- public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler)
- {
- _packetFactory.RegisterPacketHandler(packetType, factory, handler);
- }
-
- ///
- /// Unregisters a packet handler.
- ///
- /// The packet type.
- public void UnregisterPacketHandler(NodePacketType packetType)
- {
- _packetFactory.UnregisterPacketHandler(packetType);
- }
-
///
- /// Takes a serializer, deserializes the packet and routes it to the appropriate handler.
+ /// The regular task host does not support resource management.
///
- /// The node from which the packet was received.
- /// The packet type.
- /// The translator containing the data from which the packet should be reconstructed.
- public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator)
+ public override int RequestCores(int requestedCores)
{
- _packetFactory.DeserializeAndRoutePacket(nodeId, packetType, translator);
- }
-
- ///
- /// Takes a serializer and deserializes the packet.
- ///
- /// The packet type.
- /// The translator containing the data from which the packet should be reconstructed.
- public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator)
- {
- return _packetFactory.DeserializePacket(packetType, translator);
- }
-
- ///
- /// Routes the specified packet
- ///
- /// The node from which the packet was received.
- /// The packet to route.
- public void RoutePacket(int nodeId, INodePacket packet)
- {
- _packetFactory.RoutePacket(nodeId, packet);
+ throw new NotImplementedException();
}
- #endregion // INodePacketFactory Members
-
- #region INodePacketHandler Members
-
///
- /// This method is invoked by the NodePacketRouter when a packet is received and is intended for
- /// this recipient.
+ /// The regular task host does not support resource management.
///
- /// The node from which the packet was received.
- /// The packet.
- public void PacketReceived(int node, INodePacket packet)
+ public override void ReleaseCores(int coresToRelease)
{
- lock (_receivedPackets)
- {
- _receivedPackets.Enqueue(packet);
- _packetReceivedEvent.Set();
- }
+ throw new NotImplementedException();
}
- #endregion // INodePacketHandler Members
-
- #region INode Members
-
- ///
- /// Starts up the node and processes messages until the node is requested to shut down.
- ///
- /// The exception which caused shutdown, if any.
- /// The reason for shutting down.
- public NodeEngineShutdownReason Run(out Exception shutdownException, bool nodeReuse = false, byte parentPacketVersion = 1)
- {
-#if !CLR2COMPATIBILITY
- _registeredTaskObjectCache = new RegisteredTaskObjectCacheBase();
-#endif
- shutdownException = null;
-
- // Snapshot the current environment
- _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables();
-
- _nodeReuse = nodeReuse;
- _nodeEndpoint = new NodeEndpointOutOfProcTaskHost(nodeReuse, parentPacketVersion);
- _nodeEndpoint.OnLinkStatusChanged += new LinkStatusChangedDelegate(OnLinkStatusChanged);
- _nodeEndpoint.Listen(this);
-
- WaitHandle[] waitHandles = [_shutdownEvent, _packetReceivedEvent, _taskCompleteEvent, _taskCancelledEvent];
-
- while (true)
- {
- int index = WaitHandle.WaitAny(waitHandles);
- switch (index)
- {
- case 0: // shutdownEvent
- NodeEngineShutdownReason shutdownReason = HandleShutdown();
- return shutdownReason;
-
- case 1: // packetReceivedEvent
- INodePacket packet = null;
-
- int packetCount = _receivedPackets.Count;
-
- while (packetCount > 0)
- {
- lock (_receivedPackets)
- {
- if (_receivedPackets.Count > 0)
- {
- packet = _receivedPackets.Dequeue();
- }
- else
- {
- break;
- }
- }
-
- if (packet != null)
- {
- HandlePacket(packet);
- }
- }
-
- break;
- case 2: // taskCompleteEvent
- CompleteTask();
- break;
- case 3: // taskCancelledEvent
- CancelTask();
- break;
- }
- }
-
- // UNREACHABLE
- }
#endregion
-
- ///
- /// Dispatches the packet to the correct handler.
- ///
- private void HandlePacket(INodePacket packet)
- {
- switch (packet.Type)
- {
- case NodePacketType.TaskHostConfiguration:
- HandleTaskHostConfiguration(packet as TaskHostConfiguration);
- break;
- case NodePacketType.TaskHostTaskCancelled:
- _taskCancelledEvent.Set();
- break;
- case NodePacketType.NodeBuildComplete:
- HandleNodeBuildComplete(packet as NodeBuildComplete);
- break;
- }
- }
-
- ///
- /// Configure the task host according to the information received in the
- /// configuration packet
- ///
- private void HandleTaskHostConfiguration(TaskHostConfiguration taskHostConfiguration)
- {
- ErrorUtilities.VerifyThrow(!_isTaskExecuting, "Why are we getting a TaskHostConfiguration packet while we're still executing a task?");
- _currentConfiguration = taskHostConfiguration;
-
- // Kick off the task running thread.
- _taskRunnerThread = new Thread(new ParameterizedThreadStart(RunTask));
- _taskRunnerThread.Name = "Task runner for task " + taskHostConfiguration.TaskName;
- _taskRunnerThread.Start(taskHostConfiguration);
- }
-
- ///
- /// The task has been completed
- ///
- private void CompleteTask()
- {
- ErrorUtilities.VerifyThrow(!_isTaskExecuting, "The task should be done executing before CompleteTask.");
- if (_nodeEndpoint.LinkStatus == LinkStatus.Active)
- {
- TaskHostTaskComplete taskCompletePacketToSend;
-
- lock (_taskCompleteLock)
- {
- ErrorUtilities.VerifyThrowInternalNull(_taskCompletePacket, "taskCompletePacket");
- taskCompletePacketToSend = _taskCompletePacket;
- _taskCompletePacket = null;
- }
-
- _nodeEndpoint.SendData(taskCompletePacketToSend);
- }
-
- _currentConfiguration = null;
-
- // If the task has been canceled, the event will still be set.
- // If so, now that we've completed the task, we want to shut down
- // this node -- with no reuse, since we don't know whether the
- // task we canceled left the node in a good state or not.
- if (_taskCancelledEvent.WaitOne(0))
- {
- _shutdownReason = NodeEngineShutdownReason.BuildComplete;
- _shutdownEvent.Set();
- }
- }
-
- ///
- /// This task has been cancelled. Attempt to cancel the task
- ///
- private void CancelTask()
- {
- // If the task is an ICancellable task in CLR4 we will call it here and wait for it to complete
- // Otherwise it's a classic ITask.
-
- // Store in a local to avoid a race
- var wrapper = _taskWrapper;
- if (wrapper?.CancelTask() == false)
- {
- // Create a possibility for the task to be aborted if the user really wants it dropped dead asap
- if (Environment.GetEnvironmentVariable("MSBUILDTASKHOSTABORTTASKONCANCEL") == "1")
- {
- // Don't bother aborting the task if it has passed the actual user task Execute()
- // It means we're already in the process of shutting down - Wait for the taskCompleteEvent to be set instead.
- if (_isTaskExecuting)
- {
-#if FEATURE_THREAD_ABORT
- // The thread will be terminated crudely so our environment may be trashed but it's ok since we are
- // shutting down ASAP.
- _taskRunnerThread.Abort();
-#endif
- }
- }
- }
- }
-
- ///
- /// Handles the NodeBuildComplete packet.
- ///
- private void HandleNodeBuildComplete(NodeBuildComplete buildComplete)
- {
- ErrorUtilities.VerifyThrow(!_isTaskExecuting, "We should never have a task in the process of executing when we receive NodeBuildComplete.");
-
- // Sidecar TaskHost will persist after the build is done.
- if (_nodeReuse)
- {
- _shutdownReason = NodeEngineShutdownReason.BuildCompleteReuse;
- }
- else
- {
- // TaskHostNodes lock assemblies with custom tasks produced by build scripts if NodeReuse is on. This causes failures if the user builds twice.
- _shutdownReason = buildComplete.PrepareForReuse && Traits.Instance.EscapeHatches.ReuseTaskHostNodes ? NodeEngineShutdownReason.BuildCompleteReuse : NodeEngineShutdownReason.BuildComplete;
- }
- _shutdownEvent.Set();
- }
-
- ///
- /// Perform necessary actions to shut down the node.
- ///
- private NodeEngineShutdownReason HandleShutdown()
- {
- // Wait for the RunTask task runner thread before shutting down so that we can cleanly dispose all WaitHandles.
- _taskRunnerThread?.Join();
-
- using StreamWriter debugWriter = _debugCommunications
- ? File.CreateText(string.Format(CultureInfo.CurrentCulture, Path.Combine(FileUtilities.TempFileDirectory, @"MSBuild_NodeShutdown_{0}.txt"), EnvironmentUtilities.CurrentProcessId))
- : null;
-
- debugWriter?.WriteLine("Node shutting down with reason {0}.", _shutdownReason);
-
-#if !CLR2COMPATIBILITY
- _registeredTaskObjectCache.DisposeCacheObjects(RegisteredTaskObjectLifetime.Build);
- _registeredTaskObjectCache = null;
-#endif
-
- // On Windows, a process holds a handle to the current directory,
- // so reset it away from a user-requested folder that may get deleted.
- NativeMethodsShared.SetCurrentDirectory(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory);
-
- // Restore the original environment, best effort.
- try
- {
- CommunicationsUtilities.SetEnvironment(_savedEnvironment);
- }
- catch (Exception ex)
- {
- debugWriter?.WriteLine("Failed to restore the original environment: {0}.", ex);
- }
-
- if (_nodeEndpoint.LinkStatus == LinkStatus.Active)
- {
- // Notify the BuildManager that we are done.
- _nodeEndpoint.SendData(new NodeShutdown(_shutdownReason == NodeEngineShutdownReason.Error ? NodeShutdownReason.Error : NodeShutdownReason.Requested));
-
- // Flush all packets to the pipe and close it down. This blocks until the shutdown is complete.
- _nodeEndpoint.OnLinkStatusChanged -= new LinkStatusChangedDelegate(OnLinkStatusChanged);
- }
-
- _nodeEndpoint.Disconnect();
-
- // Dispose these WaitHandles
-#if CLR2COMPATIBILITY
- _packetReceivedEvent.Close();
- _shutdownEvent.Close();
- _taskCompleteEvent.Close();
- _taskCancelledEvent.Close();
-#else
- _packetReceivedEvent.Dispose();
- _shutdownEvent.Dispose();
- _taskCompleteEvent.Dispose();
- _taskCancelledEvent.Dispose();
#endif
-
- return _shutdownReason;
- }
-
- ///
- /// Event handler for the node endpoint's LinkStatusChanged event.
- ///
- private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status)
- {
- switch (status)
- {
- case LinkStatus.ConnectionFailed:
- case LinkStatus.Failed:
- _shutdownReason = NodeEngineShutdownReason.ConnectionFailed;
- _shutdownEvent.Set();
- break;
-
- case LinkStatus.Inactive:
- break;
-
- default:
- break;
- }
- }
-
- ///
- /// Task runner method
- ///
- private void RunTask(object state)
- {
- _isTaskExecuting = true;
- OutOfProcTaskHostTaskResult taskResult = null;
- 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
- // 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);
- _updateEnvironment = !taskConfiguration.BuildProcessEnvironment.ContainsValueAndIsEqual("MSBuildTaskHostDoNotUpdateEnvironment", "1", StringComparison.OrdinalIgnoreCase);
- _updateEnvironmentAndLog = taskConfiguration.BuildProcessEnvironment.ContainsValueAndIsEqual("MSBuildTaskHostUpdateEnvironmentAndLog", "1", StringComparison.OrdinalIgnoreCase);
- WarningsAsErrors = taskConfiguration.WarningsAsErrors;
- WarningsNotAsErrors = taskConfiguration.WarningsNotAsErrors;
- WarningsAsMessages = taskConfiguration.WarningsAsMessages;
- try
- {
- // Change to the startup directory
- NativeMethodsShared.SetCurrentDirectory(taskConfiguration.StartupDirectory);
-
- if (_updateEnvironment)
- {
- InitializeMismatchedEnvironmentTable(taskConfiguration.BuildProcessEnvironment);
- }
-
- // Now set the new environment
- SetTaskHostEnvironment(taskConfiguration.BuildProcessEnvironment);
-
- // Set culture
- Thread.CurrentThread.CurrentCulture = taskConfiguration.Culture;
- Thread.CurrentThread.CurrentUICulture = taskConfiguration.UICulture;
-
- string taskName = taskConfiguration.TaskName;
- string taskLocation = taskConfiguration.TaskLocation;
-#if !CLR2COMPATIBILITY
- TaskFactoryUtilities.RegisterAssemblyResolveHandlersFromManifest(taskLocation);
-#endif
- // We will not create an appdomain now because of a bug
- // As a fix, we will create the class directly without wrapping it in a domain
- _taskWrapper = new OutOfProcTaskAppDomainWrapper();
-
- taskResult = _taskWrapper.ExecuteTask(
- this as IBuildEngine,
- taskName,
- taskLocation,
- taskConfiguration.ProjectFileOfTask,
- taskConfiguration.LineNumberOfTask,
- taskConfiguration.ColumnNumberOfTask,
- taskConfiguration.TargetName,
- taskConfiguration.ProjectFile,
-#if FEATURE_APPDOMAIN
- taskConfiguration.AppDomainSetup,
-#endif
-#if !NET35
- taskConfiguration.HostServices,
-#endif
- taskParams);
- }
- catch (ThreadAbortException)
- {
- // This thread was aborted as part of Cancellation, we will return a failure task result
- taskResult = new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure);
- }
- catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
- {
- taskResult = new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringExecution, e);
- }
- finally
- {
- try
- {
- _isTaskExecuting = false;
-
- IDictionary currentEnvironment = CommunicationsUtilities.GetEnvironmentVariables();
- currentEnvironment = UpdateEnvironmentForMainNode(currentEnvironment);
-
- taskResult ??= new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure);
-
- lock (_taskCompleteLock)
- {
- _taskCompletePacket = new TaskHostTaskComplete(
- taskResult,
-#if FEATURE_REPORTFILEACCESSES
- _fileAccessData,
-#endif
- currentEnvironment);
- }
-
-#if FEATURE_APPDOMAIN
- foreach (TaskParameter param in taskParams.Values)
- {
- // Tell remoting to forget connections to the parameter
- RemotingServices.Disconnect(param);
- }
-#endif
-
- // Restore the original clean environment
- CommunicationsUtilities.SetEnvironment(_savedEnvironment);
- }
- catch (Exception e)
- {
- lock (_taskCompleteLock)
- {
- // Create a minimal taskCompletePacket to carry the exception so that the TaskHostTask does not hang while waiting
- _taskCompletePacket = new TaskHostTaskComplete(
- new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedAfterExecution, e),
-#if FEATURE_REPORTFILEACCESSES
- _fileAccessData,
-#endif
- null);
- }
- }
- finally
- {
-#if FEATURE_REPORTFILEACCESSES
- _fileAccessData = new List();
-#endif
-
- // Call CleanupTask to unload any domains and other necessary cleanup in the taskWrapper
- _taskWrapper.CleanupTask();
-
- // The task has now fully completed executing
- _taskCompleteEvent.Set();
- }
- }
- }
-
- ///
- /// 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.
- ///
- private void SetTaskHostEnvironment(IDictionary environment)
- {
- ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues");
- IDictionary updatedEnvironment = null;
-
- if (_updateEnvironment)
- {
- foreach (KeyValuePair> variable in s_mismatchedEnvironmentValues)
- {
- string oldValue = variable.Value.Key;
- string newValue = variable.Value.Value;
-
- // We don't check the return value, because having the variable not exist == be
- // null is perfectly valid, and mismatchedEnvironmentValues stores those values
- // as null as well, so the String.Equals should still return that they are equal.
- string environmentValue = null;
- environment.TryGetValue(variable.Key, out environmentValue);
-
- if (String.Equals(environmentValue, oldValue, StringComparison.OrdinalIgnoreCase))
- {
- if (updatedEnvironment == null)
- {
- if (_updateEnvironmentAndLog)
- {
- LogMessageFromResource(MessageImportance.Low, "ModifyingTaskHostEnvironmentHeader");
- }
-
- updatedEnvironment = new Dictionary(environment, StringComparer.OrdinalIgnoreCase);
- }
-
- if (newValue != null)
- {
- if (_updateEnvironmentAndLog)
- {
- LogMessageFromResource(MessageImportance.Low, "ModifyingTaskHostEnvironmentVariable", variable.Key, newValue, environmentValue ?? String.Empty);
- }
-
- updatedEnvironment[variable.Key] = newValue;
- }
- else
- {
- updatedEnvironment.Remove(variable.Key);
- }
- }
- }
- }
-
- // if it's still null here, there were no changes necessary -- so just
- // set it to what was already passed in.
- if (updatedEnvironment == null)
- {
- updatedEnvironment = environment;
- }
-
- CommunicationsUtilities.SetEnvironment(updatedEnvironment);
- }
-
- ///
- /// Given the environment of the task host at the end of task execution, make sure that any
- /// processor-specific variables have been re-applied in the correct form for the main node,
- /// so that when we pass this dictionary back to the main node, all it should have to do
- /// is just set it.
- ///
- private IDictionary UpdateEnvironmentForMainNode(IDictionary environment)
- {
- ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues");
- IDictionary updatedEnvironment = null;
-
- if (_updateEnvironment)
- {
- foreach (KeyValuePair> variable in s_mismatchedEnvironmentValues)
- {
- // Since this is munging the property list for returning to the parent 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,
- // instead of the other way around.
- string oldValue = variable.Value.Value;
- string newValue = variable.Value.Key;
-
- // We don't check the return value, because having the variable not exist == be
- // null is perfectly valid, and mismatchedEnvironmentValues stores those values
- // as null as well, so the String.Equals should still return that they are equal.
- string environmentValue = null;
- environment.TryGetValue(variable.Key, out environmentValue);
-
- if (String.Equals(environmentValue, oldValue, StringComparison.OrdinalIgnoreCase))
- {
- updatedEnvironment ??= new Dictionary(environment, StringComparer.OrdinalIgnoreCase);
-
- if (newValue != null)
- {
- updatedEnvironment[variable.Key] = newValue;
- }
- else
- {
- updatedEnvironment.Remove(variable.Key);
- }
- }
- }
- }
-
- // if it's still null here, there were no changes necessary -- so just
- // set it to what was already passed in.
- if (updatedEnvironment == null)
- {
- updatedEnvironment = environment;
- }
-
- return updatedEnvironment;
- }
-
- ///
- /// Make sure the mismatchedEnvironmentValues table has been populated. Note that this should
- /// only do actual work on the very first run of a task in the task host -- otherwise, it should
- /// already have been populated.
- ///
- private void InitializeMismatchedEnvironmentTable(IDictionary environment)
- {
- if (s_mismatchedEnvironmentValues == null)
- {
- // This is the first time that we have received a TaskHostConfiguration packet, so we
- // need to construct the mismatched environment table based on our current environment
- // (assumed to be effectively identical to startup) and the environment we were given
- // via the task host configuration, assumed to be effectively identical to the startup
- // environment of the task host, given that the configuration packet is sent immediately
- // after the node is launched.
- s_mismatchedEnvironmentValues = new Dictionary>(StringComparer.OrdinalIgnoreCase);
-
- foreach (KeyValuePair variable in _savedEnvironment)
- {
- string oldValue = variable.Value;
- string newValue;
- if (!environment.TryGetValue(variable.Key, out newValue))
- {
- s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(null, oldValue);
- }
- else
- {
- if (!String.Equals(oldValue, newValue, StringComparison.OrdinalIgnoreCase))
- {
- s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(newValue, oldValue);
- }
- }
- }
-
- foreach (KeyValuePair variable in environment)
- {
- string newValue = variable.Value;
- string oldValue;
- if (!_savedEnvironment.TryGetValue(variable.Key, out oldValue))
- {
- s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(newValue, null);
- }
- else
- {
- if (!String.Equals(oldValue, newValue, StringComparison.OrdinalIgnoreCase))
- {
- s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(newValue, oldValue);
- }
- }
- }
- }
- }
-
- ///
- /// Sends the requested packet across to the main node.
- ///
- private void SendBuildEvent(BuildEventArgs e)
- {
- if (_nodeEndpoint?.LinkStatus == LinkStatus.Active)
- {
-#pragma warning disable SYSLIB0050
- // Types which are not serializable and are not IExtendedBuildEventArgs as
- // those always implement custom serialization by WriteToStream and CreateFromStream.
- if (!e.GetType().GetTypeInfo().IsSerializable && e is not IExtendedBuildEventArgs)
-#pragma warning disable SYSLIB0050
- {
- // log a warning and bail. This will end up re-calling SendBuildEvent, but we know for a fact
- // that the warning that we constructed is serializable, so everything should be good.
- LogWarningFromResource("ExpectedEventToBeSerializable", e.GetType().Name);
- return;
- }
-
- LogMessagePacketBase logMessage = new(new KeyValuePair(_currentConfiguration.NodeId, e));
- _nodeEndpoint.SendData(logMessage);
- }
- }
-
- ///
- /// Generates the message event corresponding to a particular resource string and set of args
- ///
- private void LogMessageFromResource(MessageImportance importance, string messageResource, params object[] messageArgs)
- {
- ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log messages!");
-
- // Using the CLR 2 build event because this class is shared between MSBuildTaskHost.exe (CLR2) and MSBuild.exe (CLR4+)
- BuildMessageEventArgs message = new BuildMessageEventArgs(
- ResourceUtilities.FormatString(AssemblyResources.GetString(messageResource), messageArgs),
- null,
- _currentConfiguration.TaskName,
- importance);
-
- LogMessageEvent(message);
- }
-
- ///
- /// Generates the error event corresponding to a particular resource string and set of args
- ///
- private void LogWarningFromResource(string messageResource, params object[] messageArgs)
- {
- ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log warnings!");
-
- // Using the CLR 2 build event because this class is shared between MSBuildTaskHost.exe (CLR2) and MSBuild.exe (CLR4+)
- BuildWarningEventArgs warning = new BuildWarningEventArgs(
- null,
- null,
- ProjectFileOfTaskNode,
- LineNumberOfTaskNode,
- ColumnNumberOfTaskNode,
- 0,
- 0,
- ResourceUtilities.FormatString(AssemblyResources.GetString(messageResource), messageArgs),
- null,
- _currentConfiguration.TaskName);
-
- LogWarningEvent(warning);
- }
-
- ///
- /// Generates the error event corresponding to a particular resource string and set of args
- ///
- private void LogErrorFromResource(string messageResource)
- {
- ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log errors!");
-
- // Using the CLR 2 build event because this class is shared between MSBuildTaskHost.exe (CLR2) and MSBuild.exe (CLR4+)
- BuildErrorEventArgs error = new BuildErrorEventArgs(
- null,
- null,
- ProjectFileOfTaskNode,
- LineNumberOfTaskNode,
- ColumnNumberOfTaskNode,
- 0,
- 0,
- AssemblyResources.GetString(messageResource),
- null,
- _currentConfiguration.TaskName);
-
- LogErrorEvent(error);
- }
}
}
diff --git a/src/MSBuild/OutOfProcTaskHostNodeBase.cs b/src/MSBuild/OutOfProcTaskHostNodeBase.cs
new file mode 100644
index 00000000000..47bf0483b08
--- /dev/null
+++ b/src/MSBuild/OutOfProcTaskHostNodeBase.cs
@@ -0,0 +1,1116 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+using Microsoft.Build.BackEnd;
+using Microsoft.Build.Execution;
+using Microsoft.Build.Framework;
+#if !CLR2COMPATIBILITY
+using Microsoft.Build.Experimental.FileAccess;
+#endif
+using Microsoft.Build.Internal;
+using Microsoft.Build.Shared;
+#if FEATURE_APPDOMAIN
+using System.Runtime.Remoting;
+#endif
+
+#nullable disable
+
+namespace Microsoft.Build.CommandLine
+{
+ ///
+ /// Base class for out-of-proc task host nodes. Contains shared functionality for both
+ /// regular TaskHostFactory taskhosts and long-lived Sidecar taskhosts.
+ ///
+ internal abstract class OutOfProcTaskHostNodeBase :
+#if FEATURE_APPDOMAIN
+ MarshalByRefObject,
+#endif
+ INodePacketFactory, INodePacketHandler,
+#if CLR2COMPATIBILITY
+ IBuildEngine3
+#else
+ IBuildEngine10
+#endif
+ {
+ ///
+ /// Keeps a record of all environment variables that, on startup of the task host, have a different
+ /// value from those that are passed to the task host in the configuration packet for the first task.
+ ///
+ private static IDictionary> s_mismatchedEnvironmentValues;
+
+ ///
+ /// The endpoint used to talk to the host.
+ ///
+ protected NodeEndpointOutOfProcTaskHost _nodeEndpoint;
+
+ ///
+ /// The packet factory.
+ ///
+ private NodePacketFactory _packetFactory;
+
+ ///
+ /// The event which is set when we receive packets.
+ ///
+ private AutoResetEvent _packetReceivedEvent;
+
+ ///
+ /// The queue of packets we have received but which have not yet been processed.
+ ///
+ private Queue _receivedPackets;
+
+ ///
+ /// The current configuration for this task host.
+ ///
+ protected TaskHostConfiguration _currentConfiguration;
+
+ ///
+ /// The saved environment for the process.
+ ///
+ private IDictionary _savedEnvironment;
+
+ ///
+ /// The event which is set when we should shut down.
+ ///
+ private ManualResetEvent _shutdownEvent;
+
+ ///
+ /// The reason we are shutting down.
+ ///
+ private NodeEngineShutdownReason _shutdownReason;
+
+ ///
+ /// We set this flag to track a currently executing task
+ ///
+ protected bool _isTaskExecuting;
+
+ ///
+ /// The event which is set when a task has completed.
+ ///
+ private AutoResetEvent _taskCompleteEvent;
+
+ ///
+ /// Packet containing all the information relating to the
+ /// completed state of the task.
+ ///
+ private TaskHostTaskComplete _taskCompletePacket;
+
+ ///
+ /// Object used to synchronize access to taskCompletePacket
+ ///
+ private LockType _taskCompleteLock = new();
+
+ ///
+ /// The event which is set when a task is cancelled
+ ///
+ protected ManualResetEvent _taskCancelledEvent;
+
+ ///
+ /// The thread currently executing user task in the TaskRunner
+ ///
+ private Thread _taskRunnerThread;
+
+ ///
+ /// This is the wrapper for the user task to be executed.
+ /// We are providing a wrapper to create a possibility of executing the task in a separate AppDomain
+ ///
+ private OutOfProcTaskAppDomainWrapper _taskWrapper;
+
+ ///
+ /// Flag indicating if we should debug communications or not.
+ ///
+ private bool _debugCommunications;
+
+ ///
+ /// Flag indicating whether we should modify the environment based on any differences we find between that of the
+ /// task host at startup and the environment passed to us in our initial task configuration packet.
+ ///
+ private bool _updateEnvironment;
+
+ ///
+ /// An interim step between MSBuildTaskHostDoNotUpdateEnvironment=1 and the default update behavior.
+ ///
+ private bool _updateEnvironmentAndLog;
+
+ ///
+ /// Setting this to true means we're running a long-lived sidecar node.
+ ///
+ protected bool _nodeReuse;
+
+#if !CLR2COMPATIBILITY
+ ///
+ /// The task object cache.
+ ///
+ private RegisteredTaskObjectCacheBase _registeredTaskObjectCache;
+#endif
+
+#if FEATURE_REPORTFILEACCESSES
+ ///
+ /// The file accesses reported by the most recently completed task.
+ ///
+ private List _fileAccessData = new List();
+#endif
+
+ ///
+ /// Constructor.
+ ///
+ protected OutOfProcTaskHostNodeBase()
+ {
+ _debugCommunications = Traits.Instance.DebugNodeCommunication;
+
+ _receivedPackets = new Queue();
+
+ // These WaitHandles are disposed in HandleShutDown()
+ _packetReceivedEvent = new AutoResetEvent(false);
+ _shutdownEvent = new ManualResetEvent(false);
+ _taskCompleteEvent = new AutoResetEvent(false);
+ _taskCancelledEvent = new ManualResetEvent(false);
+
+ _packetFactory = new NodePacketFactory();
+
+ INodePacketFactory thisINodePacketFactory = (INodePacketFactory)this;
+
+ thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostConfiguration, TaskHostConfiguration.FactoryForDeserialization, this);
+ thisINodePacketFactory.RegisterPacketHandler(NodePacketType.TaskHostTaskCancelled, TaskHostTaskCancelled.FactoryForDeserialization, this);
+ thisINodePacketFactory.RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, this);
+
+#if !CLR2COMPATIBILITY
+ EngineServices = new EngineServicesImpl(this);
+#endif
+ }
+
+ #region IBuildEngine Implementation (Properties)
+
+ ///
+ /// Returns the value of ContinueOnError for the currently executing task.
+ ///
+ public bool ContinueOnError
+ {
+ get
+ {
+ ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
+ return _currentConfiguration.ContinueOnError;
+ }
+ }
+
+ ///
+ /// Returns the line number of the location in the project file of the currently executing task.
+ ///
+ public int LineNumberOfTaskNode
+ {
+ get
+ {
+ ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
+ return _currentConfiguration.LineNumberOfTask;
+ }
+ }
+
+ ///
+ /// Returns the column number of the location in the project file of the currently executing task.
+ ///
+ public int ColumnNumberOfTaskNode
+ {
+ get
+ {
+ ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
+ return _currentConfiguration.ColumnNumberOfTask;
+ }
+ }
+
+ ///
+ /// Returns the project file of the currently executing task.
+ ///
+ public string ProjectFileOfTaskNode
+ {
+ get
+ {
+ ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
+ return _currentConfiguration.ProjectFileOfTask;
+ }
+ }
+
+ #endregion // IBuildEngine Implementation (Properties)
+
+ #region IBuildEngine2 Implementation (Properties)
+
+ ///
+ /// Gets whether we're running multiple nodes. Derived classes implement this differently.
+ ///
+ public abstract bool IsRunningMultipleNodes { get; }
+
+ #endregion // IBuildEngine2 Implementation (Properties)
+
+ #region IBuildEngine7 Implementation
+ ///
+ /// Enables or disables emitting a default error when a task fails without logging errors
+ ///
+ public bool AllowFailureWithoutError { get; set; } = false;
+ #endregion
+
+ #region IBuildEngine8 Implementation
+
+ ///
+ /// Contains all warnings that should be logged as errors.
+ ///
+ private ICollection WarningsAsErrors { get; set; }
+
+ private ICollection WarningsNotAsErrors { get; set; }
+
+ private ICollection WarningsAsMessages { get; set; }
+
+ public bool ShouldTreatWarningAsError(string warningCode)
+ {
+ if (WarningsAsErrors == null || WarningsAsMessages?.Contains(warningCode) == true)
+ {
+ return false;
+ }
+
+ return (WarningsAsErrors.Count == 0 && WarningAsErrorNotOverriden(warningCode)) || WarningsAsMessages.Contains(warningCode);
+ }
+
+ private bool WarningAsErrorNotOverriden(string warningCode)
+ {
+ return WarningsNotAsErrors?.Contains(warningCode) != true;
+ }
+ #endregion
+
+ #region IBuildEngine Implementation (Methods)
+
+ ///
+ /// Sends the provided error back to the parent node to be logged.
+ ///
+ public void LogErrorEvent(BuildErrorEventArgs e)
+ {
+ SendBuildEvent(e);
+ }
+
+ ///
+ /// Sends the provided warning back to the parent node to be logged.
+ ///
+ public void LogWarningEvent(BuildWarningEventArgs e)
+ {
+ SendBuildEvent(e);
+ }
+
+ ///
+ /// Sends the provided message back to the parent node to be logged.
+ ///
+ public void LogMessageEvent(BuildMessageEventArgs e)
+ {
+ SendBuildEvent(e);
+ }
+
+ ///
+ /// Sends the provided custom event back to the parent node to be logged.
+ ///
+ public void LogCustomEvent(CustomBuildEventArgs e)
+ {
+ SendBuildEvent(e);
+ }
+
+ ///
+ /// Implementation of IBuildEngine.BuildProjectFile. Derived classes implement this differently.
+ ///
+ public abstract bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs);
+
+ #endregion // IBuildEngine Implementation (Methods)
+
+ #region IBuildEngine2 Implementation (Methods)
+
+ ///
+ /// Implementation of IBuildEngine2.BuildProjectFile. Derived classes implement this differently.
+ ///
+ public abstract bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion);
+
+ ///
+ /// Implementation of IBuildEngine2.BuildProjectFilesInParallel. Derived classes implement this differently.
+ ///
+ public abstract bool BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IDictionary[] targetOutputsPerProject, string[] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion);
+
+ #endregion // IBuildEngine2 Implementation (Methods)
+
+ #region IBuildEngine3 Implementation
+
+ ///
+ /// Implementation of IBuildEngine3.BuildProjectFilesInParallel. Derived classes implement this differently.
+ ///
+ public abstract BuildEngineResult BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IList[] removeGlobalProperties, string[] toolsVersion, bool returnTargetOutputs);
+
+ ///
+ /// Implementation of IBuildEngine3.Yield. Derived classes implement this differently.
+ ///
+ public abstract void Yield();
+
+ ///
+ /// Implementation of IBuildEngine3.Reacquire. Derived classes implement this differently.
+ ///
+ public abstract void Reacquire();
+
+ #endregion // IBuildEngine3 Implementation
+
+#if !CLR2COMPATIBILITY
+ #region IBuildEngine4 Implementation
+
+ ///
+ /// Registers an object with the system that will be disposed of at some specified time in the future.
+ ///
+ public void RegisterTaskObject(object key, object obj, RegisteredTaskObjectLifetime lifetime, bool allowEarlyCollection)
+ {
+ _registeredTaskObjectCache.RegisterTaskObject(key, obj, lifetime, allowEarlyCollection);
+ }
+
+ ///
+ /// Retrieves a previously registered task object stored with the specified key.
+ ///
+ public object GetRegisteredTaskObject(object key, RegisteredTaskObjectLifetime lifetime)
+ {
+ return _registeredTaskObjectCache.GetRegisteredTaskObject(key, lifetime);
+ }
+
+ ///
+ /// Unregisters a previously-registered task object.
+ ///
+ public object UnregisterTaskObject(object key, RegisteredTaskObjectLifetime lifetime)
+ {
+ return _registeredTaskObjectCache.UnregisterTaskObject(key, lifetime);
+ }
+
+ #endregion
+
+ #region IBuildEngine5 Implementation
+
+ ///
+ /// Logs a telemetry event.
+ ///
+ public void LogTelemetry(string eventName, IDictionary properties)
+ {
+ SendBuildEvent(new TelemetryEventArgs
+ {
+ EventName = eventName,
+ Properties = properties == null ? new Dictionary() : new Dictionary(properties),
+ });
+ }
+
+ #endregion
+
+ #region IBuildEngine6 Implementation
+
+ ///
+ /// Gets the global properties for the current project.
+ ///
+ public IReadOnlyDictionary GetGlobalProperties()
+ {
+ return new Dictionary(_currentConfiguration.GlobalProperties);
+ }
+
+ #endregion
+
+ #region IBuildEngine9 Implementation
+
+ ///
+ /// Implementation of IBuildEngine9.RequestCores. Derived classes implement this differently.
+ ///
+ public abstract int RequestCores(int requestedCores);
+
+ ///
+ /// Implementation of IBuildEngine9.ReleaseCores. Derived classes implement this differently.
+ ///
+ public abstract void ReleaseCores(int coresToRelease);
+
+ #endregion
+
+ #region IBuildEngine10 Members
+
+ [Serializable]
+ private sealed class EngineServicesImpl : EngineServices
+ {
+ private readonly OutOfProcTaskHostNodeBase _taskHost;
+
+ internal EngineServicesImpl(OutOfProcTaskHostNodeBase taskHost)
+ {
+ _taskHost = taskHost;
+ }
+
+ ///
+ /// No logging verbosity optimization in OOP nodes.
+ ///
+ public override bool LogsMessagesOfImportance(MessageImportance importance) => true;
+
+ ///
+ public override bool IsTaskInputLoggingEnabled
+ {
+ get
+ {
+ ErrorUtilities.VerifyThrow(_taskHost._currentConfiguration != null, "We should never have a null configuration during a BuildEngine callback!");
+ return _taskHost._currentConfiguration.IsTaskInputLoggingEnabled;
+ }
+ }
+
+#if FEATURE_REPORTFILEACCESSES
+ ///
+ /// Reports a file access from a task.
+ ///
+ public void ReportFileAccess(FileAccessData fileAccessData)
+ {
+ _taskHost._fileAccessData.Add(fileAccessData);
+ }
+#endif
+ }
+
+ public EngineServices EngineServices { get; }
+
+ #endregion
+
+#endif
+
+ #region INodePacketFactory Members
+
+ ///
+ /// Registers the specified handler for a particular packet type.
+ ///
+ public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler)
+ {
+ _packetFactory.RegisterPacketHandler(packetType, factory, handler);
+ }
+
+ ///
+ /// Unregisters a packet handler.
+ ///
+ public void UnregisterPacketHandler(NodePacketType packetType)
+ {
+ _packetFactory.UnregisterPacketHandler(packetType);
+ }
+
+ ///
+ /// Takes a serializer, deserializes the packet and routes it to the appropriate handler.
+ ///
+ public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator)
+ {
+ _packetFactory.DeserializeAndRoutePacket(nodeId, packetType, translator);
+ }
+
+ ///
+ /// Takes a serializer and deserializes the packet.
+ ///
+ public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator)
+ {
+ return _packetFactory.DeserializePacket(packetType, translator);
+ }
+
+ ///
+ /// Routes the specified packet
+ ///
+ public void RoutePacket(int nodeId, INodePacket packet)
+ {
+ _packetFactory.RoutePacket(nodeId, packet);
+ }
+
+ #endregion // INodePacketFactory Members
+
+ #region INodePacketHandler Members
+
+ ///
+ /// This method is invoked by the NodePacketRouter when a packet is received and is intended for this recipient.
+ ///
+ public void PacketReceived(int node, INodePacket packet)
+ {
+ lock (_receivedPackets)
+ {
+ _receivedPackets.Enqueue(packet);
+ _packetReceivedEvent.Set();
+ }
+ }
+
+ #endregion // INodePacketHandler Members
+
+ #region INode Members
+
+ ///
+ /// Starts up the node and processes messages until the node is requested to shut down.
+ ///
+ public NodeEngineShutdownReason Run(out Exception shutdownException, bool nodeReuse = false, byte parentPacketVersion = 1)
+ {
+#if !CLR2COMPATIBILITY
+ _registeredTaskObjectCache = new RegisteredTaskObjectCacheBase();
+#endif
+ shutdownException = null;
+
+ // Snapshot the current environment
+ _savedEnvironment = CommunicationsUtilities.GetEnvironmentVariables();
+
+ _nodeReuse = nodeReuse;
+ _nodeEndpoint = new NodeEndpointOutOfProcTaskHost(nodeReuse, parentPacketVersion);
+ _nodeEndpoint.OnLinkStatusChanged += new LinkStatusChangedDelegate(OnLinkStatusChanged);
+ _nodeEndpoint.Listen(this);
+
+ WaitHandle[] waitHandles = [_shutdownEvent, _packetReceivedEvent, _taskCompleteEvent, _taskCancelledEvent];
+
+ while (true)
+ {
+ int index = WaitHandle.WaitAny(waitHandles);
+ switch (index)
+ {
+ case 0: // shutdownEvent
+ NodeEngineShutdownReason shutdownReason = HandleShutdown();
+ return shutdownReason;
+
+ case 1: // packetReceivedEvent
+ INodePacket packet = null;
+
+ int packetCount = _receivedPackets.Count;
+
+ while (packetCount > 0)
+ {
+ lock (_receivedPackets)
+ {
+ if (_receivedPackets.Count > 0)
+ {
+ packet = _receivedPackets.Dequeue();
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if (packet != null)
+ {
+ HandlePacket(packet);
+ }
+ }
+
+ break;
+ case 2: // taskCompleteEvent
+ CompleteTask();
+ break;
+ case 3: // taskCancelledEvent
+ CancelTask();
+ break;
+ }
+ }
+
+ // UNREACHABLE
+ }
+ #endregion
+
+ ///
+ /// Dispatches the packet to the correct handler. Derived classes can override to handle additional packet types.
+ ///
+ protected virtual void HandlePacket(INodePacket packet)
+ {
+ switch (packet.Type)
+ {
+ case NodePacketType.TaskHostConfiguration:
+ HandleTaskHostConfiguration(packet as TaskHostConfiguration);
+ break;
+ case NodePacketType.TaskHostTaskCancelled:
+ _taskCancelledEvent.Set();
+ break;
+ case NodePacketType.NodeBuildComplete:
+ HandleNodeBuildComplete(packet as NodeBuildComplete);
+ break;
+ }
+ }
+
+ ///
+ /// Configure the task host according to the information received in the configuration packet
+ ///
+ private void HandleTaskHostConfiguration(TaskHostConfiguration taskHostConfiguration)
+ {
+ ErrorUtilities.VerifyThrow(!_isTaskExecuting, "Why are we getting a TaskHostConfiguration packet while we're still executing a task?");
+ _currentConfiguration = taskHostConfiguration;
+
+ // Kick off the task running thread.
+ _taskRunnerThread = new Thread(new ParameterizedThreadStart(RunTask));
+ _taskRunnerThread.Name = "Task runner for task " + taskHostConfiguration.TaskName;
+ _taskRunnerThread.Start(taskHostConfiguration);
+ }
+
+ ///
+ /// The task has been completed
+ ///
+ private void CompleteTask()
+ {
+ ErrorUtilities.VerifyThrow(!_isTaskExecuting, "The task should be done executing before CompleteTask.");
+ if (_nodeEndpoint.LinkStatus == LinkStatus.Active)
+ {
+ TaskHostTaskComplete taskCompletePacketToSend;
+
+ lock (_taskCompleteLock)
+ {
+ ErrorUtilities.VerifyThrowInternalNull(_taskCompletePacket, "taskCompletePacket");
+ taskCompletePacketToSend = _taskCompletePacket;
+ _taskCompletePacket = null;
+ }
+
+ _nodeEndpoint.SendData(taskCompletePacketToSend);
+ }
+
+ _currentConfiguration = null;
+
+ // If the task has been canceled, the event will still be set.
+ if (_taskCancelledEvent.WaitOne(0))
+ {
+ _shutdownReason = NodeEngineShutdownReason.BuildComplete;
+ _shutdownEvent.Set();
+ }
+ }
+
+ ///
+ /// This task has been cancelled. Attempt to cancel the task
+ ///
+ private void CancelTask()
+ {
+ var wrapper = _taskWrapper;
+ if (wrapper?.CancelTask() == false)
+ {
+ if (Environment.GetEnvironmentVariable("MSBUILDTASKHOSTABORTTASKONCANCEL") == "1")
+ {
+ if (_isTaskExecuting)
+ {
+#if FEATURE_THREAD_ABORT
+ _taskRunnerThread.Abort();
+#endif
+ }
+ }
+ }
+ }
+
+ ///
+ /// Handles the NodeBuildComplete packet.
+ ///
+ private void HandleNodeBuildComplete(NodeBuildComplete buildComplete)
+ {
+ ErrorUtilities.VerifyThrow(!_isTaskExecuting, "We should never have a task in the process of executing when we receive NodeBuildComplete.");
+
+ // Sidecar TaskHost will persist after the build is done.
+ if (_nodeReuse)
+ {
+ _shutdownReason = NodeEngineShutdownReason.BuildCompleteReuse;
+ }
+ else
+ {
+ _shutdownReason = buildComplete.PrepareForReuse && Traits.Instance.EscapeHatches.ReuseTaskHostNodes ? NodeEngineShutdownReason.BuildCompleteReuse : NodeEngineShutdownReason.BuildComplete;
+ }
+ _shutdownEvent.Set();
+ }
+
+ ///
+ /// Perform necessary actions to shut down the node.
+ ///
+ private NodeEngineShutdownReason HandleShutdown()
+ {
+ _taskRunnerThread?.Join();
+
+ using StreamWriter debugWriter = _debugCommunications
+ ? File.CreateText(string.Format(CultureInfo.CurrentCulture, Path.Combine(FileUtilities.TempFileDirectory, @"MSBuild_NodeShutdown_{0}.txt"), EnvironmentUtilities.CurrentProcessId))
+ : null;
+
+ debugWriter?.WriteLine("Node shutting down with reason {0}.", _shutdownReason);
+
+#if !CLR2COMPATIBILITY
+ _registeredTaskObjectCache.DisposeCacheObjects(RegisteredTaskObjectLifetime.Build);
+ _registeredTaskObjectCache = null;
+#endif
+
+ NativeMethodsShared.SetCurrentDirectory(BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory);
+
+ try
+ {
+ CommunicationsUtilities.SetEnvironment(_savedEnvironment);
+ }
+ catch (Exception ex)
+ {
+ debugWriter?.WriteLine("Failed to restore the original environment: {0}.", ex);
+ }
+
+ if (_nodeEndpoint.LinkStatus == LinkStatus.Active)
+ {
+ _nodeEndpoint.SendData(new NodeShutdown(_shutdownReason == NodeEngineShutdownReason.Error ? NodeShutdownReason.Error : NodeShutdownReason.Requested));
+ _nodeEndpoint.OnLinkStatusChanged -= new LinkStatusChangedDelegate(OnLinkStatusChanged);
+ }
+
+ _nodeEndpoint.Disconnect();
+
+#if CLR2COMPATIBILITY
+ _packetReceivedEvent.Close();
+ _shutdownEvent.Close();
+ _taskCompleteEvent.Close();
+ _taskCancelledEvent.Close();
+#else
+ _packetReceivedEvent.Dispose();
+ _shutdownEvent.Dispose();
+ _taskCompleteEvent.Dispose();
+ _taskCancelledEvent.Dispose();
+#endif
+
+ return _shutdownReason;
+ }
+
+ ///
+ /// Event handler for the node endpoint's LinkStatusChanged event.
+ ///
+ private void OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status)
+ {
+ switch (status)
+ {
+ case LinkStatus.ConnectionFailed:
+ case LinkStatus.Failed:
+ _shutdownReason = NodeEngineShutdownReason.ConnectionFailed;
+ _shutdownEvent.Set();
+ break;
+
+ case LinkStatus.Inactive:
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ ///
+ /// Task runner method
+ ///
+ private void RunTask(object state)
+ {
+ _isTaskExecuting = true;
+ OutOfProcTaskHostTaskResult taskResult = null;
+ TaskHostConfiguration taskConfiguration = state as TaskHostConfiguration;
+ IDictionary taskParams = taskConfiguration.TaskParameters;
+
+ _debugCommunications = taskConfiguration.BuildProcessEnvironment.ContainsValueAndIsEqual("MSBUILDDEBUGCOMM", "1", StringComparison.OrdinalIgnoreCase);
+ _updateEnvironment = !taskConfiguration.BuildProcessEnvironment.ContainsValueAndIsEqual("MSBuildTaskHostDoNotUpdateEnvironment", "1", StringComparison.OrdinalIgnoreCase);
+ _updateEnvironmentAndLog = taskConfiguration.BuildProcessEnvironment.ContainsValueAndIsEqual("MSBuildTaskHostUpdateEnvironmentAndLog", "1", StringComparison.OrdinalIgnoreCase);
+ WarningsAsErrors = taskConfiguration.WarningsAsErrors;
+ WarningsNotAsErrors = taskConfiguration.WarningsNotAsErrors;
+ WarningsAsMessages = taskConfiguration.WarningsAsMessages;
+ try
+ {
+ NativeMethodsShared.SetCurrentDirectory(taskConfiguration.StartupDirectory);
+
+ if (_updateEnvironment)
+ {
+ InitializeMismatchedEnvironmentTable(taskConfiguration.BuildProcessEnvironment);
+ }
+
+ SetTaskHostEnvironment(taskConfiguration.BuildProcessEnvironment);
+
+ Thread.CurrentThread.CurrentCulture = taskConfiguration.Culture;
+ Thread.CurrentThread.CurrentUICulture = taskConfiguration.UICulture;
+
+ string taskName = taskConfiguration.TaskName;
+ string taskLocation = taskConfiguration.TaskLocation;
+#if !CLR2COMPATIBILITY
+ TaskFactoryUtilities.RegisterAssemblyResolveHandlersFromManifest(taskLocation);
+#endif
+ _taskWrapper = new OutOfProcTaskAppDomainWrapper();
+
+ taskResult = _taskWrapper.ExecuteTask(
+ this as IBuildEngine,
+ taskName,
+ taskLocation,
+ taskConfiguration.ProjectFileOfTask,
+ taskConfiguration.LineNumberOfTask,
+ taskConfiguration.ColumnNumberOfTask,
+ taskConfiguration.TargetName,
+ taskConfiguration.ProjectFile,
+#if FEATURE_APPDOMAIN
+ taskConfiguration.AppDomainSetup,
+#endif
+#if !NET35
+ taskConfiguration.HostServices,
+#endif
+ taskParams);
+ }
+ catch (ThreadAbortException)
+ {
+ taskResult = new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure);
+ }
+ catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
+ {
+ taskResult = new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedDuringExecution, e);
+ }
+ finally
+ {
+ try
+ {
+ _isTaskExecuting = false;
+
+ IDictionary currentEnvironment = CommunicationsUtilities.GetEnvironmentVariables();
+ currentEnvironment = UpdateEnvironmentForMainNode(currentEnvironment);
+
+ taskResult ??= new OutOfProcTaskHostTaskResult(TaskCompleteType.Failure);
+
+ lock (_taskCompleteLock)
+ {
+ _taskCompletePacket = new TaskHostTaskComplete(
+ taskResult,
+#if FEATURE_REPORTFILEACCESSES
+ _fileAccessData,
+#endif
+ currentEnvironment);
+ }
+
+#if FEATURE_APPDOMAIN
+ foreach (TaskParameter param in taskParams.Values)
+ {
+ RemotingServices.Disconnect(param);
+ }
+#endif
+
+ CommunicationsUtilities.SetEnvironment(_savedEnvironment);
+ }
+ catch (Exception e)
+ {
+ lock (_taskCompleteLock)
+ {
+ _taskCompletePacket = new TaskHostTaskComplete(
+ new OutOfProcTaskHostTaskResult(TaskCompleteType.CrashedAfterExecution, e),
+#if FEATURE_REPORTFILEACCESSES
+ _fileAccessData,
+#endif
+ null);
+ }
+ }
+ finally
+ {
+#if FEATURE_REPORTFILEACCESSES
+ _fileAccessData = new List();
+#endif
+
+ _taskWrapper.CleanupTask();
+ _taskCompleteEvent.Set();
+ }
+ }
+ }
+
+ ///
+ /// Set the environment for the task host.
+ ///
+ private void SetTaskHostEnvironment(IDictionary environment)
+ {
+ ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues");
+ IDictionary updatedEnvironment = null;
+
+ if (_updateEnvironment)
+ {
+ foreach (KeyValuePair> variable in s_mismatchedEnvironmentValues)
+ {
+ string oldValue = variable.Value.Key;
+ string newValue = variable.Value.Value;
+
+ string environmentValue = null;
+ environment.TryGetValue(variable.Key, out environmentValue);
+
+ if (String.Equals(environmentValue, oldValue, StringComparison.OrdinalIgnoreCase))
+ {
+ if (updatedEnvironment == null)
+ {
+ if (_updateEnvironmentAndLog)
+ {
+ LogMessageFromResource(MessageImportance.Low, "ModifyingTaskHostEnvironmentHeader");
+ }
+
+ updatedEnvironment = new Dictionary(environment, StringComparer.OrdinalIgnoreCase);
+ }
+
+ if (newValue != null)
+ {
+ if (_updateEnvironmentAndLog)
+ {
+ LogMessageFromResource(MessageImportance.Low, "ModifyingTaskHostEnvironmentVariable", variable.Key, newValue, environmentValue ?? String.Empty);
+ }
+
+ updatedEnvironment[variable.Key] = newValue;
+ }
+ else
+ {
+ updatedEnvironment.Remove(variable.Key);
+ }
+ }
+ }
+ }
+
+ if (updatedEnvironment == null)
+ {
+ updatedEnvironment = environment;
+ }
+
+ CommunicationsUtilities.SetEnvironment(updatedEnvironment);
+ }
+
+ ///
+ /// Given the environment of the task host at the end of task execution, make sure that any
+ /// processor-specific variables have been re-applied in the correct form for the main node.
+ ///
+ private IDictionary UpdateEnvironmentForMainNode(IDictionary environment)
+ {
+ ErrorUtilities.VerifyThrowInternalNull(s_mismatchedEnvironmentValues, "mismatchedEnvironmentValues");
+ IDictionary updatedEnvironment = null;
+
+ if (_updateEnvironment)
+ {
+ foreach (KeyValuePair> variable in s_mismatchedEnvironmentValues)
+ {
+ string oldValue = variable.Value.Value;
+ string newValue = variable.Value.Key;
+
+ string environmentValue = null;
+ environment.TryGetValue(variable.Key, out environmentValue);
+
+ if (String.Equals(environmentValue, oldValue, StringComparison.OrdinalIgnoreCase))
+ {
+ updatedEnvironment ??= new Dictionary(environment, StringComparer.OrdinalIgnoreCase);
+
+ if (newValue != null)
+ {
+ updatedEnvironment[variable.Key] = newValue;
+ }
+ else
+ {
+ updatedEnvironment.Remove(variable.Key);
+ }
+ }
+ }
+ }
+
+ if (updatedEnvironment == null)
+ {
+ updatedEnvironment = environment;
+ }
+
+ return updatedEnvironment;
+ }
+
+ ///
+ /// Make sure the mismatchedEnvironmentValues table has been populated.
+ ///
+ private void InitializeMismatchedEnvironmentTable(IDictionary environment)
+ {
+ if (s_mismatchedEnvironmentValues == null)
+ {
+ s_mismatchedEnvironmentValues = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ foreach (KeyValuePair variable in _savedEnvironment)
+ {
+ string oldValue = variable.Value;
+ string newValue;
+ if (!environment.TryGetValue(variable.Key, out newValue))
+ {
+ s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(null, oldValue);
+ }
+ else
+ {
+ if (!String.Equals(oldValue, newValue, StringComparison.OrdinalIgnoreCase))
+ {
+ s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(newValue, oldValue);
+ }
+ }
+ }
+
+ foreach (KeyValuePair variable in environment)
+ {
+ string newValue = variable.Value;
+ string oldValue;
+ if (!_savedEnvironment.TryGetValue(variable.Key, out oldValue))
+ {
+ s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(newValue, null);
+ }
+ else
+ {
+ if (!String.Equals(oldValue, newValue, StringComparison.OrdinalIgnoreCase))
+ {
+ s_mismatchedEnvironmentValues[variable.Key] = new KeyValuePair(newValue, oldValue);
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Sends the requested packet across to the main node.
+ ///
+ protected void SendBuildEvent(BuildEventArgs e)
+ {
+ if (_nodeEndpoint?.LinkStatus == LinkStatus.Active)
+ {
+#pragma warning disable SYSLIB0050
+ if (!e.GetType().GetTypeInfo().IsSerializable && e is not IExtendedBuildEventArgs)
+#pragma warning disable SYSLIB0050
+ {
+ LogWarningFromResource("ExpectedEventToBeSerializable", e.GetType().Name);
+ return;
+ }
+
+ LogMessagePacketBase logMessage = new(new KeyValuePair(_currentConfiguration.NodeId, e));
+ _nodeEndpoint.SendData(logMessage);
+ }
+ }
+
+ ///
+ /// Generates the message event corresponding to a particular resource string and set of args
+ ///
+ protected void LogMessageFromResource(MessageImportance importance, string messageResource, params object[] messageArgs)
+ {
+ ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log messages!");
+
+ BuildMessageEventArgs message = new BuildMessageEventArgs(
+ ResourceUtilities.FormatString(AssemblyResources.GetString(messageResource), messageArgs),
+ null,
+ _currentConfiguration.TaskName,
+ importance);
+
+ LogMessageEvent(message);
+ }
+
+ ///
+ /// Generates the warning event corresponding to a particular resource string and set of args
+ ///
+ protected void LogWarningFromResource(string messageResource, params object[] messageArgs)
+ {
+ ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log warnings!");
+
+ BuildWarningEventArgs warning = new BuildWarningEventArgs(
+ null,
+ null,
+ ProjectFileOfTaskNode,
+ LineNumberOfTaskNode,
+ ColumnNumberOfTaskNode,
+ 0,
+ 0,
+ ResourceUtilities.FormatString(AssemblyResources.GetString(messageResource), messageArgs),
+ null,
+ _currentConfiguration.TaskName);
+
+ LogWarningEvent(warning);
+ }
+
+ ///
+ /// Generates the error event corresponding to a particular resource string
+ ///
+ protected void LogErrorFromResource(string messageResource)
+ {
+ ErrorUtilities.VerifyThrow(_currentConfiguration != null, "We should never have a null configuration when we're trying to log errors!");
+
+ BuildErrorEventArgs error = new BuildErrorEventArgs(
+ null,
+ null,
+ ProjectFileOfTaskNode,
+ LineNumberOfTaskNode,
+ ColumnNumberOfTaskNode,
+ 0,
+ 0,
+ AssemblyResources.GetString(messageResource),
+ null,
+ _currentConfiguration.TaskName);
+
+ LogErrorEvent(error);
+ }
+ }
+}
diff --git a/src/MSBuild/SidecarTaskHostNode.cs b/src/MSBuild/SidecarTaskHostNode.cs
new file mode 100644
index 00000000000..8c7f8af61c0
--- /dev/null
+++ b/src/MSBuild/SidecarTaskHostNode.cs
@@ -0,0 +1,155 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#if !CLR2COMPATIBILITY
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using Microsoft.Build.BackEnd;
+using Microsoft.Build.Framework;
+
+#nullable disable
+
+namespace Microsoft.Build.CommandLine
+{
+ ///
+ /// Sidecar task host node that supports IBuildEngine callbacks by forwarding them to the parent process.
+ /// This is used for long-lived taskhost processes in multithreaded build mode (-mt).
+ ///
+ ///
+ /// Unlike , this class fully implements IBuildEngine callbacks
+ /// by sending request packets to the parent process (TaskHostTask) and waiting for responses.
+ /// This enables tasks that use BuildProjectFile, RequestCores, Yield, etc. to work correctly
+ /// when running in a sidecar taskhost.
+ ///
+ internal sealed class SidecarTaskHostNode : OutOfProcTaskHostNodeBase
+ {
+ ///
+ /// Constructor.
+ ///
+ public SidecarTaskHostNode()
+ : base()
+ {
+ // Register packet handlers for callback response packets
+ // These will be added when the callback packet types are implemented
+ }
+
+ #region IBuildEngine2 Implementation (Properties)
+
+ ///
+ /// Gets whether we're running multiple nodes by forwarding the query to the parent process.
+ ///
+ public override bool IsRunningMultipleNodes
+ {
+ get
+ {
+ // TODO: Implement callback forwarding to parent
+ // For now, log error like regular taskhost until callback packets are implemented
+ LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
+ return false;
+ }
+ }
+
+ #endregion // IBuildEngine2 Implementation (Properties)
+
+ #region IBuildEngine Implementation (Methods)
+
+ ///
+ /// Implementation of IBuildEngine.BuildProjectFile by forwarding to the parent process.
+ ///
+ public override bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs)
+ {
+ // TODO: Implement callback forwarding to parent
+ // For now, log error like regular taskhost until callback packets are implemented
+ LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
+ return false;
+ }
+
+ #endregion // IBuildEngine Implementation (Methods)
+
+ #region IBuildEngine2 Implementation (Methods)
+
+ ///
+ /// Implementation of IBuildEngine2.BuildProjectFile by forwarding to the parent process.
+ ///
+ public override bool BuildProjectFile(string projectFileName, string[] targetNames, IDictionary globalProperties, IDictionary targetOutputs, string toolsVersion)
+ {
+ // TODO: Implement callback forwarding to parent
+ LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
+ return false;
+ }
+
+ ///
+ /// Implementation of IBuildEngine2.BuildProjectFilesInParallel by forwarding to the parent process.
+ ///
+ public override bool BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IDictionary[] targetOutputsPerProject, string[] toolsVersion, bool useResultsCache, bool unloadProjectsOnCompletion)
+ {
+ // TODO: Implement callback forwarding to parent
+ LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
+ return false;
+ }
+
+ #endregion // IBuildEngine2 Implementation (Methods)
+
+ #region IBuildEngine3 Implementation
+
+ ///
+ /// Implementation of IBuildEngine3.BuildProjectFilesInParallel by forwarding to the parent process.
+ ///
+ public override BuildEngineResult BuildProjectFilesInParallel(string[] projectFileNames, string[] targetNames, IDictionary[] globalProperties, IList[] removeGlobalProperties, string[] toolsVersion, bool returnTargetOutputs)
+ {
+ // TODO: Implement callback forwarding to parent
+ LogErrorFromResource("BuildEngineCallbacksInTaskHostUnsupported");
+ return new BuildEngineResult(false, null);
+ }
+
+ ///
+ /// Implementation of IBuildEngine3.Yield by forwarding to the parent process.
+ ///
+ public override void Yield()
+ {
+ // TODO: Implement callback forwarding to parent
+ // For now, just return silently (no-op) like regular taskhost
+ return;
+ }
+
+ ///
+ /// Implementation of IBuildEngine3.Reacquire by forwarding to the parent process.
+ ///
+ public override void Reacquire()
+ {
+ // TODO: Implement callback forwarding to parent
+ // For now, just return silently (no-op) like regular taskhost
+ return;
+ }
+
+ #endregion // IBuildEngine3 Implementation
+
+ #region IBuildEngine9 Implementation
+
+ ///
+ /// Implementation of IBuildEngine9.RequestCores by forwarding to the parent process.
+ ///
+ public override int RequestCores(int requestedCores)
+ {
+ // TODO: Implement callback forwarding to parent
+ // For now, throw like regular taskhost until callback packets are implemented
+ throw new NotImplementedException();
+ }
+
+ ///
+ /// Implementation of IBuildEngine9.ReleaseCores by forwarding to the parent process.
+ ///
+ public override void ReleaseCores(int coresToRelease)
+ {
+ // TODO: Implement callback forwarding to parent
+ // For now, throw like regular taskhost until callback packets are implemented
+ throw new NotImplementedException();
+ }
+
+ #endregion
+ }
+}
+
+#endif
diff --git a/src/MSBuild/XMake.cs b/src/MSBuild/XMake.cs
index 64ed54db5de..bcd3abe26eb 100644
--- a/src/MSBuild/XMake.cs
+++ b/src/MSBuild/XMake.cs
@@ -2902,9 +2902,11 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool
}
else if (nodeModeNumber == 2)
{
- // We now have an option to run a long-lived sidecar TaskHost so we have to handle the NodeReuse switch.
+ // Regular TaskHost node - short-lived, does not support IBuildEngine callbacks.
+ // Used for cross-targeting (architecture/runtime mismatch) scenarios.
bool nodeReuse = ProcessNodeReuseSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.NodeReuse]);
byte parentPacketVersion = ProcessParentPacketVersionSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ParentPacketVersion]);
+
OutOfProcTaskHostNode node = new();
shutdownReason = node.Run(out nodeException, nodeReuse, parentPacketVersion);
}
@@ -2923,6 +2925,16 @@ private static void StartLocalNode(CommandLineSwitches commandLineSwitches, bool
_ => throw new ArgumentOutOfRangeException(nameof(rarShutdownReason), $"Unexpected value: {rarShutdownReason}"),
};
}
+ else if (nodeModeNumber == 4)
+ {
+ // Sidecar TaskHost node - long-lived, supports IBuildEngine callbacks.
+ // Used for thread-unsafe tasks in multithreaded build mode (-mt).
+ bool nodeReuse = ProcessNodeReuseSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.NodeReuse]);
+ byte parentPacketVersion = ProcessParentPacketVersionSwitch(commandLineSwitches[CommandLineSwitches.ParameterizedSwitch.ParentPacketVersion]);
+
+ SidecarTaskHostNode node = new();
+ shutdownReason = node.Run(out nodeException, nodeReuse, parentPacketVersion);
+ }
else if (nodeModeNumber == 8)
{
// Since build function has to reuse code from *this* class and OutOfProcServerNode is in different assembly
diff --git a/src/MSBuildTaskHost/MSBuildTaskHost.csproj b/src/MSBuildTaskHost/MSBuildTaskHost.csproj
index a514cbcdcbf..f4fd468444f 100644
--- a/src/MSBuildTaskHost/MSBuildTaskHost.csproj
+++ b/src/MSBuildTaskHost/MSBuildTaskHost.csproj
@@ -241,6 +241,7 @@
+
OutOfProcTaskAppDomainWrapperBase.cs
diff --git a/src/Shared/Debugging/DebugUtils.cs b/src/Shared/Debugging/DebugUtils.cs
index 72756fc43af..0d2726e1ba8 100644
--- a/src/Shared/Debugging/DebugUtils.cs
+++ b/src/Shared/Debugging/DebugUtils.cs
@@ -18,7 +18,8 @@ private enum NodeMode
{
CentralNode,
OutOfProcNode,
- OutOfProcTaskHostNode
+ OutOfProcTaskHostNode,
+ SidecarTaskHostNode
}
static DebugUtils()
@@ -70,7 +71,7 @@ internal static void SetDebugPath()
NodeMode ScanNodeMode(string input)
{
- var match = Regex.Match(input, @"/nodemode:(?[12\s])(\s|$)", RegexOptions.IgnoreCase);
+ var match = Regex.Match(input, @"/nodemode:(?[124\s])(\s|$)", RegexOptions.IgnoreCase);
if (!match.Success)
{
@@ -84,6 +85,7 @@ NodeMode ScanNodeMode(string input)
{
"1" => NodeMode.OutOfProcNode,
"2" => NodeMode.OutOfProcTaskHostNode,
+ "4" => NodeMode.SidecarTaskHostNode,
_ => throw new NotImplementedException(),
};
}
@@ -109,11 +111,11 @@ private static bool CurrentProcessMatchesDebugName()
/// Returns true if the current process is an out-of-proc TaskHost node.
///
///
- /// True if this process was launched with /nodemode:2 (indicating it's a TaskHost process),
+ /// True if this process was launched with /nodemode:2 (TaskHost) or /nodemode:4 (SidecarTaskHost),
/// false otherwise. This is useful for conditionally enabling debugging or other behaviors
/// based on whether the code is running in the main MSBuild process or a child TaskHost process.
///
- public static bool IsInTaskHostNode() => ProcessNodeMode.Value == NodeMode.OutOfProcTaskHostNode;
+ public static bool IsInTaskHostNode() => ProcessNodeMode.Value is NodeMode.OutOfProcTaskHostNode or NodeMode.SidecarTaskHostNode;
public static string FindNextAvailableDebugFilePath(string fileName)
{