// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Buffers; using System.IO; using System.IO.Pipelines; using System.Net; using System.Threading; using System.Threading.Tasks; using Libuv.Internal; using Microsoft.AspNetCore.Connections; using Microsoft.Extensions.Logging; namespace Libuv { public partial class LibuvConnection : TransportConnection { private static readonly int MinAllocBufferSize = SlabMemoryPool.BlockSize / 2; private static readonly Action _readCallback = ReadCallback; private static readonly Func _allocCallback = AllocCallback; private readonly UvStreamHandle _socket; private readonly CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource(); private volatile ConnectionAbortedException _abortReason; private MemoryHandle _bufferHandle; private Task _processingTask; private readonly TaskCompletionSource _waitForConnectionClosedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); private bool _connectionClosed; public LibuvConnection(UvStreamHandle socket, ILibuvTrace log, LibuvThread thread, IPEndPoint remoteEndPoint, IPEndPoint localEndPoint, PipeOptions inputOptions = null, PipeOptions outputOptions = null, long? maxReadBufferSize = null, long? maxWriteBufferSize = null) { _socket = socket; LocalEndPoint = localEndPoint; RemoteEndPoint = remoteEndPoint; ConnectionClosed = _connectionClosedTokenSource.Token; Log = log; Thread = thread; maxReadBufferSize ??= 0; maxWriteBufferSize ??= 0; inputOptions ??= new PipeOptions(MemoryPool, PipeScheduler.ThreadPool, Thread, maxReadBufferSize.Value, maxReadBufferSize.Value / 2, useSynchronizationContext: false); outputOptions ??= new PipeOptions(MemoryPool, Thread, PipeScheduler.ThreadPool, maxWriteBufferSize.Value, maxWriteBufferSize.Value / 2, useSynchronizationContext: false); var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); // Set the transport and connection id Transport = pair.Transport; Application = pair.Application; } public PipeWriter Input => Application.Output; public PipeReader Output => Application.Input; public LibuvOutputConsumer OutputConsumer { get; set; } private ILibuvTrace Log { get; } private LibuvThread Thread { get; } public override MemoryPool MemoryPool => Thread.MemoryPool; public void Start() { _processingTask = StartCore(); } private async Task StartCore() { try { OutputConsumer = new LibuvOutputConsumer(Output, Thread, _socket, ConnectionId, Log); StartReading(); Exception inputError = null; Exception outputError = null; try { // This *must* happen after socket.ReadStart // The socket output consumer is the only thing that can close the connection. If the // output pipe is already closed by the time we start then it's fine since, it'll close gracefully afterwards. await OutputConsumer.WriteOutputAsync(); } catch (UvException ex) { // The connection reset/error has already been logged by LibuvOutputConsumer if (ex.StatusCode == LibuvConstants.ECANCELED) { // Connection was aborted. } else if (LibuvConstants.IsConnectionReset(ex.StatusCode)) { // Don't cause writes to throw for connection resets. inputError = new ConnectionResetException(ex.Message, ex); } else { // This is unexpected. Log.ConnectionError(ConnectionId, ex); inputError = ex; outputError = ex; } } finally { inputError ??= _abortReason ?? new ConnectionAbortedException("The libuv transport's send loop completed gracefully."); // Now, complete the input so that no more reads can happen Input.Complete(inputError); Output.Complete(outputError); // Make sure it isn't possible for a paused read to resume reading after calling uv_close // on the stream handle Input.CancelPendingFlush(); // Send a FIN Log.ConnectionWriteFin(ConnectionId, inputError.Message); // We're done with the socket now _socket.Dispose(); // Ensure this always fires FireConnectionClosed(); await _waitForConnectionClosedTcs.Task; } } catch (Exception e) { Log.LogCritical(0, e, $"{nameof(LibuvConnection)}.{nameof(Start)}() {ConnectionId}"); } } public override void Abort(ConnectionAbortedException abortReason) { _abortReason = abortReason; // Cancel WriteOutputAsync loop after setting _abortReason. Output.CancelPendingRead(); // This cancels any pending I/O. Thread.Post(s => s.Dispose(), _socket); } public override async ValueTask DisposeAsync() { Transport.Input.Complete(); Transport.Output.Complete(); if (_processingTask != null) await _processingTask; _connectionClosedTokenSource.Dispose(); } // Called on Libuv thread private static LibuvFunctions.uv_buf_t AllocCallback(UvStreamHandle handle, int suggestedSize, object state) => ((LibuvConnection)state).OnAlloc(handle, suggestedSize); private unsafe LibuvFunctions.uv_buf_t OnAlloc(UvStreamHandle handle, int suggestedSize) { var currentWritableBuffer = Input.GetMemory(MinAllocBufferSize); _bufferHandle = currentWritableBuffer.Pin(); return handle.Libuv.buf_init((IntPtr)_bufferHandle.Pointer, currentWritableBuffer.Length); } private static void ReadCallback(UvStreamHandle handle, int status, object state) { ((LibuvConnection)state).OnRead(handle, status); } private void OnRead(UvStreamHandle handle, int status) { // Cleanup state from last OnAlloc. This is safe even if OnAlloc wasn't called. _bufferHandle.Dispose(); if (status == 0) { // EAGAIN/EWOULDBLOCK so just return the buffer. // http://docs.libuv.org/en/v1.x/stream.html#c.uv_read_cb } else if (status > 0) { Log.ConnectionRead(ConnectionId, status); Input.Advance(status); var flushTask = Input.FlushAsync(); if (!flushTask.IsCompleted) // We wrote too many bytes to the reader, so pause reading and resume when // we hit the low water mark. _ = ApplyBackpressureAsync(flushTask); } else { // Given a negative status, it's possible that OnAlloc wasn't called. _socket.ReadStop(); Exception error = null; if (status == LibuvConstants.EOF) { Log.ConnectionReadFin(ConnectionId); } else { handle.Libuv.Check(status, out var uvError); error = LogAndWrapReadError(uvError); } FireConnectionClosed(); // Complete after aborting the connection Input.Complete(error); } } private void FireConnectionClosed() { // Guard against scheduling this multiple times if (_connectionClosed) return; _connectionClosed = true; ThreadPool.UnsafeQueueUserWorkItem(state => { state.CancelConnectionClosedToken(); state._waitForConnectionClosedTcs.TrySetResult(null); }, this, false); } private async Task ApplyBackpressureAsync(ValueTask flushTask) { Log.ConnectionPause(ConnectionId); _socket.ReadStop(); var result = await flushTask; // If the reader isn't complete or cancelled then resume reading if (!result.IsCompleted && !result.IsCanceled) { Log.ConnectionResume(ConnectionId); StartReading(); } } private void StartReading() { try { _socket.ReadStart(_allocCallback, _readCallback, this); } catch (UvException ex) { // ReadStart() can throw a UvException in some cases (e.g. socket is no longer connected). // This should be treated the same as OnRead() seeing a negative status. Input.Complete(LogAndWrapReadError(ex)); } } private Exception LogAndWrapReadError(UvException uvError) { if (uvError.StatusCode == LibuvConstants.ECANCELED) { // The operation was canceled by the server not the client. No need for additional logs. return new ConnectionAbortedException(uvError.Message, uvError); } if (LibuvConstants.IsConnectionReset(uvError.StatusCode)) { // Log connection resets at a lower (Debug) level. Log.ConnectionReset(ConnectionId); return new ConnectionResetException(uvError.Message, uvError); } // This is unexpected. Log.ConnectionError(ConnectionId, uvError); return new IOException(uvError.Message, uvError); } private void CancelConnectionClosedToken() { try { _connectionClosedTokenSource.Cancel(); } catch (Exception ex) { Log.LogError(0, ex, $"Unexpected exception in {nameof(LibuvConnection)}.{nameof(CancelConnectionClosedToken)}."); } } } }