File: ConnectionHost.cs
Web Access
Project: ..\..\..\src\RazorSdk\Tool\Microsoft.NET.Sdk.Razor.Tool.csproj (rzc)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.IO.Pipes;
 
namespace Microsoft.NET.Sdk.Razor.Tool
{
    // Heavily influenced by:
    // https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Server/VBCSCompiler/NamedPipeClientConnection.cs#L17
    internal abstract class ConnectionHost
    {
        private static int counter;
 
        public abstract Task<Connection> WaitForConnectionAsync(CancellationToken cancellationToken);
 
        public static ConnectionHost Create(string pipeName)
        {
            return new NamedPipeConnectionHost(pipeName);
        }
 
        private static string GetNextIdentifier()
        {
            var id = Interlocked.Increment(ref counter);
            return "connection-" + id;
        }
 
        private class NamedPipeConnectionHost : ConnectionHost
        {
            // Size of the buffers to use: 64K
            private const int PipeBufferSize = 0x10000;
 
            // From https://github.com/dotnet/corefx/blob/29cd6a0b0ac2993cee23ebaf36ca3d4bce6dd75f/src/System.IO.Pipes/ref/System.IO.Pipes.cs#L93.
            // Using the enum value directly as this option is not available in netstandard.
            private const PipeOptions PipeOptionCurrentUserOnly = (PipeOptions)536870912;
 
            private static readonly PipeOptions _pipeOptions = GetPipeOptions();
 
            public NamedPipeConnectionHost(string pipeName)
            {
                PipeName = pipeName;
            }
 
            public string PipeName { get; }
 
            public override async Task<Connection> WaitForConnectionAsync(CancellationToken cancellationToken)
            {
                // Create the pipe and begin waiting for a connection. This  doesn't block, but could fail 
                // in certain circumstances, such as the OS refusing to create the pipe for some reason 
                // or the pipe was disconnected before we starting listening.
                var pipeStream = new NamedPipeServerStream(
                    PipeName,
                    PipeDirection.InOut,
                    NamedPipeServerStream.MaxAllowedServerInstances, // Maximum connections.
                    PipeTransmissionMode.Byte,
                    _pipeOptions,
                    PipeBufferSize, // Default input buffer
                    PipeBufferSize);// Default output buffer
 
                ServerLogger.Log("Waiting for new connection");
                await pipeStream.WaitForConnectionAsync(cancellationToken);
                ServerLogger.Log("Pipe connection detected.");
 
                if (Environment.Is64BitProcess || Memory.IsMemoryAvailable())
                {
                    ServerLogger.Log("Memory available - accepting connection");
                    return new NamedPipeConnection(pipeStream, GetNextIdentifier());
                }
 
                pipeStream.Close();
                throw new Exception("Insufficient resources to process new connection.");
            }
 
            private static PipeOptions GetPipeOptions()
            {
                var options = PipeOptions.Asynchronous | PipeOptions.WriteThrough;
 
                if (Enum.IsDefined(typeof(PipeOptions), PipeOptionCurrentUserOnly))
                {
                    return options | PipeOptionCurrentUserOnly;
                }
 
                return options;
            }
        }
 
        private class NamedPipeConnection : Connection
        {
            public NamedPipeConnection(NamedPipeServerStream stream, string identifier)
            {
                Stream = stream;
                Identifier = identifier;
            }
 
            public override async Task WaitForDisconnectAsync(CancellationToken cancellationToken)
            {
                if (!(Stream is PipeStream pipeStream))
                {
                    return;
                }
 
                // We have to poll for disconnection by reading, PipeStream.IsConnected isn't reliable unless you
                // actually do a read - which will cause it to update its state.
                while (!cancellationToken.IsCancellationRequested && pipeStream.IsConnected)
                {
                    await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
 
                    try
                    {
                        ServerLogger.Log($"Before poking pipe {Identifier}.");
                        // Stream.ReadExactlyAsync with a zero-length buffer will not update the state of the pipe.
                        // We need to trigger a state update without actually reading the data.
                        _ = await Stream.ReadAsync(Array.Empty<byte>(), 0, 0, cancellationToken);
                        ServerLogger.Log($"After poking pipe {Identifier}.");
                    }
                    catch (OperationCanceledException)
                    {
                    }
                    catch (Exception e)
                    {
                        // It is okay for this call to fail.  Errors will be reflected in the
                        // IsConnected property which will be read on the next iteration.
                        ServerLogger.LogException(e, $"Error poking pipe {Identifier}.");
                    }
                }
            }
 
            protected override void Dispose(bool disposing)
            {
                ServerLogger.Log($"Pipe {Identifier}: Closing.");
 
                try
                {
                    Stream.Dispose();
                }
                catch (Exception ex)
                {
                    // The client connection failing to close isn't fatal to the server process.  It is simply a client
                    // for which we can no longer communicate and that's okay because the Close method indicates we are
                    // done with the client already.
                    var message = $"Pipe {Identifier}: Error closing pipe.";
                    ServerLogger.LogException(ex, message);
                }
            }
        }
    }
}