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 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 NamedPipeClientStream? _pipeStream;
        private BinaryReader? _reader;
        private BinaryWriter? _writer;

        /// <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)
        {
            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;
        }

        /// <summary>
        ///  Reads the next server message from the coordinator pipe.
        /// </summary>
        public ServerMessage ReadServerMessage()
        {
            Assumed.NotNull(_reader);
            return _reader.ReadServerMessage();
        }

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

        /// <summary>
        ///  Transfers ownership of the pipe, reader, and writer to the caller.
        /// </summary>
        public (NamedPipeClientStream PipeStream, BinaryReader Reader, BinaryWriter Writer) TransferOwnership()
        {
            Assumed.NotNull(_pipeStream);
            Assumed.NotNull(_reader);
            Assumed.NotNull(_writer);

            var result = (_pipeStream, _reader, _writer);

            _pipeStream = null;
            _reader = null;
            _writer = null;

            return result;
        }

        public void Dispose()
        {
            _reader?.Dispose();
            _writer?.Dispose();
            _pipeStream?.Dispose();
        }
    }
}