File: AssemblyDependency\Node\OutOfProcRarNode.cs
Web Access
Project: ..\..\..\src\Tasks\Microsoft.Build.Tasks.csproj (Microsoft.Build.Tasks.Core)
// 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.Threading;
using System.Threading.Tasks;
using Microsoft.Build.BackEnd;
using Microsoft.Build.Internal;
using Microsoft.Build.Shared;
 
namespace Microsoft.Build.Tasks.AssemblyDependency
{
    /// <summary>
    /// Implements a persistent node for the ResolveAssemblyReferences task.
    /// This manages the lifecycle of the multi-instance pipe server which executes RAR requests
    /// and does not invoke the task itself.
    /// </summary>
    public sealed class OutOfProcRarNode
    {
        private readonly ServerNodeHandshake _handshake = new(HandshakeOptions.None);
 
        private readonly int _maxNumberOfConcurrentTasks;
 
        public OutOfProcRarNode()
            : this(Environment.ProcessorCount)
        {
        }
 
        public OutOfProcRarNode(int maxNumberOfConcurrentTasks) => _maxNumberOfConcurrentTasks = maxNumberOfConcurrentTasks;
 
        /// <summary>
        /// Starts the node and begins processing RAR execution requests until cancelled.
        /// </summary>
        /// <param name="shutdownException">The exception which caused shutdown, if any.</param>
        /// <param name="cancellationToken">A cancellation token to observe while running the node loop.</param>
        /// <returns>The reason for the node shutdown.</returns>
        public RarNodeShutdownReason Run(out Exception? shutdownException, CancellationToken cancellationToken = default)
        {
            RarNodeShutdownReason shutdownReason;
            shutdownException = null;
 
            try
            {
                shutdownReason = RunNodeAsync(cancellationToken).GetAwaiter().GetResult();
            }
            catch (OperationCanceledException)
            {
                // Consider cancellation as an intentional shutdown of the node.
                shutdownReason = RarNodeShutdownReason.Complete;
            }
            catch (UnauthorizedAccessException ex)
            {
                // Access to the path is denied if the named pipe already exists or is owned by a different user.
                shutdownException = new InvalidOperationException("RAR node is already running.", ex);
                shutdownReason = RarNodeShutdownReason.AlreadyRunning;
            }
            catch (Exception ex)
            {
                shutdownException = ex;
                shutdownReason = RarNodeShutdownReason.Error;
            }
 
            if (shutdownException == null)
            {
                CommunicationsUtilities.Trace("Shutting down with reason: {0}");
            }
            else
            {
                CommunicationsUtilities.Trace("Shutting down with reason: {0}, and exception: {1}", shutdownReason, shutdownException);
            }
 
            return shutdownReason;
        }
 
        private async Task<RarNodeShutdownReason> RunNodeAsync(CancellationToken cancellationToken)
        {
            // The RAR node uses two sets of pipe servers:
            // 1. A single instance pipe to manage the lifecycle of the node.
            // 2. A multi-instance pipe to execute concurrent RAR requests.
            // Because multi-instance pipes can live across multiple processes, we can't rely on the instance cap to preven
            // multiple nodes from running in the event of a race condition.
            // This also simplifies tearing down all active pipe servers when shutdown is requested.
            using NodePipeServer pipeServer = new(NamedPipeUtil.GetRarNodePipeName(_handshake), _handshake);
 
            NodePacketFactory packetFactory = new();
            packetFactory.RegisterPacketHandler(NodePacketType.NodeBuildComplete, NodeBuildComplete.FactoryForDeserialization, null);
            pipeServer.RegisterPacketFactory(packetFactory);
 
            using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
 
            Task nodeEndpointTasks = Task.Run(() => RunNodeEndpointsAsync(linkedCts.Token), linkedCts.Token);
 
            // Run any static initializers which will add latency to the first task run.
            _ = new ResolveAssemblyReference();
 
            while (!cancellationToken.IsCancellationRequested)
            {
                LinkStatus linkStatus = await WaitForConnection(pipeServer, cancellationToken);
 
                if (linkStatus == LinkStatus.Active)
                {
                    NodeBuildComplete buildComplete = (NodeBuildComplete)pipeServer.ReadPacket();
 
                    if (!buildComplete.PrepareForReuse)
                    {
                        break;
                    }
                }
 
                pipeServer.Disconnect();
            }
 
            // Gracefully shutdown the node endpoints.
            linkedCts.Cancel();
 
            try
            {
                await nodeEndpointTasks;
            }
            catch (OperationCanceledException)
            {
                // Ignore since cancellation is expected.
            }
 
            return RarNodeShutdownReason.Complete;
 
            // WaitForConnection does not currently accept cancellation, so use Wait to watch for cancellation.
            // Cancellation is only expected when MSBuild is gracefully shutting down the node or running in unit tests.
            static async Task<LinkStatus> WaitForConnection(NodePipeServer pipeServer, CancellationToken cancellationToken)
            {
                Task<LinkStatus> linkStatusTask = Task.Run(pipeServer.WaitForConnection);
                linkStatusTask.Wait(cancellationToken);
                return await linkStatusTask;
            }
        }
 
        private async Task RunNodeEndpointsAsync(CancellationToken cancellationToken)
        {
            // Setup data shared between all endpoints.
            string pipeName = NamedPipeUtil.GetRarNodeEndpointPipeName(_handshake);
            NodePacketFactory packetFactory = new();
            packetFactory.RegisterPacketHandler(NodePacketType.RarNodeExecuteRequest, RarNodeExecuteRequest.FactoryForDeserialization, null);
 
            OutOfProcRarNodeEndpoint[] endpoints = new OutOfProcRarNodeEndpoint[_maxNumberOfConcurrentTasks];
 
            // Validate all endpoint pipe handles successfully initialize before running any read loops.
            // This allows us to bail out in the event where we can't control every pipe instance.
            for (int i = 0; i < endpoints.Length; i++)
            {
                endpoints[i] = new OutOfProcRarNodeEndpoint(
                        endpointId: i + 1,
                        pipeName,
                        _handshake,
                        _maxNumberOfConcurrentTasks,
                        packetFactory);
            }
 
            Task[] endpointTasks = new Task[endpoints.Length];
 
            for (int i = 0; i < endpoints.Length; i++)
            {
                // Avoid capturing the indexer in the closure.
                OutOfProcRarNodeEndpoint endpoint = endpoints[i];
                endpointTasks[i] = Task.Run(() => endpoint.RunAsync(cancellationToken), cancellationToken);
            }
 
            CommunicationsUtilities.Trace("{0} RAR endpoints started.", _maxNumberOfConcurrentTasks);
 
            await Task.WhenAll(endpointTasks);
 
            foreach (OutOfProcRarNodeEndpoint endpoint in endpoints)
            {
                endpoint.Dispose();
            }
 
            CommunicationsUtilities.Trace("All endpoints successfully stopped. Exiting.");
        }
    }
}