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) {