| File: ConnectionHost.cs | Web Access |
| Project: src\sdk\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); } } } } }