| File: BackEnd\Components\Communications\NodeProviderInProc.cs | Web Access |
| Project: src\msbuild\src\Build\Microsoft.Build.csproj (Microsoft.Build) |
// 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.Threading; using Microsoft.Build.Internal; #if FEATURE_THREAD_CULTURE using BuildParameters = Microsoft.Build.Execution.BuildParameters; #else using System.Globalization; #endif using NodeEngineShutdownReason = Microsoft.Build.Execution.NodeEngineShutdownReason; #nullable disable namespace Microsoft.Build.BackEnd { /// <summary> /// An implementation of a node provider for in-proc nodes. /// </summary> internal class NodeProviderInProc : INodeProvider, INodePacketFactory, IDisposable { /// <summary> /// Context for an in-proc node. This contains all the information needed to manage the node. /// </summary> internal sealed class NodeContext { /// <summary> /// The in-proc node. /// </summary> public INode _inProcNode; /// <summary> /// The in-proc node endpoint. /// </summary> public INodeEndpoint _inProcNodeEndpoint; /// <summary> /// The packet factory used to route packets from the node. /// </summary> public INodePacketFactory _packetFactory; /// <summary> /// The in-proc node thread. /// </summary> public Thread _inProcNodeThread; /// <summary> /// Event which is raised when the in-proc endpoint is connected. /// </summary> public AutoResetEvent _endpointConnectedEvent = new AutoResetEvent(false); } #region Private Data /// <summary> /// A mapping of all the nodes managed by this provider. /// </summary> private ConcurrentDictionary<int, NodeContext> _nodeContexts; /// <summary> /// Flag indicating we have disposed. /// </summary> private bool _disposed = false; /// <summary> /// Value used to ensure multiple in-proc nodes which save the operating environment are not created. /// </summary> private static Semaphore InProcNodeOwningOperatingEnvironment; /// <summary> /// The component host. /// </summary> private IBuildComponentHost _componentHost; /// <summary> /// Check to allow the inproc node to have exclusive ownership of the operating environment /// </summary> private bool _exclusiveOperatingEnvironment = false; #endregion /// <summary> /// Finalizes an instance of the <see cref="NodeProviderInProc"/> class. /// </summary> ~NodeProviderInProc() { Dispose(false /* disposing */); } /// <summary> /// Returns the type of nodes managed by this provider. /// </summary> public NodeProviderType ProviderType { get { return NodeProviderType.InProc; } } /// <summary> /// Returns the number of nodes available to create on this provider. /// </summary> public int AvailableNodes { get { int maxNodeCount = _componentHost.BuildParameters.MultiThreaded ? _componentHost.BuildParameters.MaxNodeCount : 1; return maxNodeCount - _nodeContexts.Count; } } #region IBuildComponent Members /// <summary> /// Sets the build component host. /// </summary> /// <param name="host">The component host.</param> public void InitializeComponent(IBuildComponentHost host) { _componentHost = host; // Initialize with proper capacity for performance optimization // Use Environment.ProcessorCount for concurrencyLevel to handle multi-threaded scenarios if (host.BuildParameters?.MaxNodeCount > 0) { _nodeContexts = new ConcurrentDictionary<int, NodeContext>( concurrencyLevel: Environment.ProcessorCount, capacity: host.BuildParameters.MaxNodeCount); } else { _nodeContexts = new ConcurrentDictionary<int, NodeContext>(); } } /// <summary> /// Shuts down this component. /// </summary> public void ShutdownComponent() { } #endregion #region INodeProvider Members /// <summary> /// Sends data to the specified node. /// </summary> /// <param name="nodeId">The node to which data should be sent.</param> /// <param name="packet">The data to send.</param> public void SendData(int nodeId, INodePacket packet) { ArgumentNullException.ThrowIfNull(packet); bool nodeExists = _nodeContexts.TryGetValue(nodeId, out NodeContext nodeContext); Assumed.True(nodeExists, $"InProc node {nodeId} does not exist."); nodeContext._inProcNodeEndpoint.SendData(packet); } /// <summary> /// Causes all connected nodes to be shut down. /// </summary> /// <param name="enableReuse">Flag indicating if the nodes should prepare for reuse.</param> public void ShutdownConnectedNodes(bool enableReuse) { foreach (NodeContext nodeContext in _nodeContexts.Values) { nodeContext._inProcNodeEndpoint.SendData(new NodeBuildComplete(enableReuse)); } } /// <summary> /// Causes all nodes to be shut down permanently - for InProc nodes it is the same as ShutdownConnectedNodes /// with enableReuse = false /// </summary> public void ShutdownAllNodes() { ShutdownConnectedNodes(false /* no node reuse */); } public IList<NodeInfo> CreateNodes(int nextNodeId, INodePacketFactory factory, Func<NodeInfo, NodeConfiguration> configurationFactory, int numberOfNodesToCreate) { var nodes = new List<NodeInfo>(numberOfNodesToCreate); for (int i = 0; i < numberOfNodesToCreate; i++) { int nodeId = nextNodeId + i; NodeInfo nodeInfo = new(nodeId, ProviderType); if (!CreateNode(nodeId, factory, configurationFactory(nodeInfo))) { // If it fails let it return what we have created so far so caller can somehow acquire missing nodes. break; } nodes.Add(nodeInfo); } return nodes; } /// <summary> /// Requests that a node be created on the specified machine. /// </summary> /// <param name="nodeId">The id of the node to create.</param> /// <param name="factory">The factory to use to create packets from this node.</param> /// <param name="configuration">The configuration for the node.</param> private bool CreateNode(int nodeId, INodePacketFactory factory, NodeConfiguration configuration) { // Attempt to get the operating environment semaphore if requested. if (_componentHost.BuildParameters.SaveOperatingEnvironment) { // We can only create additional in-proc nodes if we have decided not to save the operating environment. This is the global // DTAR case in Visual Studio, but other clients might enable this as well under certain special circumstances. if (Environment.GetEnvironmentVariable("MSBUILDINPROCENVCHECK") == "1") { _exclusiveOperatingEnvironment = true; } if (_exclusiveOperatingEnvironment) { if (InProcNodeOwningOperatingEnvironment == null) { InProcNodeOwningOperatingEnvironment = new Semaphore(1, 1); } if (!InProcNodeOwningOperatingEnvironment.WaitOne(0)) { // Can't take the operating environment. return false; } } } bool nodeExists = _nodeContexts.ContainsKey(nodeId); // If it doesn't already exist, create it. if (!nodeExists) { if (!InstantiateNode(nodeId, factory)) { return false; } } _nodeContexts[nodeId]._inProcNodeEndpoint.SendData(configuration); return true; } #endregion #region INodePacketFactory Members /// <summary> /// Registers a packet handler. Not used in the in-proc node. /// </summary> public void RegisterPacketHandler(NodePacketType packetType, NodePacketFactoryMethod factory, INodePacketHandler handler) => Assumed.Unreachable(); /// <summary> /// Unregisters a packet handler. Not used in the in-proc node. /// </summary> public void UnregisterPacketHandler(NodePacketType packetType) => Assumed.Unreachable(); /// <summary> /// Deserializes and routes a packet. Not used in the in-proc node. /// </summary> public void DeserializeAndRoutePacket(int nodeId, NodePacketType packetType, ITranslator translator) => Assumed.Unreachable(); /// <summary> /// Deserializes and routes a packet. Not used in the in-proc node. /// </summary> public INodePacket DeserializePacket(NodePacketType packetType, ITranslator translator) => Assumed.Unreachable<INodePacket>(); /// <summary> /// Routes a packet. /// </summary> /// <param name="nodeId">The id of the node from which the packet is being routed.</param> /// <param name="packet">The packet to route.</param> public void RoutePacket(int nodeId, INodePacket packet) { if (!_nodeContexts.TryGetValue(nodeId, out NodeContext nodeContext)) { return; } INodePacketFactory factory = nodeContext._packetFactory; // If this was a shutdown packet, we are done with the node. Release all context associated with it. Do this here, rather // than after we route the packet, because otherwise callbacks to the NodeManager to determine if we have available nodes // will report that the in-proc node is still in use when it has actually shut down. if (packet.Type == NodePacketType.NodeShutdown) { _nodeContexts.TryRemove(nodeId, out _); // Release the operating environment semaphore if we were holding it. if ((_componentHost.BuildParameters.SaveOperatingEnvironment) && (InProcNodeOwningOperatingEnvironment != null)) { InProcNodeOwningOperatingEnvironment.Release(); InProcNodeOwningOperatingEnvironment.Dispose(); InProcNodeOwningOperatingEnvironment = null; } } // Route the packet back to the NodeManager. factory.RoutePacket(nodeId, packet); } #endregion /// <summary> /// IDisposable implementation /// </summary> public void Dispose() { Dispose(true /* disposing */); GC.SuppressFinalize(this); } /// <summary> /// Factory for component creation. /// </summary> internal static IBuildComponent CreateComponent(BuildComponentType type) { Assumed.Equal(type, BuildComponentType.InProcNodeProvider, $"Cannot create component of type {type}"); return new NodeProviderInProc(); } #region Private Methods /// <summary> /// Creates a new in-proc node. /// </summary> private bool InstantiateNode(int nodeId, INodePacketFactory factory) { Assumed.False(_nodeContexts.ContainsKey(nodeId), $"In Proc node {nodeId} already instantiated."); NodeEndpointInProc.EndpointPair endpoints = NodeEndpointInProc.CreateInProcEndpoints(NodeEndpointInProc.EndpointMode.Synchronous, _componentHost, nodeId); NodeContext nodeContext = new(); _nodeContexts[nodeId] = nodeContext; nodeContext._inProcNodeEndpoint = endpoints.ManagerEndpoint; nodeContext._inProcNodeEndpoint.OnLinkStatusChanged += new LinkStatusChangedDelegate(InProcNodeEndpoint_OnLinkStatusChanged); nodeContext._packetFactory = factory; nodeContext._inProcNode = new InProcNode(nodeId, _componentHost, endpoints.NodeEndpoint); #if FEATURE_THREAD_CULTURE nodeContext._inProcNodeThread = new Thread(() => InProcNodeThreadProc(nodeContext._inProcNode), BuildParameters.ThreadStackSize); #else CultureInfo culture = _componentHost.BuildParameters.Culture; CultureInfo uiCulture = _componentHost.BuildParameters.UICulture; nodeContext._inProcNodeThread = new Thread(() => { CultureInfo.CurrentCulture = culture; CultureInfo.CurrentUICulture = uiCulture; InProcNodeThreadProc(nodeContext._inProcNode); }); #endif nodeContext._inProcNodeThread.Name = $"In-proc Node {nodeId} ({_componentHost.Name})"; nodeContext._inProcNodeThread.IsBackground = true; #if FEATURE_THREAD_CULTURE nodeContext._inProcNodeThread.CurrentCulture = _componentHost.BuildParameters.Culture; nodeContext._inProcNodeThread.CurrentUICulture = _componentHost.BuildParameters.UICulture; #endif nodeContext._inProcNodeThread.Start(); nodeContext._inProcNodeEndpoint.Connect(this); int connectionTimeout = CommunicationsUtilities.NodeConnectionTimeout; bool connected = nodeContext._endpointConnectedEvent.WaitOne(connectionTimeout); Assumed.True(connected, $"In-proc node failed to start up within {connectionTimeout}ms"); return true; } /// <summary> /// Thread proc which runs the in-proc node. /// </summary> private void InProcNodeThreadProc(INode inProcNode) { Exception e; NodeEngineShutdownReason reason = inProcNode.Run(out e); InProcNodeShutdown(reason, e); } /// <summary> /// Callback invoked when the link status of the endpoint has changed. /// </summary> /// <param name="endpoint">The endpoint whose status has changed.</param> /// <param name="status">The new link status.</param> private void InProcNodeEndpoint_OnLinkStatusChanged(INodeEndpoint endpoint, LinkStatus status) { if (status == LinkStatus.Active) { bool foundEndpoint = false; foreach (NodeContext nodeContext in _nodeContexts.Values) { if (endpoint == nodeContext._inProcNodeEndpoint) { nodeContext._endpointConnectedEvent.Set(); foundEndpoint = true; } } if (!foundEndpoint) { // We don't verify this outside of the 'if' because we don't care about the link going down, which will occur // after we have cleared the inProcNodeEndpoint due to shutting down the node. Assumed.True(foundEndpoint, "Received link status event for a node other than our peer."); } } } /// <summary> /// Callback invoked when the endpoint shuts down. /// </summary> /// <param name="reason">The reason the endpoint is shutting down.</param> /// <param name="e">Any exception which was raised that caused the endpoint to shut down.</param> private void InProcNodeShutdown(NodeEngineShutdownReason reason, Exception e) { switch (reason) { case NodeEngineShutdownReason.BuildComplete: case NodeEngineShutdownReason.BuildCompleteReuse: case NodeEngineShutdownReason.Error: break; case NodeEngineShutdownReason.ConnectionFailed: InternalError.Throw($"Unexpected shutdown code {reason} received."); break; } } /// <summary> /// Dispose implementation. /// </summary> private void Dispose(bool disposing) { if (!_disposed) { if (disposing) { foreach (NodeContext nodeContext in _nodeContexts.Values) { if (nodeContext._endpointConnectedEvent != null) { nodeContext._endpointConnectedEvent.Dispose(); nodeContext._endpointConnectedEvent = null; } } } _disposed = true; } } // The process here is the same as in the main node. public IEnumerable<Process> GetProcesses() => throw new NotImplementedException(); #endregion } }