File: BackEnd\BuildManager\CoordinatorClient.Connection.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.Immutable;
using System.IO;
using System.IO.Pipes;
using System.Text;
using System.Threading;
using Microsoft.Build.Framework.Coordinator;

namespace Microsoft.Build.BackEnd;

internal sealed partial class CoordinatorClient
{
    /// <summary>
    ///  Owns a connected coordinator pipe during handshake and grant negotiation.
    /// </summary>
    private sealed class Connection : IDisposable
    {
        private readonly NamedPipeClientStream _pipeStream;
        private readonly BinaryReader _reader;
        private readonly BinaryWriter _writer;
        private readonly LockType _writeLock = new();
        private int _disposed;

        /// <summary>
        ///  Gets the unique identifier sent during the coordinator handshake.
        /// </summary>
        public Guid Id { get; }

        /// <summary>
        ///  Gets the capabilities advertised by the coordinator server during handshake.
        /// </summary>
        public ImmutableArray<string> ServerCapabilities { get; private set; }

        private Connection(NamedPipeClientStream pipeStream)
        {
            _pipeStream = pipeStream;
            _reader = new BinaryReader(pipeStream, Encoding.UTF8, leaveOpen: true);
            _writer = new BinaryWriter(pipeStream, Encoding.UTF8, leaveOpen: true);

            Id = Guid.NewGuid();
        }

        /// <summary>
        ///  Creates a handshaken coordinator connection over an already-connected pipe.
        /// </summary>
        /// <returns>
        ///  A connection that owns the pipe, reader, and writer; or <see langword="null"/> if the handshake failed.
        /// </returns>
        public static Connection? TryCreate(NamedPipeClientStream pipeStream, int processId, ICoordinatorDebugOutput output)
        {
            var connection = new Connection(pipeStream);

            if (connection.TryHandshake(processId, output))
            {
                return connection;
            }

            // If the handshake wasn't successful.
            connection.Dispose();
            return null;
        }

        private bool TryHandshake(int processId, ICoordinatorDebugOutput output)
        {
            try
            {
                output.WriteLine($"CoordinatorClient: Sending handshake (ConnectionId {Id})");
                WriteClientMessage(new ClientHandshakeMessage(Id, processId, capabilities: [Capabilities.NestedGrants]));

                ServerMessage response = ReadServerMessage();

                if (response is ServerHandshakeMessage serverHandshake)
                {
                    ServerCapabilities = serverHandshake.Capabilities;
                    output.WriteLine($"CoordinatorClient: Handshake complete (server capabilities: [{string.Join(", ", serverHandshake.Capabilities)}])");
                    return true;
                }

                if (response is ErrorMessage error)
                {
                    output.WriteLine($"CoordinatorClient: Server rejected handshake: {error.Message}");
                    return false;
                }

                output.WriteLine($"CoordinatorClient: Unexpected handshake response: {response.GetType().Name}");
                return false;
            }
            catch (Exception ex) when (ex is IOException or EndOfStreamException)
            {
                output.WriteLine($"CoordinatorClient: Exception during handshake: {ex.Message}");
                return false;
            }
        }

        /// <summary>
        ///  Reads the next server message from the coordinator pipe.
        /// </summary>
        public ServerMessage ReadServerMessage() => _reader.ReadServerMessage();

        /// <summary>
        ///  Writes a client message to the coordinator pipe.
        /// </summary>
        public void WriteClientMessage(ClientMessage message)
        {
            lock (_writeLock)
            {
                _writer.Write(message);
            }
        }

        /// <summary>
        ///  Checks whether the coordinator server advertised a capability during handshake.
        /// </summary>
        public bool HasServerCapability(string capability) => ServerCapabilities.Contains(capability);

        public void Dispose()
        {
            if (Interlocked.Exchange(ref _disposed, 1) != 0)
            {
                return;
            }

            try
            {
                lock (_writeLock)
                {
                    _writer.Dispose();
                }
            }
            catch (IOException)
            {
                // Flush in BinaryWriter.Dispose can throw if the pipe is already broken.
            }

            _reader.Dispose();
            _pipeStream.Dispose();
        }
    }
}