|
// 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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using Microsoft.Build.Exceptions;
using Microsoft.Build.Framework;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using Constants = Microsoft.Build.Framework.Constants;
#nullable disable
namespace Microsoft.Build.BackEnd
{
/// <summary>
/// The provider for out-of-proc nodes. This manages the lifetime of external MSBuild.exe processes
/// which act as child nodes for the build system.
/// </summary>
internal class NodeProviderOutOfProcTaskHost : NodeProviderOutOfProcBase, INodeProvider, INodePacketFactory, INodePacketHandler
{
/// <summary>
/// Store the path for MSBuild / MSBuildTaskHost so that we don't have to keep recalculating it.
/// </summary>
private static string s_baseTaskHostPath;
/// <summary>
/// Store the 64-bit path for MSBuild / MSBuildTaskHost so that we don't have to keep recalculating it.
/// </summary>
private static string s_baseTaskHostPath64;
/// <summary>
/// Store the 64-bit path for MSBuild / MSBuildTaskHost so that we don't have to keep recalculating it.
/// </summary>
private static string s_baseTaskHostPathArm64;
/// <summary>
/// Store the path for the 32-bit MSBuildTaskHost so that we don't have to keep re-calculating it.
/// </summary>
private static string s_pathToX32Clr2;
/// <summary>
/// Store the path for the 64-bit MSBuildTaskHost so that we don't have to keep re-calculating it.
/// </summary>
private static string s_pathToX64Clr2;
/// <summary>
/// Store the path for the 32-bit MSBuild so that we don't have to keep re-calculating it.
/// </summary>
private static string s_pathToX32Clr4;
/// <summary>
/// Store the path for the 64-bit MSBuild so that we don't have to keep re-calculating it.
/// </summary>
private static string s_pathToX64Clr4;
/// <summary>
/// Store the path for the 64-bit MSBuild so that we don't have to keep re-calculating it.
/// </summary>
private static string s_pathToArm64Clr4;
/// <summary>
/// Name for MSBuild.exe
/// </summary>
private static string s_msbuildName;
/// <summary>
/// Name for MSBuildTaskHost.exe
/// </summary>
private static string s_msbuildTaskHostName;
/// <summary>
/// Are there any active nodes?
/// </summary>
private ManualResetEvent _noNodesActiveEvent;
/// <summary>
/// A mapping of all the task host nodes managed by this provider.
/// The key is a TaskHostNodeKey combining HandshakeOptions and scheduled node ID.
/// </summary>
private ConcurrentDictionary<TaskHostNodeKey, NodeContext> _nodeContexts;
/// <summary>
/// Reverse mapping from communication node ID to TaskHostNodeKey.
/// Used for O(1) lookup when handling node termination from ShutdownAllNodes.
/// </summary>
private ConcurrentDictionary<int, TaskHostNodeKey> _nodeIdToNodeKey;
/// <summary>
/// A mapping of all of the INodePacketFactories wrapped by this provider.
/// Keyed by the communication node ID (NodeContext.NodeId) for O(1) packet routing.
/// Thread-safe to support parallel taskhost creation in /mt mode where multiple thread nodes
/// can simultaneously create their own taskhosts.
/// </summary>
private ConcurrentDictionary<int, INodePacketFactory> _nodeIdToPacketFactory;
/// <summary>
/// A mapping of all of the INodePacketHandlers wrapped by this provider.
/// Keyed by the communication node ID (NodeContext.NodeId) for O(1) packet routing.
/// Thread-safe to support parallel taskhost creation in /mt mode where multiple thread nodes
/// can simultaneously create their own taskhosts.
/// </summary>
private ConcurrentDictionary<int, INodePacketHandler> _nodeIdToPacketHandler;
/// <summary>
/// Keeps track of the set of node IDs for which we have not yet received shutdown notification.
/// </summary>
private HashSet<int> _activeNodes;
/// <summary>
/// Counter for generating unique communication node IDs.
/// Incremented atomically for each new node created.
/// </summary>
private int _nextNodeId;
/// <summary>
/// Packet factory we use if there's not already one associated with a particular context.
/// </summary>
private NodePacketFactory _localPacketFactory;
/// <summary>
/// Constructor.
/// </summary>
private NodeProviderOutOfProcTaskHost()
{
}
#region INodeProvider Members
/// <summary>
/// Returns the node provider type.
/// </summary>
public NodeProviderType ProviderType
{
[DebuggerStepThrough]
get
{ return NodeProviderType.OutOfProc; }
}
/// <summary>
/// Returns the number of available nodes.
/// </summary>
public int AvailableNodes
{
get
{
throw new NotImplementedException("This property is not implemented because available nodes are unlimited.");
}
}
/// <summary>
/// Returns the name of the CLR2 Task Host executable
/// </summary>
internal static string TaskHostNameForClr2TaskHost
{
get
{
if (s_msbuildTaskHostName == null)
{
s_msbuildTaskHostName = Environment.GetEnvironmentVariable("MSBUILDTASKHOST_EXE_NAME");
if (s_msbuildTaskHostName == null)
{
s_msbuildTaskHostName = "MSBuildTaskHost.exe";
}
}
return s_msbuildTaskHostName;
}
}
/// <summary>
/// Instantiates a new MSBuild process acting as a child node.
/// </summary>
public IList<NodeInfo> CreateNodes(int nextNodeId, INodePacketFactory packetFactory, Func<NodeInfo, NodeConfiguration> configurationFactory, int numberOfNodesToCreate)
{
throw new NotImplementedException("Use the other overload of CreateNode instead");
}
/// <summary>
/// Sends data to the specified node.
/// Note: For task hosts, use the overload that takes TaskHostNodeKey instead.
/// </summary>
/// <param name="nodeId">The node to which data shall be sent.</param>
/// <param name="packet">The packet to send.</param>
public void SendData(int nodeId, INodePacket packet)
{
throw new NotImplementedException("For task hosts, use the overload that takes TaskHostNodeKey.");
}
/// <summary>
/// Sends data to the specified task host node.
/// </summary>
/// <param name="nodeKey">The task host node key identifying the target node.</param>
/// <param name="packet">The packet to send.</param>
internal void SendData(TaskHostNodeKey nodeKey, INodePacket packet)
{
ErrorUtilities.VerifyThrow(_nodeContexts.TryGetValue(nodeKey, out NodeContext context), "Invalid host context specified: {0}.", nodeKey);
SendData(context, packet);
}
/// <summary>
/// Shuts down all of the connected managed nodes.
/// </summary>
/// <param name="enableReuse">Flag indicating if nodes should prepare for reuse.</param>
public void ShutdownConnectedNodes(bool enableReuse)
{
// Send the build completion message to the nodes, causing them to shutdown or reset.
List<NodeContext> contextsToShutDown = [.. _nodeContexts.Values];
ShutdownConnectedNodes(contextsToShutDown, enableReuse);
_noNodesActiveEvent.WaitOne();
}
/// <summary>
/// Shuts down all of the managed nodes permanently.
/// </summary>
public void ShutdownAllNodes()
{
ShutdownAllNodes(ComponentHost.BuildParameters.EnableNodeReuse, NodeContextTerminated);
}
#endregion
#region IBuildComponent Members
/// <summary>
/// Initializes the component.
/// </summary>
/// <param name="host">The component host.</param>
public void InitializeComponent(IBuildComponentHost host)
{
ComponentHost = host;
_nodeContexts = new ConcurrentDictionary<TaskHostNodeKey, NodeContext>();
_nodeIdToNodeKey = new ConcurrentDictionary<int, TaskHostNodeKey>();
_nodeIdToPacketFactory = new ConcurrentDictionary<int, INodePacketFactory>();
_nodeIdToPacketHandler = new ConcurrentDictionary<int, INodePacketHandler>();
_activeNodes = [];
_nextNodeId = 0;
_noNodesActiveEvent = new ManualResetEvent(true);
_localPacketFactory = new NodePacketFactory();
(this as INodePacketFactory).RegisterPacketHandler(NodePacketType.LogMessage, LogMessagePacket.FactoryForDeserialization, this);
(this as INodePacketFactory).RegisterPacketHandler(NodePacketType.TaskHostTaskComplete, TaskHostTaskComplete.FactoryForDeserialization, this);
(this as INodePacketFactory).RegisterPacketHandler(NodePacketType.NodeShutdown, NodeShutdown.FactoryForDeserialization, this);
}
/// <summary>
/// Shuts down the component
/// </summary>
public void ShutdownComponent()
{
}
#endregion
#region INodePacketFactory Members
/// <summary>
/// Registers the specified handler for a particular packet type.
/// </summary>
/// <param name="packetType">The packet type.</param>
/// <param name="factory">The factory for packets of the specified type.</param>
/// <param name="handler">The handler to be called when packets of the specified type are received.</param>
public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler)
{
_localPacketFactory.RegisterPacketHandler(packetType, factory, handler);
}
/// <summary>
/// Unregisters a packet handler.
/// </summary>
/// <param name="packetType">The packet type.</param>
public void UnregisterPacketHandler(NodePacketType packetType)
{
_localPacketFactory.UnregisterPacketHandler(packetType);
}
/// <summary>
/// Takes a serializer, deserializes the packet and routes it to the appropriate handler.
/// </summary>
/// <param name="nodeId">The node from which the packet was received.</param>
/// <param name="packetType">The packet type.</param>
/// <param name="translator">The translator containing the data from which the packet should be reconstructed.</param>
public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator)
{
if (_nodeIdToPacketFactory.TryGetValue(nodeId, out INodePacketFactory nodePacketFactory))
{
nodePacketFactory.DeserializeAndRoutePacket(nodeId, packetType, translator);
}
else
{
_localPacketFactory.DeserializeAndRoutePacket(nodeId, packetType, translator);
}
}
/// <summary>
/// Takes a serializer and deserializes the packet.
/// </summary>
/// <param name="packetType">The packet type.</param>
/// <param name="translator">The translator containing the data from which the packet should be reconstructed.</param>
public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator)
{
return _localPacketFactory.DeserializePacket(packetType, translator);
}
/// <summary>
/// Routes the specified packet
/// </summary>
/// <param name="nodeId">The node from which the packet was received.</param>
/// <param name="packet">The packet to route.</param>
public void RoutePacket(int nodeId, INodePacket packet)
{
if (_nodeIdToPacketFactory.TryGetValue(nodeId, out INodePacketFactory nodePacketFactory))
{
nodePacketFactory.RoutePacket(nodeId, packet);
}
else
{
_localPacketFactory.RoutePacket(nodeId, packet);
}
}
#endregion
#region INodePacketHandler Members
/// <summary>
/// This method is invoked by the NodePacketRouter when a packet is received and is intended for
/// this recipient.
/// </summary>
/// <param name="node">The node from which the packet was received.</param>
/// <param name="packet">The packet.</param>
public void PacketReceived(int node, INodePacket packet)
{
if (_nodeIdToPacketHandler.TryGetValue(node, out INodePacketHandler packetHandler))
{
packetHandler.PacketReceived(node, packet);
}
else
{
ErrorUtilities.VerifyThrow(packet.Type == NodePacketType.NodeShutdown, "We should only ever handle packets of type NodeShutdown -- everything else should only come in when there's an active task");
// May also be removed by unnatural termination, so don't assume it's there
lock (_activeNodes)
{
if (_activeNodes.Contains(node))
{
_activeNodes.Remove(node);
}
if (_activeNodes.Count == 0)
{
_noNodesActiveEvent.Set();
}
}
}
}
#endregion
/// <summary>
/// Static factory for component creation.
/// </summary>
internal static IBuildComponent CreateComponent(BuildComponentType componentType)
{
ErrorUtilities.VerifyThrow(componentType == BuildComponentType.OutOfProcTaskHostNodeProvider, "Factory cannot create components of type {0}", componentType);
return new NodeProviderOutOfProcTaskHost();
}
/// <summary>
/// Clears out our cached values for the various task host names and paths.
/// FOR UNIT TESTING ONLY
/// </summary>
internal static void ClearCachedTaskHostPaths()
{
s_msbuildName = null;
s_msbuildTaskHostName = null;
s_pathToX32Clr2 = null;
s_pathToX32Clr4 = null;
s_pathToX64Clr2 = null;
s_pathToX64Clr4 = null;
s_pathToArm64Clr4 = null;
s_baseTaskHostPath = null;
s_baseTaskHostPath64 = null;
s_baseTaskHostPathArm64 = null;
}
/// <summary>
/// Given a TaskHostContext, returns the name of the executable we should be searching for.
/// </summary>
internal static string GetTaskHostNameFromHostContext(HandshakeOptions hostContext)
{
ErrorUtilities.VerifyThrowInternalErrorUnreachable(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost));
if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2))
{
return TaskHostNameForClr2TaskHost;
}
if (string.IsNullOrEmpty(s_msbuildName))
{
s_msbuildName = Environment.GetEnvironmentVariable("MSBUILD_EXE_NAME");
if (!string.IsNullOrEmpty(s_msbuildName))
{
return s_msbuildName;
}
// Default based on whether it's .NET or Framework
s_msbuildName = Constants.MSBuildExecutableName;
}
return s_msbuildName;
}
/// <summary>
/// Given a TaskHostContext, returns the appropriate runtime host and MSBuild assembly locations
/// based on the handshake options.
/// </summary>
/// <param name="hostContext">The handshake options specifying the desired task host configuration (architecture, CLR version, runtime).</param>
/// <returns>
/// The full path to MSBuild.exe.
/// </returns>
internal static string GetMSBuildExecutablePathForNonNETRuntimes(HandshakeOptions hostContext)
{
ErrorUtilities.VerifyThrowInternalErrorUnreachable(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost));
var toolName = GetTaskHostNameFromHostContext(hostContext);
s_baseTaskHostPath = BuildEnvironmentHelper.Instance.MSBuildToolsDirectory32;
s_baseTaskHostPath64 = BuildEnvironmentHelper.Instance.MSBuildToolsDirectory64;
s_baseTaskHostPathArm64 = BuildEnvironmentHelper.Instance.MSBuildToolsDirectoryArm64;
bool isX64 = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.X64);
bool isArm64 = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.Arm64);
bool isCLR2 = Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2);
if (isCLR2)
{
if (isArm64)
{
ErrorUtilities.ThrowInternalError("ARM64 CLR2 task hosts are not supported.");
}
return isX64
? Path.Combine(GetOrInitializeX64Clr2Path(toolName), toolName)
: Path.Combine(GetOrInitializeX32Clr2Path(toolName), toolName);
}
if (isX64)
{
return Path.Combine(s_pathToX64Clr4 ??= s_baseTaskHostPath64, toolName);
}
if (isArm64)
{
return Path.Combine(s_pathToArm64Clr4 ??= s_baseTaskHostPathArm64, toolName);
}
return Path.Combine(s_pathToX32Clr4 ??= s_baseTaskHostPath, toolName);
}
/// <summary>
/// Handles the handshake scenario where a .NET task host is requested from a .NET Framework process.
/// </summary>
/// <returns>
/// A tuple containing:
/// - RuntimeHostPath: The path to the dotnet executable that will host the .NET runtime
/// - MSBuildPath: The path to MSBuild.dll/MSBuild app host.
/// </returns>
internal static (string RuntimeHostPath, string MSBuildPath) GetMSBuildLocationForNETRuntime(HandshakeOptions hostContext, TaskHostParameters taskHostParameters)
{
ErrorUtilities.VerifyThrowInternalErrorUnreachable(Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.TaskHost));
return (taskHostParameters.DotnetHostPath, GetMSBuildPath(taskHostParameters));
}
private static string GetMSBuildPath(in TaskHostParameters taskHostParameters)
{
if (taskHostParameters.MSBuildAssemblyPath != null)
{
ValidateNetHostSdkVersion(taskHostParameters.MSBuildAssemblyPath);
return taskHostParameters.MSBuildAssemblyPath;
}
#if NET
// In .NET we resolve the full path based on the tools directory that points to the directory with App Host
return BuildEnvironmentHelper.Instance.CurrentMSBuildToolsDirectory;
#else
throw new InvalidProjectFileException(ResourceUtilities.GetResourceString("NETHostTaskLoad_Failed"));
#endif
static void ValidateNetHostSdkVersion(string path)
{
const int MinimumSdkVersion = 10;
if (string.IsNullOrEmpty(path))
{
ErrorUtilities.ThrowInternalError(ResourceUtilities.GetResourceString("SDKPathResolution_Failed"));
}
if (!FileSystems.Default.DirectoryExists(path))
{
ErrorUtilities.ThrowInternalError(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("SDKPathCheck_Failed", path));
}
var sdkVersion = ExtractSdkVersionFromPath(path);
if (sdkVersion is null or < MinimumSdkVersion)
{
throw new InvalidProjectFileException(ResourceUtilities.FormatResourceStringIgnoreCodeAndKeyword("NETHostVersion_Failed", sdkVersion, MinimumSdkVersion));
}
}
}
/// <summary>
/// Extracts the major version number from an SDK directory path by parsing the last directory name.
/// </summary>
/// <param name="path">
/// The full path to an SDK directory.
/// Example: "C:\Program Files\dotnet\sdk\10.0.100-preview.7.25322.101".
/// </param>
/// <returns>
/// The major version number if successfully parsed from the directory name, otherwise null.
/// For the example path above, this would return 10.
/// </returns>
/// <remarks>
/// The method works by:
/// 1. Extracting the last directory name from the path (e.g., "10.0.100-preview.7.25322.101")
/// 2. Finding the first dot in that directory name
/// 3. Parsing the substring before the first dot as an integer (the major version)
///
/// Returns null if the path is invalid, the last directory name is empty,
/// there's no dot in the directory name, or the major version cannot be parsed as an integer.
/// </remarks>
private static int? ExtractSdkVersionFromPath(string path)
{
string lastDirectoryName = Path.GetFileName(path.TrimEnd(Path.DirectorySeparatorChar));
if (string.IsNullOrEmpty(lastDirectoryName))
{
return null;
}
int dotIndex = lastDirectoryName.IndexOf('.');
if (dotIndex <= 0)
{
return null;
}
return int.TryParse(lastDirectoryName.Substring(0, dotIndex), out int majorVersion)
? majorVersion
: null;
}
private static string GetOrInitializeX64Clr2Path(string toolName)
{
s_pathToX64Clr2 ??= GetPathFromEnvironmentOrDefault("MSBUILDTASKHOSTLOCATION64", s_baseTaskHostPath64, toolName);
return s_pathToX64Clr2;
}
private static string GetOrInitializeX32Clr2Path(string toolName)
{
s_pathToX32Clr2 ??= GetPathFromEnvironmentOrDefault("MSBUILDTASKHOSTLOCATION", s_baseTaskHostPath, toolName);
return s_pathToX32Clr2;
}
private static string GetPathFromEnvironmentOrDefault(string environmentVariable, string defaultPath, string toolName)
{
string envPath = Environment.GetEnvironmentVariable(environmentVariable);
if (!string.IsNullOrEmpty(envPath))
{
string fullPath = Path.Combine(envPath, toolName);
if (FileUtilities.FileExistsNoThrow(fullPath))
{
return envPath;
}
}
return defaultPath;
}
/// <summary>
/// Make sure a node in the requested context exists.
/// </summary>
internal bool AcquireAndSetUpHost(
TaskHostNodeKey nodeKey,
INodePacketFactory factory,
INodePacketHandler handler,
TaskHostConfiguration configuration,
in TaskHostParameters taskHostParameters)
{
bool nodeCreationSucceeded;
if (!_nodeContexts.ContainsKey(nodeKey))
{
nodeCreationSucceeded = CreateNode(nodeKey, factory, handler, configuration, taskHostParameters);
}
else
{
// node already exists, so "creation" automatically succeeded
nodeCreationSucceeded = true;
}
if (nodeCreationSucceeded)
{
NodeContext context = _nodeContexts[nodeKey];
// Map the transport ID directly to the handlers for O(1) packet routing
_nodeIdToPacketFactory[context.NodeId] = factory;
_nodeIdToPacketHandler[context.NodeId] = handler;
// Configure the node.
context.SendData(configuration);
return true;
}
return false;
}
/// <summary>
/// Expected to be called when TaskHostTask is done with host of the given context.
/// </summary>
internal void DisconnectFromHost(TaskHostNodeKey nodeKey)
{
// The node context might already have been removed by NodeContextTerminated if the task host
// process terminated before we got here. This is a valid race condition - just return early.
// Note: NodeContextTerminated does NOT remove handlers, so they'll be orphaned in this case,
// but that's acceptable since the task host is dead and the handlers will be cleaned up
// when the provider is shut down.
if (!_nodeContexts.TryGetValue(nodeKey, out NodeContext context))
{
CommunicationsUtilities.Trace("DisconnectFromHost: Node context already removed for key: {0}", nodeKey);
return;
}
bool successRemoveFactory = _nodeIdToPacketFactory.TryRemove(context.NodeId, out _);
bool successRemoveHandler = _nodeIdToPacketHandler.TryRemove(context.NodeId, out _);
ErrorUtilities.VerifyThrow(successRemoveFactory && successRemoveHandler, "Why are we trying to disconnect from a context that we already disconnected from? Did we call DisconnectFromHost twice?");
}
/// <summary>
/// Instantiates a new MSBuild or MSBuildTaskHost process acting as a child node.
/// </summary>
internal bool CreateNode(TaskHostNodeKey nodeKey, INodePacketFactory factory, INodePacketHandler handler, TaskHostConfiguration configuration, in TaskHostParameters taskHostParameters)
{
ErrorUtilities.VerifyThrowArgumentNull(factory);
ErrorUtilities.VerifyThrow(!_nodeContexts.ContainsKey(nodeKey), "We should not already have a node for this context! Did we forget to call DisconnectFromHost somewhere?");
HandshakeOptions hostContext = nodeKey.HandshakeOptions;
// Generate a unique node ID for communication purposes using atomic increment.
int communicationNodeId = Interlocked.Increment(ref _nextNodeId);
// Create callbacks that capture the TaskHostNodeKey
void OnNodeContextCreated(NodeContext context) => NodeContextCreated(context, nodeKey);
NodeLaunchData nodeLaunchData = ResolveNodeLaunchConfiguration(hostContext, taskHostParameters);
if (nodeLaunchData.MSBuildLocation == null)
{
return default;
}
CommunicationsUtilities.Trace("For a host context of {0}, spawning executable from {1}.", hostContext, nodeLaunchData.MSBuildLocation);
IList<NodeContext> nodeContexts = GetNodes(
nodeLaunchData,
communicationNodeId,
this,
OnNodeContextCreated,
NodeContextTerminated,
1);
return nodeContexts.Count == 1;
// Resolves the node launch configuration based on the host context.
NodeLaunchData ResolveNodeLaunchConfiguration(HandshakeOptions hostContext, in TaskHostParameters taskHostParameters)
{
// Handle .NET task host context
if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NET))
{
return ResolveAppHostOrFallback(GetMSBuildPath(taskHostParameters), taskHostParameters.DotnetHostPath, hostContext, IsNodeReuseEnabled(hostContext));
}
#if FEATURE_NET35_TASKHOST
// CLR2 task host (MSBuildTaskHost.exe) requires special handling:
// - Empty command-line args (MSBuildTaskHost.Main() takes no arguments)
// - Handshake with toolsDirectory set to the EXE's directory so the
// salt matches what the child process computes on startup.
if (Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2))
{
string msbuildLocation = GetMSBuildExecutablePathForNonNETRuntimes(hostContext);
string toolsDirectory = Path.GetDirectoryName(msbuildLocation) ?? string.Empty;
return new NodeLaunchData(msbuildLocation, string.Empty, new Handshake(hostContext, toolsDirectory));
}
#endif
// CLR4 task host (MSBuild.exe on .NET Framework)
return new NodeLaunchData(GetMSBuildExecutablePathForNonNETRuntimes(hostContext), BuildCommandLineArgs(IsNodeReuseEnabled(hostContext)), new Handshake(hostContext));
}
}
/// <summary>
/// Determines whether node reuse should be enabled for the given host context.
/// Node reuse is disabled for CLR2 because it uses legacy MSBuildTaskHost.
/// </summary>
private static bool IsNodeReuseEnabled(HandshakeOptions hostContext) =>
Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.NodeReuse) && !Handshake.IsHandshakeOptionEnabled(hostContext, HandshakeOptions.CLR2);
/// <summary>
/// Resolves whether to use the MSBuild app host or fall back to dotnet.exe.
/// </summary>
/// <param name="msbuildAssemblyPath">Path to the MSBuild assembly/app host directory.</param>
/// <param name="dotnetHostPath">Path to the dotnet executable.</param>
/// <param name="hostContext">The handshake options for the host context.</param>
/// <param name="nodeReuseEnabled">Whether node reuse is enabled.</param>
/// <returns>The resolved node launch configuration.</returns>
private NodeLaunchData ResolveAppHostOrFallback(
string msbuildAssemblyPath,
string dotnetHostPath,
HandshakeOptions hostContext,
bool nodeReuseEnabled)
{
string appHostPath = Path.Combine(msbuildAssemblyPath, Constants.MSBuildExecutableName);
string commandLineArgs = BuildCommandLineArgs(nodeReuseEnabled);
if (FileSystems.Default.FileExists(appHostPath))
{
CommunicationsUtilities.Trace("For a host context of {0}, using app host from {1}.", hostContext, appHostPath);
IDictionary<string, string> dotnetOverrides = DotnetHostEnvironmentHelper.CreateDotnetRootEnvironmentOverrides(dotnetHostPath);
return dotnetOverrides == null
? throw new NodeFailedToLaunchException(errorCode: null, ResourceUtilities.GetResourceString("DotnetHostPathNotSet"))
: new NodeLaunchData(
appHostPath,
commandLineArgs,
new Handshake(hostContext, toolsDirectory: msbuildAssemblyPath),
dotnetOverrides);
}
// Auto-discover dotnet host path when not explicitly provided.
string resolvedDotnetHostPath = dotnetHostPath;
#if RUNTIME_TYPE_NETCORE
if (string.IsNullOrEmpty(resolvedDotnetHostPath))
{
resolvedDotnetHostPath = CurrentHost.GetCurrentHost();
}
#endif
CommunicationsUtilities.Trace("For a host context of {0}, app host not found at {1}, falling back to dotnet.exe from {2}.", hostContext, appHostPath, resolvedDotnetHostPath);
return new NodeLaunchData(
resolvedDotnetHostPath,
$"\"{Path.Combine(msbuildAssemblyPath, Constants.MSBuildAssemblyName)}\" {commandLineArgs}",
new Handshake(hostContext, toolsDirectory: msbuildAssemblyPath));
}
private string BuildCommandLineArgs(bool nodeReuseEnabled) => $"/nologo {NodeModeHelper.ToCommandLineArgument(NodeMode.OutOfProcTaskHostNode)} /nodereuse:{nodeReuseEnabled} /low:{ComponentHost.BuildParameters.LowPriority} /parentpacketversion:{NodePacketTypeExtensions.PacketVersion} ";
/// <summary>
/// Method called when a context created.
/// </summary>
private void NodeContextCreated(NodeContext context, TaskHostNodeKey nodeKey)
{
_nodeContexts[nodeKey] = context;
_nodeIdToNodeKey[context.NodeId] = nodeKey;
lock (_activeNodes)
{
_activeNodes.Add(context.NodeId);
_noNodesActiveEvent.Reset();
}
// Start the asynchronous read.
context.BeginAsyncPacketRead();
}
/// <summary>
/// Method called when a context terminates (called from CreateNode callbacks or ShutdownAllNodes).
/// </summary>
private void NodeContextTerminated(int nodeId)
{
// Remove from nodeKey-based lookup if we have it
if (_nodeIdToNodeKey.TryRemove(nodeId, out TaskHostNodeKey nodeKey))
{
_nodeContexts.TryRemove(nodeKey, out _);
}
// May also be removed by unnatural termination, so don't assume it's there
lock (_activeNodes)
{
_activeNodes.Remove(nodeId);
if (_activeNodes.Count == 0)
{
_noNodesActiveEvent.Set();
}
}
}
public IEnumerable<Process> GetProcesses() => _nodeContexts.Values.Select(context => context.Process);
}
}
|