File: CoordinatorServer.Connection.cs
Web Access
Project: src\msbuild\src\MSBuild.Coordinator\MSBuild.Coordinator.csproj (MSBuild.Coordinator)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.IO;
using System.IO.Pipes;
using System.Text;
using Microsoft.Build.Framework.Coordinator;

namespace Microsoft.Build.Coordinator;

internal sealed partial class CoordinatorServer
{
    /// <summary>
    ///  Owns a newly-connected client pipe during server-side handshake and request negotiation.
    /// </summary>
    private sealed class Connection : IDisposable
    {
        private NamedPipeServerStream? _pipeStream;
        private BinaryReader? _reader;
        private BinaryWriter? _writer;

        /// <summary>
        ///  Gets the unique identifier received during the client handshake.
        /// </summary>
        public Guid Id { get; private set; }

        /// <summary>
        ///  Gets the process ID received during the client handshake.
        /// </summary>
        public int ProcessId { get; private set; }

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

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

        /// <summary>
        ///  Creates a handshaken server 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(NamedPipeServerStream pipeStream, ICoordinatorDebugOutput output)
        {
            var connection = new Connection(pipeStream);

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

            connection.Dispose();
            return null;
        }

        private bool TryHandshake(ICoordinatorDebugOutput output)
        {
            try
            {
                ClientMessage clientMessage = ReadClientMessage();

                if (clientMessage is not ClientHandshakeMessage handshake)
                {
                    output.WriteLine($"CoordinatorServer: Rejected client — first message was {clientMessage.GetType().Name}");
                    WriteServerMessage(new ErrorMessage("First message must be Handshake"));
                    return false;
                }

                Id = handshake.ConnectionId;
                ProcessId = handshake.ProcessId;
                ClientCapabilities = handshake.Capabilities;

                output.WriteLine($"CoordinatorServer: Handshake received (ConnectionId {Id}, PID {ProcessId}, Capabilities: [{string.Join(", ", ClientCapabilities)}])");

                if (ProcessId <= 0)
                {
                    output.WriteLine($"CoordinatorServer: Rejected client — invalid handshake (PID={ProcessId})");
                    WriteServerMessage(new ErrorMessage("Invalid handshake: ProcessId must be > 0"));
                    return false;
                }

                WriteServerMessage(new ServerHandshakeMessage([Capabilities.NestedGrants]));

                return true;
            }
            catch (Exception ex)
            {
                output.WriteLine($"CoordinatorServer: Exception during handshake: {ex}");
                return false;
            }
        }

        /// <summary>
        ///  Transfers ownership of the pipe, reader, and writer to the caller.
        /// </summary>
        public (NamedPipeServerStream 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()
        {
            try
            {
                _writer?.Dispose();
            }
            catch (IOException)
            {
                // The pipe may already be broken if the client disconnected.
            }

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

        /// <summary>
        ///  Reads the next client message from the coordinator pipe.
        /// </summary>
        public ClientMessage ReadClientMessage()
        {
            Assumed.NotNull(_reader);
            return _reader.ReadClientMessage();
        }

        /// <summary>
        ///  Writes a server message to the coordinator pipe.
        /// </summary>
        public void WriteServerMessage(ServerMessage message)
        {
            Assumed.NotNull(_writer);
            _writer.Write(message);
        }
    }
}