ModernUO/Projects/Server/LibUv/LibuvConnection.cs
2019-12-26 18:08:50 -08:00

309 lines
9.8 KiB
C#

// 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<UvStreamHandle, int, object> _readCallback = ReadCallback;
private static readonly Func<UvStreamHandle, int, object, LibuvFunctions.uv_buf_t> _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<object> _waitForConnectionClosedTcs = new TaskCompletionSource<object>(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<byte> 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<FlushResult> 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)}.");
}
}
}
}