diff --git a/.gitignore b/.gitignore index a595e413a..6d4d9c510 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,27 @@ /Distribution/rdrand.so /Distribution/rdrand.dll /Distribution/zlib.dll + +# LibUv Dependencies +/Distribution/libuv.dll +/Distribution/libuv.so +/Distribution/Microsoft.AspNetCore.Connections.Abstractions.dll +/Distribution/Microsoft.AspNetCore.Http.Features.dll +/Distribution/Microsoft.Extensions.Configuration.Abstractions.dll +/Distribution/Microsoft.Extensions.Configuration.Binder.dll +/Distribution/Microsoft.Extensions.Configuration.dll +/Distribution/Microsoft.Extensions.DependencyInjection.Abstractions.dll +/Distribution/Microsoft.Extensions.DependencyInjection.dll +/Distribution/Microsoft.Extensions.FileProviders.Abstractions.dll +/Distribution/Microsoft.Extensions.Hosting.Abstractions.dll +/Distribution/Microsoft.Extensions.Logging.Abstractions.dll +/Distribution/Microsoft.Extensions.Logging.Configuration.dll +/Distribution/Microsoft.Extensions.Logging.Console.dll +/Distribution/Microsoft.Extensions.Logging.dll +/Distribution/Microsoft.Extensions.Options.ConfigurationExtensions.dll +/Distribution/Microsoft.Extensions.Options.dll +/Distribution/Microsoft.Extensions.Primitives.dll + /Projects/Scripts/obj /Projects/Scripts/bin /Projects/Server/obj diff --git a/Contributing.md b/Contributing.md index 6a4faf938..64b88cc70 100644 --- a/Contributing.md +++ b/Contributing.md @@ -6,7 +6,7 @@ One of the easiest ways to contribute is to participate in discussions on GitHub Start a discussion on the [repository issue tracker](https://github.com/modernuo/modernuo/issues). ## Reporting security issues and bugs -Security issues and bugs should be reported privately, via email, to the hi@modernuo.com. +Security issues and bugs should be reported privately, via email, to hi@modernuo.com. You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. @@ -35,7 +35,7 @@ If you don't know what a pull request is read this article: https://help.github. configurations, environment variables, dependencies, etc. 1. Increase the version numbers to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). -1. You may merge the Pull Request in once you have the sign-off of two other developers, or if you +1. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. ## Code of Conduct diff --git a/Projects/Scripts/Accounting/AccessRestrictions.cs b/Projects/Scripts/Accounting/AccessRestrictions.cs index ee40c4d10..49389abb8 100644 --- a/Projects/Scripts/Accounting/AccessRestrictions.cs +++ b/Projects/Scripts/Accounting/AccessRestrictions.cs @@ -16,7 +16,7 @@ namespace Server { try { - IPAddress ip = ((IPEndPoint)e.Socket.RemoteEndPoint).Address; + IPAddress ip = ((IPEndPoint)e.Context.RemoteEndPoint).Address; if (Firewall.IsBlocked(ip)) { @@ -43,4 +43,4 @@ namespace Server } } } -} \ No newline at end of file +} diff --git a/Projects/Scripts/Misc/ClientVerification.cs b/Projects/Scripts/Misc/ClientVerification.cs index ea24ae0e5..eacc7aa6d 100644 --- a/Projects/Scripts/Misc/ClientVerification.cs +++ b/Projects/Scripts/Misc/ClientVerification.cs @@ -108,7 +108,7 @@ namespace Server.Misc Timer.DelayCall(KickDelay, delegate { - if (state.Socket != null) + if (state.Connection != null) { Console.WriteLine("Client: {0}: Disconnecting, bad version", state); state.Dispose(); @@ -173,4 +173,4 @@ namespace Server.Misc Kick } } -} \ No newline at end of file +} diff --git a/Projects/Scripts/Misc/ServerList.cs b/Projects/Scripts/Misc/ServerList.cs index 469fc8d84..86a6ea029 100644 --- a/Projects/Scripts/Misc/ServerList.cs +++ b/Projects/Scripts/Misc/ServerList.cs @@ -3,24 +3,25 @@ using System.IO; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; +using Microsoft.AspNetCore.Connections; using Server.Network; namespace Server.Misc { public class ServerList { - /* + /* * The default setting for Address, a value of 'null', will use your local IP address. If all of your local IP addresses * are private network addresses and AutoDetect is 'true' then RunUO will attempt to discover your public IP address * for you automatically. * * If you do not plan on allowing clients outside of your LAN to connect, you can set AutoDetect to 'false' and leave * Address set to 'null'. - * + * * If your public IP address cannot be determined, you must change the value of Address to your public IP address * manually to allow clients outside of your LAN to connect to your server. Address can be either an IP address or * a hostname that will be resolved when RunUO starts. - * + * * If you want players outside your LAN to be able to connect to your server and you are behind a router, you must also * forward TCP port 2593 to your private IP address. The procedure for doing this varies by manufacturer but generally * involves configuration of the router through your web browser. @@ -32,7 +33,7 @@ namespace Server.Misc * properly and fully supports listening on multiple ports. If a client with a public IP address is connecting to a * locally private address, the server will direct the client to either the AutoDetected IP address or the manually entered * IP address or hostname, whichever is applicable. Loopback clients will be directed to loopback. - * + * * If you would like to listen on additional ports (i.e. 22, 23, 80, for clients behind highly restrictive egress * firewalls) or specific IP adddresses you can do so by modifying the file SocketOptions.cs found in this directory. */ @@ -64,7 +65,7 @@ namespace Server.Misc try { NetState ns = e.State; - Socket s = ns.Socket; + ConnectionContext s = ns.Connection; IPEndPoint ipep = (IPEndPoint)s.LocalEndPoint; diff --git a/Projects/Scripts/Misc/SocketOptions.cs b/Projects/Scripts/Misc/SocketOptions.cs index d51fb20e5..56d12f2d7 100644 --- a/Projects/Scripts/Misc/SocketOptions.cs +++ b/Projects/Scripts/Misc/SocketOptions.cs @@ -5,8 +5,6 @@ namespace Server { public class SocketOptions { - private const bool NagleEnabled = false; // Should the Nagle algorithm be enabled? This may reduce performance - private static IPEndPoint[] m_ListenerEndPoints = { new IPEndPoint(IPAddress.Any, 2593) // Default: Listen on port 2593 on all IP addresses @@ -16,24 +14,10 @@ namespace Server // new IPEndPoint( IPAddress.Parse( "1.2.3.4" ), 2593 ), // Listen on port 2593 on IP address 1.2.3.4 }; - public static void Initialize() - { - EventSink.SocketConnect += EventSink_SocketConnect; - } - public static void RegisterListeners() { for (int i = 0; i < m_ListenerEndPoints.Length; i++) Core.MessagePump.AddListener(m_ListenerEndPoints[i]); } - - private static void EventSink_SocketConnect(SocketConnectEventArgs e) - { - if (!e.AllowConnection) - return; - - if (!NagleEnabled) - e.Socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, 1); // RunUO uses its own algorithm - } } } diff --git a/Projects/Scripts/Scripts.csproj b/Projects/Scripts/Scripts.csproj index f3b49987e..e7fa412b8 100644 --- a/Projects/Scripts/Scripts.csproj +++ b/Projects/Scripts/Scripts.csproj @@ -35,4 +35,7 @@ false + + + diff --git a/Projects/Scripts/SpecialSystems/Engines/PreventInaccess.cs b/Projects/Scripts/SpecialSystems/Engines/PreventInaccess.cs index 8d81d45fb..fcde69079 100644 --- a/Projects/Scripts/SpecialSystems/Engines/PreventInaccess.cs +++ b/Projects/Scripts/SpecialSystems/Engines/PreventInaccess.cs @@ -63,7 +63,7 @@ namespace Server.Misc } } - private static bool HasDisconnected(Mobile m) => m.NetState?.Socket == null; + private static bool HasDisconnected(Mobile m) => m.NetState?.Connection == null; private static LocationInfo GetRandomDestination() => m_Destinations[Utility.Random(m_Destinations.Length)]; diff --git a/Projects/Server/ApplicationLifetime.cs b/Projects/Server/ApplicationLifetime.cs new file mode 100644 index 000000000..314cdcdea --- /dev/null +++ b/Projects/Server/ApplicationLifetime.cs @@ -0,0 +1,108 @@ +// 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.Threading; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Hosting +{ + /// + /// Allows consumers to perform cleanup during a graceful shutdown. + /// + internal class ApplicationLifetime : IHostApplicationLifetime + { + private readonly CancellationTokenSource _startedSource = new CancellationTokenSource(); + private readonly CancellationTokenSource _stoppingSource = new CancellationTokenSource(); + private readonly CancellationTokenSource _stoppedSource = new CancellationTokenSource(); + private readonly ILogger _logger; + + public ApplicationLifetime(ILogger logger) => _logger = logger; + + /// + /// Triggered when the application host has fully started and is about to wait + /// for a graceful shutdown. + /// + public CancellationToken ApplicationStarted => _startedSource.Token; + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// Request may still be in flight. Shutdown will block until this event completes. + /// + public CancellationToken ApplicationStopping => _stoppingSource.Token; + + /// + /// Triggered when the application host is performing a graceful shutdown. + /// All requests should be complete at this point. Shutdown will block + /// until this event completes. + /// + public CancellationToken ApplicationStopped => _stoppedSource.Token; + + /// + /// Signals the ApplicationStopping event and blocks until it completes. + /// + public void StopApplication() + { + // Lock on CTS to synchronize multiple calls to StopApplication. This guarantees that the first call + // to StopApplication and its callbacks run to completion before subsequent calls to StopApplication, + // which will no-op since the first call already requested cancellation, get a chance to execute. + lock (_stoppingSource) + { + try + { + ExecuteHandlers(_stoppingSource); + } + catch (Exception) + { + /* _logger.ApplicationError(LoggerEventIds.ApplicationStoppingException, + "An error occurred stopping the application", + ex);*/ + } + } + } + + /// + /// Signals the ApplicationStarted event and blocks until it completes. + /// + public void NotifyStarted() + { + try + { + ExecuteHandlers(_startedSource); + } + catch (Exception) + { + /* _logger.ApplicationError(LoggerEventIds.ApplicationStartupException, + "An error occurred starting the application", + ex);*/ + } + } + + /// + /// Signals the ApplicationStopped event and blocks until it completes. + /// + public void NotifyStopped() + { + try + { + ExecuteHandlers(_stoppedSource); + } + catch (Exception) + { + /* _logger.ApplicationError(LoggerEventIds.ApplicationStoppedException, + "An error occurred stopping the application", + ex);*/ + } + } + + private void ExecuteHandlers(CancellationTokenSource cancel) + { + // Noop if this is already cancelled + if (cancel.IsCancellationRequested) return; + + // Run the cancellation token callbacks + cancel.Cancel(false); + } + } +} diff --git a/Projects/Server/Assemblies/libuv.dll b/Projects/Server/Assemblies/libuv.dll new file mode 100644 index 000000000..d744f343d Binary files /dev/null and b/Projects/Server/Assemblies/libuv.dll differ diff --git a/Projects/Server/Assemblies/libuv.linux.so b/Projects/Server/Assemblies/libuv.linux.so new file mode 100755 index 000000000..d9caeba12 Binary files /dev/null and b/Projects/Server/Assemblies/libuv.linux.so differ diff --git a/Projects/Server/Assemblies/libuv.osx.so b/Projects/Server/Assemblies/libuv.osx.so new file mode 100755 index 000000000..8b98042f1 Binary files /dev/null and b/Projects/Server/Assemblies/libuv.osx.so differ diff --git a/Projects/Server/Buffers/DiagnosticMemoryPool.cs b/Projects/Server/Buffers/DiagnosticMemoryPool.cs new file mode 100644 index 000000000..9f032a571 --- /dev/null +++ b/Projects/Server/Buffers/DiagnosticMemoryPool.cs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace System.Buffers +{ + /// + /// Used to allocate and distribute re-usable blocks of memory. + /// + public class DiagnosticMemoryPool : MemoryPool + { + private readonly MemoryPool _pool; + + private readonly bool _allowLateReturn; + + private readonly bool _rentTracking; + + private readonly object _syncObj; + + private readonly HashSet _blocks; + + private readonly List _blockAccessExceptions; + + private readonly TaskCompletionSource _allBlocksReturned; + + private int _totalBlocks; + + /// + /// This default value passed in to Rent to use the default value for the pool. + /// + private const int AnySize = -1; + + public DiagnosticMemoryPool(MemoryPool pool, bool allowLateReturn = false, bool rentTracking = false) + { + _pool = pool; + _allowLateReturn = allowLateReturn; + _rentTracking = rentTracking; + _blocks = new HashSet(); + _syncObj = new object(); + _allBlocksReturned = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _blockAccessExceptions = new List(); + } + + public bool IsDisposed { get; private set; } + + public override IMemoryOwner Rent(int size = AnySize) + { + lock (_syncObj) + { + if (IsDisposed) MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPool); + + var diagnosticPoolBlock = new DiagnosticPoolBlock(this, _pool.Rent(size)); + if (_rentTracking) diagnosticPoolBlock.Track(); + _totalBlocks++; + _blocks.Add(diagnosticPoolBlock); + return diagnosticPoolBlock; + } + } + + public override int MaxBufferSize => _pool.MaxBufferSize; + + internal void Return(DiagnosticPoolBlock block) + { + bool returnedAllBlocks; + lock (_syncObj) + { + _blocks.Remove(block); + returnedAllBlocks = _blocks.Count == 0; + } + + if (IsDisposed) + { + if (!_allowLateReturn) MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockReturnedToDisposedPool(block); + + if (returnedAllBlocks) SetAllBlocksReturned(); + } + + } + + internal void ReportException(Exception exception) + { + lock (_syncObj) + { + _blockAccessExceptions.Add(exception); + } + } + + protected override void Dispose(bool disposing) + { + if (IsDisposed) MemoryPoolThrowHelper.ThrowInvalidOperationException_DoubleDispose(); + + bool allBlocksReturned = false; + try + { + lock (_syncObj) + { + IsDisposed = true; + allBlocksReturned = _blocks.Count == 0; + if (!allBlocksReturned && !_allowLateReturn) MemoryPoolThrowHelper.ThrowInvalidOperationException_DisposingPoolWithActiveBlocks(_totalBlocks - _blocks.Count, _totalBlocks, _blocks.ToArray()); + + if (_blockAccessExceptions.Any()) throw CreateAccessExceptions(); + } + } + finally + { + if (allBlocksReturned) SetAllBlocksReturned(); + } + } + + private void SetAllBlocksReturned() + { + if (_blockAccessExceptions.Any()) + _allBlocksReturned.SetException(CreateAccessExceptions()); + else + _allBlocksReturned.SetResult(null); + } + + private AggregateException CreateAccessExceptions() => new AggregateException("Exceptions occurred while accessing blocks", _blockAccessExceptions.ToArray()); + + public async Task WhenAllBlocksReturnedAsync(TimeSpan timeout) + { + var task = await Task.WhenAny(_allBlocksReturned.Task, Task.Delay(timeout)); + if (task != _allBlocksReturned.Task) + MemoryPoolThrowHelper.ThrowInvalidOperationException_BlocksWereNotReturnedInTime(_totalBlocks - _blocks.Count, _totalBlocks, _blocks.ToArray()); + + await task; + } + } +} diff --git a/Projects/Server/Buffers/DiagnosticPoolBlock.cs b/Projects/Server/Buffers/DiagnosticPoolBlock.cs new file mode 100644 index 000000000..9623596f8 --- /dev/null +++ b/Projects/Server/Buffers/DiagnosticPoolBlock.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Threading; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.Buffers +{ + /// + /// Block tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The + /// individual blocks are then treated as independent array segments. + /// + public sealed class DiagnosticPoolBlock : MemoryManager + { + /// + /// Back-reference to the memory pool which this block was allocated from. It may only be returned to this pool. + /// + private readonly DiagnosticMemoryPool _pool; + + private readonly IMemoryOwner _memoryOwner; + private MemoryHandle? _memoryHandle; + private Memory _memory; + + private readonly object _syncObj = new object(); + private bool _isDisposed; + private int _pinCount; + + + /// + /// This object cannot be instantiated outside of the static Create method + /// + internal DiagnosticPoolBlock(DiagnosticMemoryPool pool, IMemoryOwner memoryOwner) + { + _pool = pool; + _memoryOwner = memoryOwner; + _memory = memoryOwner.Memory; + } + + public override Memory Memory + { + get + { + try + { + lock (_syncObj) + { + if (_isDisposed) MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPoolBlock); + + if (_pool.IsDisposed) MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(this); + + return CreateMemory(_memory.Length); + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + } + + protected override void Dispose(bool disposing) + { + try + { + lock (_syncObj) + { + if (Volatile.Read(ref _pinCount) > 0) MemoryPoolThrowHelper.ThrowInvalidOperationException_ReturningPinnedBlock(this); + + if (_isDisposed) MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockDoubleDispose(this); + + _memoryOwner.Dispose(); + + _pool.Return(this); + + _isDisposed = true; + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + public override Span GetSpan() + { + try + { + lock (_syncObj) + { + if (_isDisposed) MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPoolBlock); + + if (_pool.IsDisposed) MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(this); + + return _memory.Span; + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + public override MemoryHandle Pin(int byteOffset = 0) + { + try + { + lock (_syncObj) + { + if (_isDisposed) MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPoolBlock); + + if (_pool.IsDisposed) MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(this); + + if (byteOffset < 0 || byteOffset > _memory.Length) MemoryPoolThrowHelper.ThrowArgumentOutOfRangeException(_memory.Length, byteOffset); + + _pinCount++; + + _memoryHandle ??= _memory.Pin(); + + unsafe + { + return new MemoryHandle(((IntPtr)_memoryHandle.Value.Pointer + byteOffset).ToPointer(), default, this); + } + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + protected override bool TryGetArray(out ArraySegment segment) + { + try + { + lock (_syncObj) + { + if (_isDisposed) MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPoolBlock); + + if (_pool.IsDisposed) MemoryPoolThrowHelper.ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(this); + + return MemoryMarshal.TryGetArray(_memory, out segment); + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + public override void Unpin() + { + try + { + lock (_syncObj) + { + if (_pinCount == 0) MemoryPoolThrowHelper.ThrowInvalidOperationException_PinCountZero(this); + + _pinCount--; + + if (_pinCount == 0) + { + Debug.Assert(_memoryHandle.HasValue); + _memoryHandle.Value.Dispose(); + _memoryHandle = null; + } + } + } + catch (Exception exception) + { + _pool.ReportException(exception); + throw; + } + } + + public StackTrace Leaser { get; set; } + + public void Track() + { + Leaser = new StackTrace(false); + } + } +} diff --git a/Projects/Server/Buffers/MemoryPoolBlock.cs b/Projects/Server/Buffers/MemoryPoolBlock.cs new file mode 100644 index 000000000..2f1b87e4c --- /dev/null +++ b/Projects/Server/Buffers/MemoryPoolBlock.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace System.Buffers +{ + /// + /// Block tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The + /// individual blocks are then treated as independent array segments. + /// + public sealed class MemoryPoolBlock : IMemoryOwner + { + private readonly int _offset; + private readonly int _length; + + /// + /// This object cannot be instantiated outside of the static Create method + /// + internal MemoryPoolBlock(SlabMemoryPool pool, MemoryPoolSlab slab, int offset, int length) + { + _offset = offset; + _length = length; + + Pool = pool; + Slab = slab; + + Memory = MemoryMarshal.CreateFromPinnedArray(slab.Array, _offset, _length); + } + + /// + /// Back-reference to the memory pool which this block was allocated from. It may only be returned to this pool. + /// + public SlabMemoryPool Pool { get; } + + /// + /// Back-reference to the slab from which this block was taken, or null if it is one-time-use memory. + /// + public MemoryPoolSlab Slab { get; } + + public Memory Memory { get; } + + ~MemoryPoolBlock() + { + Pool.RefreshBlock(Slab, _offset, _length); + } + + public void Dispose() + { + Pool.Return(this); + } + + public void Lease() + { + } + } +} diff --git a/Projects/Server/Buffers/MemoryPoolFactory.cs b/Projects/Server/Buffers/MemoryPoolFactory.cs new file mode 100644 index 000000000..f8a3f37c5 --- /dev/null +++ b/Projects/Server/Buffers/MemoryPoolFactory.cs @@ -0,0 +1,19 @@ +// 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. + +namespace System.Buffers +{ + public static class SlabMemoryPoolFactory + { + public static MemoryPool Create() + { +#if DEBUG + return new DiagnosticMemoryPool(CreateSlabMemoryPool()); +#else + return CreateSlabMemoryPool(); +#endif + } + + public static MemoryPool CreateSlabMemoryPool() => new SlabMemoryPool(); + } +} diff --git a/Projects/Server/Buffers/MemoryPoolSlab.cs b/Projects/Server/Buffers/MemoryPoolSlab.cs new file mode 100644 index 000000000..38be52e5b --- /dev/null +++ b/Projects/Server/Buffers/MemoryPoolSlab.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Runtime.InteropServices; + +namespace System.Buffers +{ + /// + /// Slab tracking object used by the byte buffer memory pool. A slab is a large allocation which is divided into smaller blocks. The + /// individual blocks are then treated as independent array segments. + /// + public class MemoryPoolSlab : IDisposable + { + /// + /// This handle pins the managed array in memory until the slab is disposed. This prevents it from being + /// relocated and enables any subsections of the array to be used as native memory pointers to P/Invoked API calls. + /// + private GCHandle _gcHandle; + private bool _isDisposed; + + public MemoryPoolSlab(byte[] data) + { + Array = data; + _gcHandle = GCHandle.Alloc(data, GCHandleType.Pinned); + NativePointer = _gcHandle.AddrOfPinnedObject(); + } + + /// + /// True as long as the blocks from this slab are to be considered returnable to the pool. In order to shrink the + /// memory pool size an entire slab must be removed. That is done by (1) setting IsActive to false and removing the + /// slab from the pool's _slabs collection, (2) as each block currently in use is Return()ed to the pool it will + /// be allowed to be garbage collected rather than re-pooled, and (3) when all block tracking objects are garbage + /// collected and the slab is no longer references the slab will be garbage collected and the memory unpinned will + /// be unpinned by the slab's Dispose. + /// + public bool IsActive => !_isDisposed; + + public IntPtr NativePointer { get; private set; } + + public byte[] Array { get; private set; } + + public static MemoryPoolSlab Create(int length) + { + // allocate and pin requested memory length + var array = new byte[length]; + + // allocate and return slab tracking object + return new MemoryPoolSlab(array); + } + + protected void Dispose(bool disposing) + { + if (_isDisposed) return; + + _isDisposed = true; + + Array = null; + NativePointer = IntPtr.Zero;; + + if (_gcHandle.IsAllocated) _gcHandle.Free(); + } + + ~MemoryPoolSlab() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Projects/Server/Buffers/MemoryPoolThrowHelper.cs b/Projects/Server/Buffers/MemoryPoolThrowHelper.cs new file mode 100644 index 000000000..cf6284148 --- /dev/null +++ b/Projects/Server/Buffers/MemoryPoolThrowHelper.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; + +namespace System.Buffers +{ + public class MemoryPoolThrowHelper + { + public static void ThrowArgumentOutOfRangeException(int sourceLength, int offset) + { + throw GetArgumentOutOfRangeException(sourceLength, offset); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException(int sourceLength, int offset) => + (uint)offset > (uint)sourceLength ? new ArgumentOutOfRangeException(GetArgumentName(ExceptionArgument.offset)) : new ArgumentOutOfRangeException(GetArgumentName(ExceptionArgument.length)); + public static void ThrowInvalidOperationException_PinCountZero(DiagnosticPoolBlock block) + { + throw new InvalidOperationException(GenerateMessage("Can't unpin, pin count is zero", block)); + } + + public static void ThrowInvalidOperationException_ReturningPinnedBlock(DiagnosticPoolBlock block) + { + throw new InvalidOperationException(GenerateMessage("Disposing pinned block", block)); + } + + public static void ThrowInvalidOperationException_DoubleDispose() + { + throw new InvalidOperationException("Object is being disposed twice"); + } + + public static void ThrowInvalidOperationException_BlockDoubleDispose(DiagnosticPoolBlock block) + { + throw new InvalidOperationException("Block is being disposed twice"); + } + + public static void ThrowInvalidOperationException_BlockReturnedToDisposedPool(DiagnosticPoolBlock block) + { + throw new InvalidOperationException(GenerateMessage("Block is being returned to disposed pool", block)); + } + + public static void ThrowInvalidOperationException_BlockIsBackedByDisposedSlab(DiagnosticPoolBlock block) + { + throw new InvalidOperationException(GenerateMessage("Block is backed by disposed slab", block)); + } + + public static void ThrowInvalidOperationException_DisposingPoolWithActiveBlocks(int returned, int total, DiagnosticPoolBlock[] blocks) + { + throw new InvalidOperationException(GenerateMessage($"Memory pool with active blocks is being disposed, {returned} of {total} returned", blocks)); + } + + public static void ThrowInvalidOperationException_BlocksWereNotReturnedInTime(int returned, int total, DiagnosticPoolBlock[] blocks) + { + throw new InvalidOperationException(GenerateMessage($"Blocks were not returned in time, {returned} of {total} returned ", blocks)); + } + + private static string GenerateMessage(string message, params DiagnosticPoolBlock[] blocks) + { + StringBuilder builder = new StringBuilder(message); + foreach (var diagnosticPoolBlock in blocks) + if (diagnosticPoolBlock.Leaser != null) + { + builder.AppendLine(); + + builder.AppendLine("Block leased from:"); + builder.AppendLine(diagnosticPoolBlock.Leaser.ToString()); + } + + return builder.ToString(); + } + + public static void ThrowArgumentOutOfRangeException_BufferRequestTooLarge(int maxSize) + { + throw GetArgumentOutOfRangeException_BufferRequestTooLarge(maxSize); + } + + public static void ThrowObjectDisposedException(ExceptionArgument argument) + { + throw GetObjectDisposedException(argument); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ArgumentOutOfRangeException GetArgumentOutOfRangeException_BufferRequestTooLarge(int maxSize) => new ArgumentOutOfRangeException(GetArgumentName(ExceptionArgument.size), $"Cannot allocate more than {maxSize} bytes in a single buffer"); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static ObjectDisposedException GetObjectDisposedException(ExceptionArgument argument) => new ObjectDisposedException(GetArgumentName(argument)); + + private static string GetArgumentName(ExceptionArgument argument) + { + Debug.Assert(Enum.IsDefined(typeof(ExceptionArgument), argument), "The enum value is not defined, please check the ExceptionArgument Enum."); + + return argument.ToString(); + } + + public enum ExceptionArgument + { + size, + offset, + length, + MemoryPoolBlock, + MemoryPool + } + } +} diff --git a/Projects/Server/Buffers/SlabMemoryPool.cs b/Projects/Server/Buffers/SlabMemoryPool.cs new file mode 100644 index 000000000..acde6b894 --- /dev/null +++ b/Projects/Server/Buffers/SlabMemoryPool.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Threading; + +namespace System.Buffers +{ + /// + /// Used to allocate and distribute re-usable blocks of memory. + /// + public sealed class SlabMemoryPool : MemoryPool + { + /// + /// The size of a block. 4096 is chosen because most operating systems use 4k pages. + /// + private const int _blockSize = 4096; + + /// + /// Allocating 32 contiguous blocks per slab makes the slab size 128k. This is larger than the 85k size which will place the memory + /// in the large object heap. This means the GC will not try to relocate this array, so the fact it remains pinned does not negatively + /// affect memory management's compactification. + /// + private const int _blockCount = 32; + + /// + /// Max allocation block size for pooled blocks, + /// larger values can be leased but they will be disposed after use rather than returned to the pool. + /// + public override int MaxBufferSize { get; } = _blockSize; + + /// + /// The size of a block. 4096 is chosen because most operating systems use 4k pages. + /// + public static int BlockSize => _blockSize; + + /// + /// 4096 * 32 gives you a slabLength of 128k contiguous bytes allocated per slab + /// + private static readonly int _slabLength = _blockSize * _blockCount; + + /// + /// Thread-safe collection of blocks which are currently in the pool. A slab will pre-allocate all of the block tracking objects + /// and add them to this collection. When memory is requested it is taken from here first, and when it is returned it is re-added. + /// + private readonly ConcurrentQueue _blocks = new ConcurrentQueue(); + + /// + /// Thread-safe collection of slabs which have been allocated by this pool. As long as a slab is in this collection and slab.IsActive, + /// the blocks will be added to _blocks when returned. + /// + private readonly ConcurrentStack _slabs = new ConcurrentStack(); + + /// + /// This is part of implementing the IDisposable pattern. + /// + private bool _isDisposed; // To detect redundant calls + + private int _totalAllocatedBlocks; + + private readonly object _disposeSync = new object(); + + /// + /// This default value passed in to Rent to use the default value for the pool. + /// + private const int AnySize = -1; + + public override IMemoryOwner Rent(int size = AnySize) + { + if (size > _blockSize) MemoryPoolThrowHelper.ThrowArgumentOutOfRangeException_BufferRequestTooLarge(_blockSize); + + var block = Lease(); + return block; + } + + /// + /// Called to take a block from the pool. + /// + /// The block that is reserved for the called. It must be passed to Return when it is no longer being used. + private MemoryPoolBlock Lease() + { + if (_isDisposed) MemoryPoolThrowHelper.ThrowObjectDisposedException(MemoryPoolThrowHelper.ExceptionArgument.MemoryPool); + + if (_blocks.TryDequeue(out MemoryPoolBlock block)) + { + // block successfully taken from the stack - return it + + block.Lease(); + return block; + } + // no blocks available - grow the pool + block = AllocateSlab(); + block.Lease(); + return block; + } + + /// + /// Internal method called when a block is requested and the pool is empty. It allocates one additional slab, creates all of the + /// block tracking objects, and adds them all to the pool. + /// + private MemoryPoolBlock AllocateSlab() + { + var slab = MemoryPoolSlab.Create(_slabLength); + _slabs.Push(slab); + + var basePtr = slab.NativePointer; + // Page align the blocks + var offset = (int)((((ulong)basePtr + (uint)_blockSize - 1) & ~((uint)_blockSize - 1)) - (ulong)basePtr); + // Ensure page aligned + Debug.Assert(((ulong)basePtr + (uint)offset) % _blockSize == 0); + + var blockCount = (_slabLength - offset) / _blockSize; + Interlocked.Add(ref _totalAllocatedBlocks, blockCount); + + MemoryPoolBlock block = null; + + for (int i = 0; i < blockCount; i++) + { + block = new MemoryPoolBlock(this, slab, offset, _blockSize); + + if (i != blockCount - 1) // last block + { +#if BLOCK_LEASE_TRACKING + block.IsLeased = true; +#endif + Return(block); + } + + offset += _blockSize; + } + + return block; + } + + /// + /// Called to return a block to the pool. Once Return has been called the memory no longer belongs to the caller, and + /// Very Bad Things will happen if the memory is read of modified subsequently. If a caller fails to call Return and the + /// block tracking object is garbage collected, the block tracking object's finalizer will automatically re-create and return + /// a new tracking object into the pool. This will only happen if there is a bug in the server, however it is necessary to avoid + /// leaving "dead zones" in the slab due to lost block tracking objects. + /// + /// The block to return. It must have been acquired by calling Lease on the same memory pool instance. + internal void Return(MemoryPoolBlock block) + { +#if BLOCK_LEASE_TRACKING + Debug.Assert(block.Pool == this, "Returned block was not leased from this pool"); + Debug.Assert(block.IsLeased, $"Block being returned to pool twice: {block.Leaser}{Environment.NewLine}"); + block.IsLeased = false; +#endif + + if (!_isDisposed) + _blocks.Enqueue(block); + else + GC.SuppressFinalize(block); + } + + // This method can ONLY be called from the finalizer of MemoryPoolBlock + internal void RefreshBlock(MemoryPoolSlab slab, int offset, int length) + { + lock (_disposeSync) + { + if (!_isDisposed && slab?.IsActive == true) + // Need to make a new object because this one is being finalized + // Note, this must be called within the _disposeSync lock because the block + // could be disposed at the same time as the finalizer. + Return(new MemoryPoolBlock(this, slab, offset, length)); + } + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) return; + + lock (_disposeSync) + { + _isDisposed = true; + + if (disposing) + while (_slabs.TryPop(out MemoryPoolSlab slab)) + // dispose managed state (managed objects). + slab.Dispose(); + + // Discard blocks in pool + while (_blocks.TryDequeue(out MemoryPoolBlock block)) GC.SuppressFinalize(block); + } + } + } +} diff --git a/Projects/Server/Buffers/SpanWriter.cs b/Projects/Server/Buffers/SpanWriter.cs index 8634d9e75..4103f462d 100644 --- a/Projects/Server/Buffers/SpanWriter.cs +++ b/Projects/Server/Buffers/SpanWriter.cs @@ -1,22 +1,22 @@ -/*************************************************************************** - * SpanWriter.cs - * ------------------- - * begin : August 5, 2019 - * copyright : (C) The ModernUO Team - * email : hi@modernuo.com - * - * $Id$ - * - ***************************************************************************/ - -/*************************************************************************** - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - ***************************************************************************/ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SpanWriter.cs - Created: 2019/08/05 - Updated: 2019/12/24 * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ using System; using System.Text; diff --git a/Projects/Server/EventSink.cs b/Projects/Server/EventSink.cs index 0d12f3433..f93416e65 100644 --- a/Projects/Server/EventSink.cs +++ b/Projects/Server/EventSink.cs @@ -22,6 +22,7 @@ using System; using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using Microsoft.AspNetCore.Connections; using Server.Accounting; using Server.Guilds; using Server.Network; @@ -380,13 +381,13 @@ namespace Server public class SocketConnectEventArgs : EventArgs { - public SocketConnectEventArgs(Socket s) + public SocketConnectEventArgs(ConnectionContext c) { - Socket = s; + Context = c; AllowConnection = true; } - public Socket Socket{ get; } + public ConnectionContext Context{ get; } public bool AllowConnection{ get; set; } } diff --git a/Projects/Server/Kestrel/CorrelationIdGenerator.cs b/Projects/Server/Kestrel/CorrelationIdGenerator.cs new file mode 100644 index 000000000..1be328db1 --- /dev/null +++ b/Projects/Server/Kestrel/CorrelationIdGenerator.cs @@ -0,0 +1,43 @@ +// 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.Threading; + +namespace Microsoft.AspNetCore.Connections +{ + public static class CorrelationIdGenerator + { + // Base32 encoding - in ascii sort order for easy text based sorting + private static readonly char[] s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV".ToCharArray(); + + // Seed the _lastConnectionId for this application instance with + // the number of 100-nanosecond intervals that have elapsed since 12:00:00 midnight, January 1, 0001 + // for a roughly increasing _lastId over restarts + private static long _lastId = DateTime.UtcNow.Ticks; + + public static string GetNextId() => GenerateId(Interlocked.Increment(ref _lastId)); + + private static string GenerateId(long id) + { + return string.Create(13, id, (buffer, value) => + { + char[] encode32Chars = s_encode32Chars; + + buffer[12] = encode32Chars[value & 31]; + buffer[11] = encode32Chars[(value >> 5) & 31]; + buffer[10] = encode32Chars[(value >> 10) & 31]; + buffer[9] = encode32Chars[(value >> 15) & 31]; + buffer[8] = encode32Chars[(value >> 20) & 31]; + buffer[7] = encode32Chars[(value >> 25) & 31]; + buffer[6] = encode32Chars[(value >> 30) & 31]; + buffer[5] = encode32Chars[(value >> 35) & 31]; + buffer[4] = encode32Chars[(value >> 40) & 31]; + buffer[3] = encode32Chars[(value >> 45) & 31]; + buffer[2] = encode32Chars[(value >> 50) & 31]; + buffer[1] = encode32Chars[(value >> 55) & 31]; + buffer[0] = encode32Chars[(value >> 60) & 31]; + }); + } + } +} diff --git a/Projects/Server/Kestrel/TransportConnection.FeatureCollection.cs b/Projects/Server/Kestrel/TransportConnection.FeatureCollection.cs new file mode 100644 index 000000000..70033b7c9 --- /dev/null +++ b/Projects/Server/Kestrel/TransportConnection.FeatureCollection.cs @@ -0,0 +1,44 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.IO.Pipelines; +using System.Threading; +using Microsoft.AspNetCore.Connections.Features; + +namespace Microsoft.AspNetCore.Connections +{ + public partial class TransportConnection : IConnectionIdFeature, + IConnectionTransportFeature, + IConnectionItemsFeature, + IMemoryPoolFeature, + IConnectionLifetimeFeature + { + // NOTE: When feature interfaces are added to or removed from this TransportConnection class implementation, + // then the list of `features` in the generated code project MUST also be updated. + // See also: tools/CodeGenerator/TransportConnectionFeatureCollection.cs + + MemoryPool IMemoryPoolFeature.MemoryPool => MemoryPool; + + IDuplexPipe IConnectionTransportFeature.Transport + { + get => Transport; + set => Transport = value; + } + + IDictionary IConnectionItemsFeature.Items + { + get => Items; + set => Items = value; + } + + CancellationToken IConnectionLifetimeFeature.ConnectionClosed + { + get => ConnectionClosed; + set => ConnectionClosed = value; + } + + void IConnectionLifetimeFeature.Abort() => Abort(new ConnectionAbortedException("The connection was aborted by the application via IConnectionLifetimeFeature.Abort().")); + } +} diff --git a/Projects/Server/Kestrel/TransportConnection.Generated.cs b/Projects/Server/Kestrel/TransportConnection.Generated.cs new file mode 100644 index 000000000..eb6f2ba25 --- /dev/null +++ b/Projects/Server/Kestrel/TransportConnection.Generated.cs @@ -0,0 +1,248 @@ +// 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.Collections; +using System.Collections.Generic; + +using Microsoft.AspNetCore.Connections.Features; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections +{ + public partial class TransportConnection : IFeatureCollection + { + private static readonly Type IConnectionIdFeatureType = typeof(IConnectionIdFeature); + private static readonly Type IConnectionTransportFeatureType = typeof(IConnectionTransportFeature); + private static readonly Type IConnectionItemsFeatureType = typeof(IConnectionItemsFeature); + private static readonly Type IMemoryPoolFeatureType = typeof(IMemoryPoolFeature); + private static readonly Type IConnectionLifetimeFeatureType = typeof(IConnectionLifetimeFeature); + + private object _currentIConnectionIdFeature; + private object _currentIConnectionTransportFeature; + private object _currentIConnectionItemsFeature; + private object _currentIMemoryPoolFeature; + private object _currentIConnectionLifetimeFeature; + + private int _featureRevision; + + private List> MaybeExtra; + + private void FastReset() + { + _currentIConnectionIdFeature = this; + _currentIConnectionTransportFeature = this; + _currentIConnectionItemsFeature = this; + _currentIMemoryPoolFeature = this; + _currentIConnectionLifetimeFeature = this; + + } + + // Internal for testing + internal void ResetFeatureCollection() + { + FastReset(); + MaybeExtra?.Clear(); + _featureRevision++; + } + + private object ExtraFeatureGet(Type key) + { + if (MaybeExtra == null) + { + return null; + } + for (var i = 0; i < MaybeExtra.Count; i++) + { + var kv = MaybeExtra[i]; + if (kv.Key == key) + { + return kv.Value; + } + } + return null; + } + + private void ExtraFeatureSet(Type key, object value) + { + if (MaybeExtra == null) + { + MaybeExtra = new List>(2); + } + + for (var i = 0; i < MaybeExtra.Count; i++) + { + if (MaybeExtra[i].Key == key) + { + MaybeExtra[i] = new KeyValuePair(key, value); + return; + } + } + MaybeExtra.Add(new KeyValuePair(key, value)); + } + + bool IFeatureCollection.IsReadOnly => false; + + int IFeatureCollection.Revision => _featureRevision; + + object IFeatureCollection.this[Type key] + { + get + { + object feature = null; + if (key == IConnectionIdFeatureType) + { + feature = _currentIConnectionIdFeature; + } + else if (key == IConnectionTransportFeatureType) + { + feature = _currentIConnectionTransportFeature; + } + else if (key == IConnectionItemsFeatureType) + { + feature = _currentIConnectionItemsFeature; + } + else if (key == IMemoryPoolFeatureType) + { + feature = _currentIMemoryPoolFeature; + } + else if (key == IConnectionLifetimeFeatureType) + { + feature = _currentIConnectionLifetimeFeature; + } + else if (MaybeExtra != null) + { + feature = ExtraFeatureGet(key); + } + + return feature; + } + + set + { + _featureRevision++; + + if (key == IConnectionIdFeatureType) + { + _currentIConnectionIdFeature = value; + } + else if (key == IConnectionTransportFeatureType) + { + _currentIConnectionTransportFeature = value; + } + else if (key == IConnectionItemsFeatureType) + { + _currentIConnectionItemsFeature = value; + } + else if (key == IMemoryPoolFeatureType) + { + _currentIMemoryPoolFeature = value; + } + else if (key == IConnectionLifetimeFeatureType) + { + _currentIConnectionLifetimeFeature = value; + } + else + { + ExtraFeatureSet(key, value); + } + } + } + + TFeature IFeatureCollection.Get() + { + TFeature feature = default; + if (typeof(TFeature) == typeof(IConnectionIdFeature)) + { + feature = (TFeature)_currentIConnectionIdFeature; + } + else if (typeof(TFeature) == typeof(IConnectionTransportFeature)) + { + feature = (TFeature)_currentIConnectionTransportFeature; + } + else if (typeof(TFeature) == typeof(IConnectionItemsFeature)) + { + feature = (TFeature)_currentIConnectionItemsFeature; + } + else if (typeof(TFeature) == typeof(IMemoryPoolFeature)) + { + feature = (TFeature)_currentIMemoryPoolFeature; + } + else if (typeof(TFeature) == typeof(IConnectionLifetimeFeature)) + { + feature = (TFeature)_currentIConnectionLifetimeFeature; + } + else if (MaybeExtra != null) + { + feature = (TFeature)(ExtraFeatureGet(typeof(TFeature))); + } + + return feature; + } + + void IFeatureCollection.Set(TFeature feature) + { + _featureRevision++; + if (typeof(TFeature) == typeof(IConnectionIdFeature)) + { + _currentIConnectionIdFeature = feature; + } + else if (typeof(TFeature) == typeof(IConnectionTransportFeature)) + { + _currentIConnectionTransportFeature = feature; + } + else if (typeof(TFeature) == typeof(IConnectionItemsFeature)) + { + _currentIConnectionItemsFeature = feature; + } + else if (typeof(TFeature) == typeof(IMemoryPoolFeature)) + { + _currentIMemoryPoolFeature = feature; + } + else if (typeof(TFeature) == typeof(IConnectionLifetimeFeature)) + { + _currentIConnectionLifetimeFeature = feature; + } + else + { + ExtraFeatureSet(typeof(TFeature), feature); + } + } + + private IEnumerable> FastEnumerable() + { + if (_currentIConnectionIdFeature != null) + { + yield return new KeyValuePair(IConnectionIdFeatureType, _currentIConnectionIdFeature); + } + if (_currentIConnectionTransportFeature != null) + { + yield return new KeyValuePair(IConnectionTransportFeatureType, _currentIConnectionTransportFeature); + } + if (_currentIConnectionItemsFeature != null) + { + yield return new KeyValuePair(IConnectionItemsFeatureType, _currentIConnectionItemsFeature); + } + if (_currentIMemoryPoolFeature != null) + { + yield return new KeyValuePair(IMemoryPoolFeatureType, _currentIMemoryPoolFeature); + } + if (_currentIConnectionLifetimeFeature != null) + { + yield return new KeyValuePair(IConnectionLifetimeFeatureType, _currentIConnectionLifetimeFeature); + } + + if (MaybeExtra != null) + { + foreach (var item in MaybeExtra) + { + yield return item; + } + } + } + + IEnumerator> IEnumerable>.GetEnumerator() => FastEnumerable().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => FastEnumerable().GetEnumerator(); + } +} diff --git a/Projects/Server/Kestrel/TransportConnection.cs b/Projects/Server/Kestrel/TransportConnection.cs new file mode 100644 index 000000000..fcd066608 --- /dev/null +++ b/Projects/Server/Kestrel/TransportConnection.cs @@ -0,0 +1,59 @@ +// 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.Buffers; +using System.Collections.Generic; +using System.IO.Pipelines; +using System.Net; +using System.Threading; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Connections +{ + public abstract partial class TransportConnection : ConnectionContext + { + private IDictionary _items; + private string _connectionId; + + public TransportConnection() + { + FastReset(); + } + + public override EndPoint LocalEndPoint { get; set; } + public override EndPoint RemoteEndPoint { get; set; } + + public override string ConnectionId + { + get => _connectionId ??= CorrelationIdGenerator.GetNextId(); + set => _connectionId = value; + } + + public override IFeatureCollection Features => this; + + public virtual MemoryPool MemoryPool { get; } + + public override IDuplexPipe Transport { get; set; } + + public IDuplexPipe Application { get; set; } + + public override IDictionary Items + { + get => _items ??= new ConnectionItems(); + set => _items = value; + } + + public override CancellationToken ConnectionClosed { get; set; } + + // DO NOT remove this override to ConnectionContext.Abort. Doing so would cause + // any TransportConnection that does not override Abort or calls base.Abort + // to stack overflow when IConnectionLifetimeFeature.Abort() is called. + // That said, all derived types should override this method should override + // this implementation of Abort because canceling pending output reads is not + // sufficient to abort the connection if there is backpressure. + public override void Abort(ConnectionAbortedException abortReason) + { + Application.Input.CancelPendingRead(); + } + } +} diff --git a/Projects/Server/LibUv/IAsyncDisposable.cs b/Projects/Server/LibUv/IAsyncDisposable.cs new file mode 100644 index 000000000..70f4a53a5 --- /dev/null +++ b/Projects/Server/LibUv/IAsyncDisposable.cs @@ -0,0 +1,12 @@ +// 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.Threading.Tasks; + +namespace Libuv +{ + interface IAsyncDisposable + { + Task DisposeAsync(); + } +} diff --git a/Projects/Server/LibUv/ILibuvTrace.cs b/Projects/Server/LibUv/ILibuvTrace.cs new file mode 100644 index 000000000..371018cd2 --- /dev/null +++ b/Projects/Server/LibUv/ILibuvTrace.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.Extensions.Logging; + +namespace Libuv +{ + public interface ILibuvTrace : ILogger + { + void ConnectionRead(string connectionId, int count); + + void ConnectionReadFin(string connectionId); + + void ConnectionWriteFin(string connectionId, string reason); + + void ConnectionWrite(string connectionId, int count); + + void ConnectionWriteCallback(string connectionId, int status); + + void ConnectionError(string connectionId, Exception ex); + + void ConnectionReset(string connectionId); + + void ConnectionPause(string connectionId); + + void ConnectionResume(string connectionId); + } +} diff --git a/Projects/Server/LibUv/Internal/LibuvFunctions.cs b/Projects/Server/LibUv/Internal/LibuvFunctions.cs new file mode 100644 index 000000000..f215c0ae6 --- /dev/null +++ b/Projects/Server/LibUv/Internal/LibuvFunctions.cs @@ -0,0 +1,612 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Server; + +namespace Libuv.Internal +{ + public class LibuvFunctions + { + public LibuvFunctions() + { + _uv_loop_init = NativeMethods.uv_loop_init; + _uv_loop_close = NativeMethods.uv_loop_close; + _uv_run = NativeMethods.uv_run; + _uv_stop = NativeMethods.uv_stop; + _uv_ref = NativeMethods.uv_ref; + _uv_unref = NativeMethods.uv_unref; + _uv_fileno = NativeMethods.uv_fileno; + _uv_close = NativeMethods.uv_close; + _uv_async_init = NativeMethods.uv_async_init; + _uv_async_send = NativeMethods.uv_async_send; + _uv_unsafe_async_send = NativeMethods.uv_unsafe_async_send; + _uv_tcp_init = NativeMethods.uv_tcp_init; + _uv_tcp_bind = NativeMethods.uv_tcp_bind; + _uv_tcp_open = NativeMethods.uv_tcp_open; + _uv_tcp_nodelay = NativeMethods.uv_tcp_nodelay; + _uv_pipe_init = NativeMethods.uv_pipe_init; + _uv_pipe_bind = NativeMethods.uv_pipe_bind; + _uv_pipe_open = NativeMethods.uv_pipe_open; + _uv_listen = NativeMethods.uv_listen; + _uv_accept = NativeMethods.uv_accept; + _uv_pipe_connect = NativeMethods.uv_pipe_connect; + _uv_pipe_pending_count = NativeMethods.uv_pipe_pending_count; + _uv_read_start = NativeMethods.uv_read_start; + _uv_read_stop = NativeMethods.uv_read_stop; + _uv_try_write = NativeMethods.uv_try_write; + unsafe + { + _uv_write = NativeMethods.uv_write; + _uv_write2 = NativeMethods.uv_write2; + } + _uv_err_name = NativeMethods.uv_err_name; + _uv_strerror = NativeMethods.uv_strerror; + _uv_loop_size = NativeMethods.uv_loop_size; + _uv_handle_size = NativeMethods.uv_handle_size; + _uv_req_size = NativeMethods.uv_req_size; + _uv_ip4_addr = NativeMethods.uv_ip4_addr; + _uv_ip6_addr = NativeMethods.uv_ip6_addr; + _uv_tcp_getpeername = NativeMethods.uv_tcp_getpeername; + _uv_tcp_getsockname = NativeMethods.uv_tcp_getsockname; + _uv_walk = NativeMethods.uv_walk; + _uv_timer_init = NativeMethods.uv_timer_init; + _uv_timer_start = NativeMethods.uv_timer_start; + _uv_timer_stop = NativeMethods.uv_timer_stop; + _uv_now = NativeMethods.uv_now; + } + + // Second ctor that doesn't set any fields only to be used by MockLibuv + public LibuvFunctions(bool onlyForTesting) + { + } + + public void ThrowIfErrored(int statusCode) + { + // Note: method is explicitly small so the success case is easily inlined + if (statusCode < 0) ThrowError(statusCode); + } + + private void ThrowError(int statusCode) + { + // Note: only has one throw block so it will marked as "Does not return" by the jit + // and not inlined into previous function, while also marking as a function + // that does not need cpu register prep to call (see: https://github.com/dotnet/coreclr/pull/6103) + throw GetError(statusCode); + } + + public void Check(int statusCode, out UvException error) + { + // Note: method is explicitly small so the success case is easily inlined + error = statusCode < 0 ? GetError(statusCode) : null; + } + + // Note: method marked as NoInlining so it doesn't bloat either of the two preceding functions + // Check and ThrowError and alter their jit heuristics. + [MethodImpl(MethodImplOptions.NoInlining)] + private UvException GetError(int statusCode) => + new UvException($"Error {statusCode} {err_name(statusCode)} {strerror(statusCode)}", statusCode); + + public Func _uv_loop_init; + public void loop_init(UvLoopHandle handle) + { + ThrowIfErrored(_uv_loop_init(handle)); + } + + public Func _uv_loop_close; + public void loop_close(UvLoopHandle handle) + { + handle.Validate(true); + ThrowIfErrored(_uv_loop_close(handle.InternalGetHandle())); + } + + public Func _uv_run; + public void run(UvLoopHandle handle, int mode) + { + handle.Validate(); + ThrowIfErrored(_uv_run(handle, mode)); + } + + public Action _uv_stop; + public void stop(UvLoopHandle handle) + { + handle.Validate(); + _uv_stop(handle); + } + + public Action _uv_ref; + public void @ref(UvHandle handle) + { + handle.Validate(); + _uv_ref(handle); + } + + public Action _uv_unref; + public void unref(UvHandle handle) + { + handle.Validate(); + _uv_unref(handle); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate int uv_fileno_func(UvHandle handle, ref IntPtr socket); + public uv_fileno_func _uv_fileno; + public void uv_fileno(UvHandle handle, ref IntPtr socket) + { + handle.Validate(); + ThrowIfErrored(_uv_fileno(handle, ref socket)); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_close_cb(IntPtr handle); + public Action _uv_close; + public void close(UvHandle handle, uv_close_cb close_cb) + { + handle.Validate(true); + _uv_close(handle.InternalGetHandle(), close_cb); + } + + public void close(IntPtr handle, uv_close_cb close_cb) + { + _uv_close(handle, close_cb); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_async_cb(IntPtr handle); + public Func _uv_async_init; + public void async_init(UvLoopHandle loop, UvAsyncHandle handle, uv_async_cb cb) + { + loop.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_async_init(loop, handle, cb)); + } + + public Func _uv_async_send; + public void async_send(UvAsyncHandle handle) + { + ThrowIfErrored(_uv_async_send(handle)); + } + + public Func _uv_unsafe_async_send; + public void unsafe_async_send(IntPtr handle) + { + ThrowIfErrored(_uv_unsafe_async_send(handle)); + } + + public Func _uv_tcp_init; + public void tcp_init(UvLoopHandle loop, UvTcpHandle handle) + { + loop.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_tcp_init(loop, handle)); + } + + public delegate int uv_tcp_bind_func(UvTcpHandle handle, ref SockAddr addr, int flags); + public uv_tcp_bind_func _uv_tcp_bind; + public void tcp_bind(UvTcpHandle handle, ref SockAddr addr, int flags) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_bind(handle, ref addr, flags)); + } + + public Func _uv_tcp_open; + public void tcp_open(UvTcpHandle handle, IntPtr hSocket) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_open(handle, hSocket)); + } + + public Func _uv_tcp_nodelay; + public void tcp_nodelay(UvTcpHandle handle, bool enable) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_nodelay(handle, enable ? 1 : 0)); + } + + public Func _uv_pipe_init; + public void pipe_init(UvLoopHandle loop, UvPipeHandle handle, bool ipc) + { + loop.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_pipe_init(loop, handle, ipc ? -1 : 0)); + } + + public Func _uv_pipe_bind; + public void pipe_bind(UvPipeHandle handle, string name) + { + handle.Validate(); + ThrowIfErrored(_uv_pipe_bind(handle, name)); + } + + public Func _uv_pipe_open; + public void pipe_open(UvPipeHandle handle, IntPtr hSocket) + { + handle.Validate(); + ThrowIfErrored(_uv_pipe_open(handle, hSocket)); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_connection_cb(IntPtr server, int status); + public Func _uv_listen; + public void listen(UvStreamHandle handle, int backlog, uv_connection_cb cb) + { + handle.Validate(); + ThrowIfErrored(_uv_listen(handle, backlog, cb)); + } + + public Func _uv_accept; + public void accept(UvStreamHandle server, UvStreamHandle client) + { + server.Validate(); + client.Validate(); + ThrowIfErrored(_uv_accept(server, client)); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_connect_cb(IntPtr req, int status); + public Action _uv_pipe_connect; + public void pipe_connect(UvConnectRequest req, UvPipeHandle handle, string name, uv_connect_cb cb) + { + req.Validate(); + handle.Validate(); + _uv_pipe_connect(req, handle, name, cb); + } + + public Func _uv_pipe_pending_count; + public int pipe_pending_count(UvPipeHandle handle) + { + handle.Validate(); + return _uv_pipe_pending_count(handle); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_alloc_cb(IntPtr server, int suggested_size, out uv_buf_t buf); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_read_cb(IntPtr server, int nread, ref uv_buf_t buf); + public Func _uv_read_start; + public void read_start(UvStreamHandle handle, uv_alloc_cb alloc_cb, uv_read_cb read_cb) + { + handle.Validate(); + ThrowIfErrored(_uv_read_start(handle, alloc_cb, read_cb)); + } + + public Func _uv_read_stop; + public void read_stop(UvStreamHandle handle) + { + handle.Validate(); + ThrowIfErrored(_uv_read_stop(handle)); + } + + public Func _uv_try_write; + public int try_write(UvStreamHandle handle, uv_buf_t[] bufs, int nbufs) + { + handle.Validate(); + var count = _uv_try_write(handle, bufs, nbufs); + ThrowIfErrored(count); + return count; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_write_cb(IntPtr req, int status); + + public unsafe delegate int uv_write_func(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, uv_write_cb cb); + public uv_write_func _uv_write; + public unsafe void write(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, uv_write_cb cb) + { + req.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_write(req, handle, bufs, nbufs, cb)); + } + + public unsafe delegate int uv_write2_func(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, UvStreamHandle sendHandle, uv_write_cb cb); + public uv_write2_func _uv_write2; + public unsafe void write2(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, UvStreamHandle sendHandle, uv_write_cb cb) + { + req.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_write2(req, handle, bufs, nbufs, sendHandle, cb)); + } + + public Func _uv_err_name; + public string err_name(int err) + { + IntPtr ptr = _uv_err_name(err); + return ptr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(ptr); + } + + public Func _uv_strerror; + public string strerror(int err) + { + IntPtr ptr = _uv_strerror(err); + return ptr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(ptr); + } + + public Func _uv_loop_size; + public int loop_size() => _uv_loop_size(); + + public Func _uv_handle_size; + public int handle_size(HandleType handleType) => _uv_handle_size(handleType); + + public Func _uv_req_size; + public int req_size(RequestType reqType) => _uv_req_size(reqType); + + public delegate int uv_ip4_addr_func(string ip, int port, out SockAddr addr); + public uv_ip4_addr_func _uv_ip4_addr; + public void ip4_addr(string ip, int port, out SockAddr addr, out UvException error) + { + Check(_uv_ip4_addr(ip, port, out addr), out error); + } + + public delegate int uv_ip6_addr_func(string ip, int port, out SockAddr addr); + public uv_ip6_addr_func _uv_ip6_addr; + public void ip6_addr(string ip, int port, out SockAddr addr, out UvException error) + { + Check(_uv_ip6_addr(ip, port, out addr), out error); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_walk_cb(IntPtr handle, IntPtr arg); + public Func _uv_walk; + public void walk(UvLoopHandle loop, uv_walk_cb walk_cb, IntPtr arg) + { + loop.Validate(); + _uv_walk(loop, walk_cb, arg); + } + + public Func _uv_timer_init; + public void timer_init(UvLoopHandle loop, UvTimerHandle handle) + { + loop.Validate(); + handle.Validate(); + ThrowIfErrored(_uv_timer_init(loop, handle)); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void uv_timer_cb(IntPtr handle); + public Func _uv_timer_start; + public void timer_start(UvTimerHandle handle, uv_timer_cb cb, long timeout, long repeat) + { + handle.Validate(); + ThrowIfErrored(_uv_timer_start(handle, cb, timeout, repeat)); + } + + public Func _uv_timer_stop; + public void timer_stop(UvTimerHandle handle) + { + handle.Validate(); + ThrowIfErrored(_uv_timer_stop(handle)); + } + + public Func _uv_now; + public long now(UvLoopHandle loop) + { + loop.Validate(); + return _uv_now(loop); + } + + public delegate int uv_tcp_getsockname_func(UvTcpHandle handle, out SockAddr addr, ref int namelen); + public uv_tcp_getsockname_func _uv_tcp_getsockname; + public void tcp_getsockname(UvTcpHandle handle, out SockAddr addr, ref int namelen) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_getsockname(handle, out addr, ref namelen)); + } + + public delegate int uv_tcp_getpeername_func(UvTcpHandle handle, out SockAddr addr, ref int namelen); + public uv_tcp_getpeername_func _uv_tcp_getpeername; + public void tcp_getpeername(UvTcpHandle handle, out SockAddr addr, ref int namelen) + { + handle.Validate(); + ThrowIfErrored(_uv_tcp_getpeername(handle, out addr, ref namelen)); + } + + public uv_buf_t buf_init(IntPtr memory, int len) => new uv_buf_t(memory, len, Core.IsWindows); + + public struct uv_buf_t + { + // this type represents a WSABUF struct on Windows + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms741542(v=vs.85).aspx + // and an iovec struct on *nix + // http://man7.org/linux/man-pages/man2/readv.2.html + // because the order of the fields in these structs is different, the field + // names in this type don't have meaningful symbolic names. instead, they are + // assigned in the correct order by the constructor at runtime + + private readonly IntPtr _field0; + private readonly IntPtr _field1; + + public uv_buf_t(IntPtr memory, int len, bool windows) + { + if (windows) + { + _field0 = (IntPtr)len; + _field1 = memory; + } + else + { + _field0 = memory; + _field1 = (IntPtr)len; + } + } + } + + public enum HandleType + { + Unknown = 0, + ASYNC, + CHECK, + FS_EVENT, + FS_POLL, + HANDLE, + IDLE, + NAMED_PIPE, + POLL, + PREPARE, + PROCESS, + STREAM, + TCP, + TIMER, + TTY, + UDP, + SIGNAL, + } + + public enum RequestType + { + Unknown = 0, + REQ, + CONNECT, + WRITE, + SHUTDOWN, + UDP_SEND, + FS, + WORK, + GETADDRINFO, + GETNAMEINFO, + } + + private static class NativeMethods + { + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_loop_init(UvLoopHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_loop_close(IntPtr a0); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_run(UvLoopHandle handle, int mode); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern void uv_stop(UvLoopHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern void uv_ref(UvHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern void uv_unref(UvHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_fileno(UvHandle handle, ref IntPtr socket); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern void uv_close(IntPtr handle, uv_close_cb close_cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_async_init(UvLoopHandle loop, UvAsyncHandle handle, uv_async_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_async_send(UvAsyncHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl, EntryPoint = "uv_async_send")] + public static extern int uv_unsafe_async_send(IntPtr handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_init(UvLoopHandle loop, UvTcpHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_bind(UvTcpHandle handle, ref SockAddr addr, int flags); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_open(UvTcpHandle handle, IntPtr hSocket); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_nodelay(UvTcpHandle handle, int enable); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_pipe_init(UvLoopHandle loop, UvPipeHandle handle, int ipc); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_pipe_bind(UvPipeHandle loop, string name); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_pipe_open(UvPipeHandle handle, IntPtr hSocket); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_listen(UvStreamHandle handle, int backlog, uv_connection_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_accept(UvStreamHandle server, UvStreamHandle client); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public static extern void uv_pipe_connect(UvConnectRequest req, UvPipeHandle handle, string name, uv_connect_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_pipe_pending_count(UvPipeHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_read_start(UvStreamHandle handle, uv_alloc_cb alloc_cb, uv_read_cb read_cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_read_stop(UvStreamHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_try_write(UvStreamHandle handle, uv_buf_t[] bufs, int nbufs); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int uv_write(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, uv_write_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe int uv_write2(UvRequest req, UvStreamHandle handle, uv_buf_t* bufs, int nbufs, UvStreamHandle sendHandle, uv_write_cb cb); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr uv_err_name(int err); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr uv_strerror(int err); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_loop_size(); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_handle_size(HandleType handleType); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_req_size(RequestType reqType); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_ip4_addr(string ip, int port, out SockAddr addr); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_ip6_addr(string ip, int port, out SockAddr addr); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_getsockname(UvTcpHandle handle, out SockAddr name, ref int namelen); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_tcp_getpeername(UvTcpHandle handle, out SockAddr name, ref int namelen); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_walk(UvLoopHandle loop, uv_walk_cb walk_cb, IntPtr arg); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_timer_init(UvLoopHandle loop, UvTimerHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_timer_start(UvTimerHandle handle, uv_timer_cb cb, long timeout, long repeat); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_timer_stop(UvTimerHandle handle); + + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern long uv_now(UvLoopHandle loop); + + [DllImport("WS2_32.dll", CallingConvention = CallingConvention.Winapi)] + public static extern unsafe int WSAIoctl( + IntPtr socket, + int dwIoControlCode, + int* lpvInBuffer, + uint cbInBuffer, + int* lpvOutBuffer, + int cbOutBuffer, + out uint lpcbBytesReturned, + IntPtr lpOverlapped, + IntPtr lpCompletionRoutine + ); + + [DllImport("WS2_32.dll", CallingConvention = CallingConvention.Winapi)] + public static extern int WSAGetLastError(); + } + } +} diff --git a/Projects/Server/LibUv/Internal/SockAddr.cs b/Projects/Server/LibUv/Internal/SockAddr.cs new file mode 100644 index 000000000..09c6e6473 --- /dev/null +++ b/Projects/Server/LibUv/Internal/SockAddr.cs @@ -0,0 +1,107 @@ +// 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.Net; +using System.Runtime.InteropServices; +using Server; + +namespace Libuv.Internal +{ + [StructLayout(LayoutKind.Sequential)] + public struct SockAddr + { + // this type represents native memory occupied by sockaddr struct + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms740496(v=vs.85).aspx + // although the c/c++ header defines it as a 2-byte short followed by a 14-byte array, + // the simplest way to reserve the same size in c# is with four nameless long values + private long _field0; + private long _field1; + private long _field2; + private long _field3; + + public SockAddr(long ignored) => _field0 = _field1 = _field2 = _field3 = 0; + + public unsafe IPEndPoint GetIPEndPoint() + { + // The bytes are represented in network byte order. + // + // Example 1: [2001:4898:e0:391:b9ef:1124:9d3e:a354]:39179 + // + // 0000 0000 0b99 0017 => The third and fourth bytes 990B is the actual port + // 9103 e000 9848 0120 => IPv6 address is represented in the 128bit field1 and field2. + // 54a3 3e9d 2411 efb9 Read these two 64-bit long from right to left byte by byte. + // 0000 0000 0000 0010 => Scope ID 0x10 (eg [::1%16]) the first 4 bytes of field3 in host byte order. + // + // Example 2: 10.135.34.141:39178 when adopt dual-stack sockets, IPv4 is mapped to IPv6 + // + // 0000 0000 0a99 0017 => The port representation are the same + // 0000 0000 0000 0000 + // 8d22 870a ffff 0000 => IPv4 occupies the last 32 bit: 0A.87.22.8d is the actual address. + // 0000 0000 0000 0000 + // + // Example 3: 10.135.34.141:12804, not dual-stack sockets + // + // 8d22 870a fd31 0002 => sa_family == AF_INET (02) + // 0000 0000 0000 0000 + // 0000 0000 0000 0000 + // 0000 0000 0000 0000 + // + // Example 4: 127.0.0.1:52798, on a Mac OS + // + // 0100 007F 3ECE 0210 => sa_family == AF_INET (02) Note that struct sockaddr on mac use + // 0000 0000 0000 0000 the second unint8 field for sa family type + // 0000 0000 0000 0000 http://www.opensource.apple.com/source/xnu/xnu-1456.1.26/bsd/sys/socket.h + // 0000 0000 0000 0000 + // + // Reference: + // - Windows: https://msdn.microsoft.com/en-us/library/windows/desktop/ms740506(v=vs.85).aspx + // - Linux: https://github.com/torvalds/linux/blob/6a13feb9c82803e2b815eca72fa7a9f5561d7861/include/linux/socket.h + // - Linux (sin6_scope_id): https://github.com/torvalds/linux/blob/5924bbecd0267d87c24110cbe2041b5075173a25/net/sunrpc/addr.c#L82 + // - Apple: http://www.opensource.apple.com/source/xnu/xnu-1456.1.26/bsd/sys/socket.h + + // Quick calculate the port by mask the field and locate the byte 3 and byte 4 + // and then shift them to correct place to form a int. + var port = ((int)(_field0 & 0x00FF0000) >> 8) | (int)((_field0 & 0xFF000000) >> 24); + + int family = (int)_field0; + if (Core.IsDarwin) + // see explanation in example 4 + family >>= 8; + family &= 0xFF; + + if (family == 2) + // AF_INET => IPv4 + return new IPEndPoint(new IPAddress((_field0 >> 32) & 0xFFFFFFFF), port); + + if (IsIPv4MappedToIPv6()) + { + var ipv4bits = (_field2 >> 32) & 0x00000000FFFFFFFF; + return new IPEndPoint(new IPAddress(ipv4bits), port); + } + + // otherwise IPv6 + var bytes = new byte[16]; + fixed (byte* b = bytes) + { + *(long*)b = _field1; + *(long*)(b + 8) = _field2; + } + + return new IPEndPoint(new IPAddress(bytes, ScopeId), port); + } + + public uint ScopeId + { + get => (uint)_field3; + set + { + _field3 &= unchecked ((long)0xFFFFFFFF00000000); + _field3 |= value; + } + } + + // If the IPAddress is an IPv4 mapped to IPv6, return the IPv4 representation instead. + // For example [::FFFF:127.0.0.1] will be transform to IPAddress of 127.0.0.1 + private bool IsIPv4MappedToIPv6() => _field1 == 0 && (_field2 & 0xFFFFFFFF) == 0xFFFF0000; + } +} diff --git a/Projects/Server/LibUv/Internal/UvAsyncHandle.cs b/Projects/Server/LibUv/Internal/UvAsyncHandle.cs new file mode 100644 index 000000000..f27a179a8 --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvAsyncHandle.cs @@ -0,0 +1,72 @@ +// 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.Diagnostics; +using System.Threading; + +namespace Libuv.Internal +{ + public class UvAsyncHandle : UvHandle + { + private static readonly LibuvFunctions.uv_close_cb _destroyMemory = handle => DestroyMemory(handle); + + private static readonly LibuvFunctions.uv_async_cb _uv_async_cb = handle => AsyncCb(handle); + private Action _callback; + private Action, IntPtr> _queueCloseHandle; + + public UvAsyncHandle(ILibuvTrace logger) : base(logger) + { + } + + public void Init(UvLoopHandle loop, Action callback, Action, IntPtr> queueCloseHandle) + { + CreateMemory( + loop.Libuv, + loop.ThreadId, + loop.Libuv.handle_size(LibuvFunctions.HandleType.ASYNC)); + + _callback = callback; + _queueCloseHandle = queueCloseHandle; + _uv.async_init(loop, this, _uv_async_cb); + } + + public void Send() + { + _uv.async_send(this); + } + + private static void AsyncCb(IntPtr handle) + { + FromIntPtr(handle)._callback.Invoke(); + } + + protected override bool ReleaseHandle() + { + var memory = handle; + if (memory != IntPtr.Zero) + { + handle = IntPtr.Zero; + + if (Thread.CurrentThread.ManagedThreadId == ThreadId) + { + _uv.close(memory, _destroyMemory); + } + else if (_queueCloseHandle != null) + { + // This can be called from the finalizer. + // Ensure the closure doesn't reference "this". + LibuvFunctions uv = _uv; + _queueCloseHandle(memory2 => uv.close(memory2, _destroyMemory), memory); + uv.unsafe_async_send(memory); + } + else + { + Debug.Assert(false, "UvAsyncHandle not initialized with queueCloseHandle action"); + return false; + } + } + return true; + } + } +} diff --git a/Projects/Server/LibUv/Internal/UvConnectRequest.cs b/Projects/Server/LibUv/Internal/UvConnectRequest.cs new file mode 100644 index 000000000..cc60539ac --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvConnectRequest.cs @@ -0,0 +1,75 @@ +// 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 Microsoft.Extensions.Logging; + +namespace Libuv.Internal +{ + /// + /// Summary description for UvWriteRequest + /// + public class UvConnectRequest : UvRequest + { + private static readonly LibuvFunctions.uv_connect_cb _uv_connect_cb = UvConnectCb; + + private Action _callback; + private object _state; + + public UvConnectRequest(ILibuvTrace logger) : base (logger) + { + } + + public override void Init(LibuvThread thread) + { + DangerousInit(thread.Loop); + + base.Init(thread); + } + + public void DangerousInit(UvLoopHandle loop) + { + var requestSize = loop.Libuv.req_size(LibuvFunctions.RequestType.CONNECT); + CreateMemory( + loop.Libuv, + loop.ThreadId, + requestSize); + } + + public void Connect( + UvPipeHandle pipe, + string name, + Action callback, + object state) + { + _callback = callback; + _state = state; + + Libuv.pipe_connect(this, pipe, name, _uv_connect_cb); + } + + private static void UvConnectCb(IntPtr ptr, int status) + { + var req = FromIntPtr(ptr); + + var callback = req._callback; + req._callback = null; + + var state = req._state; + req._state = null; + + UvException error = null; + if (status < 0) req.Libuv.Check(status, out error); + + try + { + callback(req, status, error, state); + } + catch (Exception ex) + { + req._log.LogError(0, ex, "UvConnectRequest"); + throw; + } + } + } +} diff --git a/Projects/Server/LibUv/Internal/UvException.cs b/Projects/Server/LibUv/Internal/UvException.cs new file mode 100644 index 000000000..3d4e492ef --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvException.cs @@ -0,0 +1,14 @@ +// 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; + +namespace Libuv.Internal +{ + public class UvException : Exception + { + public UvException(string message, int statusCode) : base(message) => StatusCode = statusCode; + + public int StatusCode { get; } + } +} diff --git a/Projects/Server/LibUv/Internal/UvHandle.cs b/Projects/Server/LibUv/Internal/UvHandle.cs new file mode 100644 index 000000000..888791bcc --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvHandle.cs @@ -0,0 +1,66 @@ +// 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.Diagnostics; +using System.Threading; + +namespace Libuv.Internal +{ + public abstract class UvHandle : UvMemory + { + private static readonly LibuvFunctions.uv_close_cb _destroyMemory = DestroyMemory; + private Action, IntPtr> _queueCloseHandle; + + protected UvHandle(ILibuvTrace logger) : base (logger) + { + } + + protected void CreateHandle( + LibuvFunctions uv, + int threadId, + int size, + Action, IntPtr> queueCloseHandle) + { + _queueCloseHandle = queueCloseHandle; + CreateMemory(uv, threadId, size); + } + + protected override bool ReleaseHandle() + { + IntPtr memory = handle; + if (memory != IntPtr.Zero) + { + handle = IntPtr.Zero; + + if (Thread.CurrentThread.ManagedThreadId == ThreadId) + { + _uv.close(memory, _destroyMemory); + } + else if (_queueCloseHandle != null) + { + // This can be called from the finalizer. + // Ensure the closure doesn't reference "this". + LibuvFunctions uv = _uv; + _queueCloseHandle(memory2 => uv.close(memory2, _destroyMemory), memory); + } + else + { + Debug.Assert(false, "UvHandle not initialized with queueCloseHandle action"); + return false; + } + } + return true; + } + + public void Reference() + { + _uv.@ref(this); + } + + public void Unreference() + { + _uv.unref(this); + } + } +} diff --git a/Projects/Server/LibUv/Internal/UvLoopHandle.cs b/Projects/Server/LibUv/Internal/UvLoopHandle.cs new file mode 100644 index 000000000..c99c8896f --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvLoopHandle.cs @@ -0,0 +1,54 @@ +// 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.Threading; + +namespace Libuv.Internal +{ + public class UvLoopHandle : UvMemory + { + public UvLoopHandle(ILibuvTrace logger) : base(logger) + { + } + + public void Init(LibuvFunctions uv) + { + CreateMemory( + uv, + Thread.CurrentThread.ManagedThreadId, + uv.loop_size()); + + _uv.loop_init(this); + } + + public void Run(int mode = 0) + { + _uv.run(this, mode); + } + + public void Stop() + { + _uv.stop(this); + } + + public long Now() => _uv.now(this); + + protected override unsafe bool ReleaseHandle() + { + var memory = handle; + if (memory != IntPtr.Zero) + { + // loop_close clears the gcHandlePtr + var gcHandlePtr = *(IntPtr*)memory; + + _uv.loop_close(this); + handle = IntPtr.Zero; + + DestroyMemory(memory, gcHandlePtr); + } + + return true; + } + } +} diff --git a/Projects/Server/LibUv/Internal/UvMemory.cs b/Projects/Server/LibUv/Internal/UvMemory.cs new file mode 100644 index 000000000..b5369ab5c --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvMemory.cs @@ -0,0 +1,79 @@ +// 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. +#define TRACE + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Libuv.Internal +{ + /// + /// Summary description for UvMemory + /// + public abstract class UvMemory : SafeHandle + { + protected LibuvFunctions _uv; + protected int _threadId; + protected readonly ILibuvTrace _log; + private readonly GCHandleType _handleType; + + protected UvMemory(ILibuvTrace logger, GCHandleType handleType = GCHandleType.Weak) : base(IntPtr.Zero, true) + { + _log = logger; + _handleType = handleType; + } + + public LibuvFunctions Libuv => _uv; + + public override bool IsInvalid => handle == IntPtr.Zero; + + public int ThreadId + { + get => _threadId; + private set => _threadId = value; + } + + protected unsafe void CreateMemory(LibuvFunctions uv, int threadId, int size) + { + _uv = uv; + ThreadId = threadId; + + handle = Marshal.AllocCoTaskMem(size); + *(IntPtr*)handle = GCHandle.ToIntPtr(GCHandle.Alloc(this, _handleType)); + } + + protected static unsafe void DestroyMemory(IntPtr memory) + { + var gcHandlePtr = *(IntPtr*)memory; + DestroyMemory(memory, gcHandlePtr); + } + + protected static void DestroyMemory(IntPtr memory, IntPtr gcHandlePtr) + { + if (gcHandlePtr != IntPtr.Zero) + { + var gcHandle = GCHandle.FromIntPtr(gcHandlePtr); + gcHandle.Free(); + } + Marshal.FreeCoTaskMem(memory); + } + + public IntPtr InternalGetHandle() => handle; + + public void Validate(bool closed = false) + { + Debug.Assert(closed || !IsClosed, "Handle is closed"); + Debug.Assert(!IsInvalid, "Handle is invalid"); + + Debug.Assert(_threadId == Thread.CurrentThread.ManagedThreadId, "ThreadId is incorrect"); + } + + public static unsafe THandle FromIntPtr(IntPtr handle) + { + GCHandle gcHandle = GCHandle.FromIntPtr(*(IntPtr*)handle); + return (THandle)gcHandle.Target; + } + } +} diff --git a/Projects/Server/LibUv/Internal/UvPipeHandle.cs b/Projects/Server/LibUv/Internal/UvPipeHandle.cs new file mode 100644 index 000000000..188777209 --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvPipeHandle.cs @@ -0,0 +1,36 @@ +// 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; + +namespace Libuv.Internal +{ + public class UvPipeHandle : UvStreamHandle + { + public UvPipeHandle(ILibuvTrace logger) : base(logger) + { + } + + public void Init(UvLoopHandle loop, Action, IntPtr> queueCloseHandle, bool ipc = false) + { + CreateHandle( + loop.Libuv, + loop.ThreadId, + loop.Libuv.handle_size(LibuvFunctions.HandleType.NAMED_PIPE), queueCloseHandle); + + _uv.pipe_init(loop, this, ipc); + } + + public void Open(IntPtr fileDescriptor) + { + _uv.pipe_open(this, fileDescriptor); + } + + public void Bind(string name) + { + _uv.pipe_bind(this, name); + } + + public int PendingCount() => _uv.pipe_pending_count(this); + } +} diff --git a/Projects/Server/LibUv/Internal/UvRequest.cs b/Projects/Server/LibUv/Internal/UvRequest.cs new file mode 100644 index 000000000..ed7199ce2 --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvRequest.cs @@ -0,0 +1,31 @@ +// 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.Runtime.InteropServices; + +namespace Libuv.Internal +{ + public class UvRequest : UvMemory + { + protected UvRequest(ILibuvTrace logger) : base(logger, GCHandleType.Normal) + { + } + + public virtual void Init(LibuvThread thread) + { +#if DEBUG + // Store weak handles to all UvRequest objects so we can do leak detection + // while running tests + thread.Requests.Add(new WeakReference(this)); +#endif + } + + protected override bool ReleaseHandle() + { + DestroyMemory(handle); + handle = IntPtr.Zero; + return true; + } + } +} diff --git a/Projects/Server/LibUv/Internal/UvStreamHandle.cs b/Projects/Server/LibUv/Internal/UvStreamHandle.cs new file mode 100644 index 000000000..2edf8253b --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvStreamHandle.cs @@ -0,0 +1,151 @@ +// 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.Runtime.InteropServices; +using Microsoft.Extensions.Logging; + +namespace Libuv.Internal +{ + public abstract class UvStreamHandle : UvHandle + { + private static readonly LibuvFunctions.uv_connection_cb _uv_connection_cb = UvConnectionCb; + // Ref and out lamda params must be explicitly typed + private static readonly LibuvFunctions.uv_alloc_cb _uv_alloc_cb = + (IntPtr handle, int suggested_size, out LibuvFunctions.uv_buf_t buf) => UvAllocCb(handle, suggested_size, out buf); + private static readonly LibuvFunctions.uv_read_cb _uv_read_cb = + (IntPtr handle, int status, ref LibuvFunctions.uv_buf_t buf) => UvReadCb(handle, status, ref buf); + + private Action _listenCallback; + private object _listenState; + private GCHandle _listenVitality; + + private Func _allocCallback; + private Action _readCallback; + private object _readState; + private GCHandle _readVitality; + + protected UvStreamHandle(ILibuvTrace logger) : base(logger) + { + } + + protected override bool ReleaseHandle() + { + if (_listenVitality.IsAllocated) _listenVitality.Free(); + if (_readVitality.IsAllocated) _readVitality.Free(); + return base.ReleaseHandle(); + } + + public void Listen(int backlog, Action callback, object state) + { + if (_listenVitality.IsAllocated) throw new InvalidOperationException("TODO: Listen may not be called more than once"); + try + { + _listenCallback = callback; + _listenState = state; + _listenVitality = GCHandle.Alloc(this, GCHandleType.Normal); + _uv.listen(this, backlog, _uv_connection_cb); + } + catch + { + _listenCallback = null; + _listenState = null; + if (_listenVitality.IsAllocated) _listenVitality.Free(); + throw; + } + } + + public void Accept(UvStreamHandle handle) + { + _uv.accept(this, handle); + } + + public void ReadStart( + Func allocCallback, + Action readCallback, + object state) + { + if (_readVitality.IsAllocated) throw new InvalidOperationException("TODO: ReadStop must be called before ReadStart may be called again"); + + try + { + _allocCallback = allocCallback; + _readCallback = readCallback; + _readState = state; + _readVitality = GCHandle.Alloc(this, GCHandleType.Normal); + _uv.read_start(this, _uv_alloc_cb, _uv_read_cb); + } + catch + { + _allocCallback = null; + _readCallback = null; + _readState = null; + if (_readVitality.IsAllocated) _readVitality.Free(); + throw; + } + } + + // UvStreamHandle.ReadStop() should be idempotent to match uv_read_stop() + public void ReadStop() + { + if (_readVitality.IsAllocated) _readVitality.Free(); + _allocCallback = null; + _readCallback = null; + _readState = null; + _uv.read_stop(this); + } + + public int TryWrite(LibuvFunctions.uv_buf_t buf) + { + return _uv.try_write(this, new[] { buf }, 1); + } + + private static void UvConnectionCb(IntPtr handle, int status) + { + var stream = FromIntPtr(handle); + + stream.Libuv.Check(status, out var error); + + try + { + stream._listenCallback(stream, status, error, stream._listenState); + } + catch (Exception ex) + { + stream._log.LogError(0, ex, "UvConnectionCb"); + throw; + } + } + + private static void UvAllocCb(IntPtr handle, int suggested_size, out LibuvFunctions.uv_buf_t buf) + { + var stream = FromIntPtr(handle); + try + { + buf = stream._allocCallback(stream, suggested_size, stream._readState); + } + catch (Exception ex) + { + stream._log.LogError(0, ex, "UvAllocCb"); + buf = stream.Libuv.buf_init(IntPtr.Zero, 0); + throw; + } + } + + private static void UvReadCb(IntPtr handle, int status, ref LibuvFunctions.uv_buf_t buf) + { + var stream = FromIntPtr(handle); + + try + { + stream._readCallback(stream, status, stream._readState); + } + catch (Exception ex) + { + stream._log.LogError(0, ex, "UbReadCb"); + throw; + } + } + + } +} diff --git a/Projects/Server/LibUv/Internal/UvTcpHandle.cs b/Projects/Server/LibUv/Internal/UvTcpHandle.cs new file mode 100644 index 000000000..745477201 --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvTcpHandle.cs @@ -0,0 +1,72 @@ +// 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.Net; +using System.Runtime.InteropServices; + +namespace Libuv.Internal +{ + public class UvTcpHandle : UvStreamHandle + { + public UvTcpHandle(ILibuvTrace logger) : base(logger) + { + } + + public void Init(UvLoopHandle loop, Action, IntPtr> queueCloseHandle) + { + CreateHandle( + loop.Libuv, + loop.ThreadId, + loop.Libuv.handle_size(LibuvFunctions.HandleType.TCP), queueCloseHandle); + + _uv.tcp_init(loop, this); + } + + public void Open(IntPtr fileDescriptor) + { + _uv.tcp_open(this, fileDescriptor); + } + + public void Bind(IPEndPoint endPoint) + { + var addressText = endPoint.Address.ToString(); + + _uv.ip4_addr(addressText, endPoint.Port, out var addr, out var error1); + + if (error1 != null) + { + _uv.ip6_addr(addressText, endPoint.Port, out addr, out var error2); + if (error2 != null) throw error1; + + if (endPoint.Address.ScopeId != addr.ScopeId) + // IPAddress.ScopeId cannot be less than 0 or greater than 0xFFFFFFFF + // https://msdn.microsoft.com/en-us/library/system.net.ipaddress.scopeid(v=vs.110).aspx + addr.ScopeId = (uint)endPoint.Address.ScopeId; + } + + _uv.tcp_bind(this, ref addr, 0); + } + + public IPEndPoint GetPeerIPEndPoint() + { + int namelen = Marshal.SizeOf(); + _uv.tcp_getpeername(this, out var socketAddress, ref namelen); + + return socketAddress.GetIPEndPoint(); + } + + public IPEndPoint GetSockIPEndPoint() + { + int namelen = Marshal.SizeOf(); + _uv.tcp_getsockname(this, out var socketAddress, ref namelen); + + return socketAddress.GetIPEndPoint(); + } + + public void NoDelay(bool enable) + { + _uv.tcp_nodelay(this, enable); + } + } +} diff --git a/Projects/Server/LibUv/Internal/UvTimerHandle.cs b/Projects/Server/LibUv/Internal/UvTimerHandle.cs new file mode 100644 index 000000000..5fc52af17 --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvTimerHandle.cs @@ -0,0 +1,56 @@ +// 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 Microsoft.Extensions.Logging; + +namespace Libuv.Internal +{ + public class UvTimerHandle : UvHandle + { + private static readonly LibuvFunctions.uv_timer_cb _uv_timer_cb = UvTimerCb; + + private Action _callback; + + public UvTimerHandle(ILibuvTrace logger) : base(logger) + { + } + + public void Init(UvLoopHandle loop, Action, IntPtr> queueCloseHandle) + { + CreateHandle( + loop.Libuv, + loop.ThreadId, + loop.Libuv.handle_size(LibuvFunctions.HandleType.TIMER), + queueCloseHandle); + + _uv.timer_init(loop, this); + } + + public void Start(Action callback, long timeout, long repeat) + { + _callback = callback; + _uv.timer_start(this, _uv_timer_cb, timeout, repeat); + } + + public void Stop() + { + _uv.timer_stop(this); + } + + private static void UvTimerCb(IntPtr handle) + { + var timer = FromIntPtr(handle); + + try + { + timer._callback(timer); + } + catch (Exception ex) + { + timer._log.LogError(0, ex, nameof(UvTimerCb)); + throw; + } + } + } +} diff --git a/Projects/Server/LibUv/Internal/UvWriteReq.cs b/Projects/Server/LibUv/Internal/UvWriteReq.cs new file mode 100644 index 000000000..1b30c4ccb --- /dev/null +++ b/Projects/Server/LibUv/Internal/UvWriteReq.cs @@ -0,0 +1,237 @@ +// 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.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.Extensions.Logging; + +namespace Libuv.Internal +{ + /// + /// Summary description for UvWriteRequest + /// + public class UvWriteReq : UvRequest + { + private static readonly LibuvFunctions.uv_write_cb _uv_write_cb = (ptr, status) => UvWriteCb(ptr, status); + + private IntPtr _bufs; + + private Action _callback; + private object _state; + private const int BUFFER_COUNT = 4; + + private LibuvAwaitable _awaitable = new LibuvAwaitable(); + private List _pins = new List(BUFFER_COUNT + 1); + private List _handles = new List(BUFFER_COUNT + 1); + + public UvWriteReq(ILibuvTrace logger) : base(logger) + { + } + + public override void Init(LibuvThread thread) + { + DangerousInit(thread.Loop); + + base.Init(thread); + } + + public void DangerousInit(UvLoopHandle loop) + { + var requestSize = loop.Libuv.req_size(LibuvFunctions.RequestType.WRITE); + var bufferSize = Marshal.SizeOf() * BUFFER_COUNT; + CreateMemory( + loop.Libuv, + loop.ThreadId, + requestSize + bufferSize); + _bufs = handle + requestSize; + } + + public LibuvAwaitable WriteAsync(UvStreamHandle handle, in ReadOnlySequence buffer) + { + Write(handle, buffer, LibuvAwaitable.Callback, _awaitable); + return _awaitable; + } + + public LibuvAwaitable WriteAsync(UvStreamHandle handle, ArraySegment> bufs) + { + Write(handle, bufs, LibuvAwaitable.Callback, _awaitable); + return _awaitable; + } + + private unsafe void Write( + UvStreamHandle handle, + in ReadOnlySequence buffer, + Action callback, + object state) + { + try + { + var nBuffers = 0; + if (buffer.IsSingleSegment) + nBuffers = 1; + else + foreach (var _ in buffer) + nBuffers++; + + var pBuffers = (LibuvFunctions.uv_buf_t*)_bufs; + if (nBuffers > BUFFER_COUNT) + { + // create and pin buffer array when it's larger than the pre-allocated one + var bufArray = new LibuvFunctions.uv_buf_t[nBuffers]; + var gcHandle = GCHandle.Alloc(bufArray, GCHandleType.Pinned); + _pins.Add(gcHandle); + pBuffers = (LibuvFunctions.uv_buf_t*)gcHandle.AddrOfPinnedObject(); + } + + if (nBuffers == 1) + { + var memory = buffer.First; + var memoryHandle = memory.Pin(); + _handles.Add(memoryHandle); + + // Fast path for single buffer + pBuffers[0] = Libuv.buf_init( + (IntPtr)memoryHandle.Pointer, + memory.Length); + } + else + { + var index = 0; + foreach (var memory in buffer) + { + // This won't actually pin the buffer since we're already using pinned memory + var memoryHandle = memory.Pin(); + _handles.Add(memoryHandle); + + // create and pin each segment being written + pBuffers[index] = Libuv.buf_init( + (IntPtr)memoryHandle.Pointer, + memory.Length); + index++; + } + } + + _callback = callback; + _state = state; + _uv.write(this, handle, pBuffers, nBuffers, _uv_write_cb); + } + catch + { + _callback = null; + _state = null; + UnpinGcHandles(); + throw; + } + } + + private void Write( + UvStreamHandle handle, + ArraySegment> bufs, + Action callback, + object state) + { + WriteArraySegmentInternal(handle, bufs, null, callback, state); + } + + public void Write2( + UvStreamHandle handle, + ArraySegment> bufs, + UvStreamHandle sendHandle, + Action callback, + object state) + { + WriteArraySegmentInternal(handle, bufs, sendHandle, callback, state); + } + + private unsafe void WriteArraySegmentInternal( + UvStreamHandle handle, + ArraySegment> bufs, + UvStreamHandle sendHandle, + Action callback, + object state) + { + try + { + var pBuffers = (LibuvFunctions.uv_buf_t*)_bufs; + var nBuffers = bufs.Count; + if (nBuffers > BUFFER_COUNT) + { + // create and pin buffer array when it's larger than the pre-allocated one + var bufArray = new LibuvFunctions.uv_buf_t[nBuffers]; + var gcHandle = GCHandle.Alloc(bufArray, GCHandleType.Pinned); + _pins.Add(gcHandle); + pBuffers = (LibuvFunctions.uv_buf_t*)gcHandle.AddrOfPinnedObject(); + } + + for (var index = 0; index < nBuffers; index++) + { + // create and pin each segment being written + var buf = bufs.Array[bufs.Offset + index]; + + var gcHandle = GCHandle.Alloc(buf.Array, GCHandleType.Pinned); + _pins.Add(gcHandle); + pBuffers[index] = Libuv.buf_init( + gcHandle.AddrOfPinnedObject() + buf.Offset, + buf.Count); + } + + _callback = callback; + _state = state; + + if (sendHandle == null) + _uv.write(this, handle, pBuffers, nBuffers, _uv_write_cb); + else + _uv.write2(this, handle, pBuffers, nBuffers, sendHandle, _uv_write_cb); + } + catch + { + _callback = null; + _state = null; + UnpinGcHandles(); + throw; + } + } + + // Safe handle has instance method called Unpin + // so using UnpinGcHandles to avoid conflict + private void UnpinGcHandles() + { + var pinList = _pins; + var count = pinList.Count; + for (var i = 0; i < count; i++) pinList[i].Free(); + pinList.Clear(); + + var handleList = _handles; + count = handleList.Count; + for (var i = 0; i < count; i++) handleList[i].Dispose(); + handleList.Clear(); + } + + private static void UvWriteCb(IntPtr ptr, int status) + { + var req = FromIntPtr(ptr); + req.UnpinGcHandles(); + + var callback = req._callback; + req._callback = null; + + var state = req._state; + req._state = null; + + UvException error = null; + if (status < 0) req.Libuv.Check(status, out error); + + try + { + callback(req, status, error, state); + } + catch (Exception ex) + { + req._log.LogError(0, ex, "UvWriteCb"); + throw; + } + } + } +} diff --git a/Projects/Server/LibUv/LibuvAwaitable.cs b/Projects/Server/LibUv/LibuvAwaitable.cs new file mode 100644 index 000000000..efdaeae29 --- /dev/null +++ b/Projects/Server/LibUv/LibuvAwaitable.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using Libuv.Internal; + +namespace Libuv +{ + public class LibuvAwaitable : ICriticalNotifyCompletion where TRequest : UvRequest + { + private static readonly Action _callbackCompleted = () => { }; + + private Action _callback; + + private UvException _exception; + + private int _status; + + public static readonly Action Callback = (req, status, error, state) => + { + var awaitable = (LibuvAwaitable)state; + + awaitable._exception = error; + awaitable._status = status; + + var continuation = Interlocked.Exchange(ref awaitable._callback, _callbackCompleted); + + continuation?.Invoke(); + }; + + public LibuvAwaitable GetAwaiter() => this; + public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted); + + public UvWriteResult GetResult() + { + Debug.Assert(_callback == _callbackCompleted); + + var exception = _exception; + var status = _status; + + // Reset the awaitable state + _exception = null; + _status = 0; + _callback = null; + + return new UvWriteResult(status, exception); + } + + public void OnCompleted(Action continuation) + { + // There should never be a race between IsCompleted and OnCompleted since both operations + // should always be on the libuv thread + if (ReferenceEquals(_callback, _callbackCompleted)) + Debug.Fail($"{typeof(LibuvAwaitable)}.{nameof(OnCompleted)} raced with {nameof(IsCompleted)}, scheduling callback."); + + _callback = continuation; + } + + public void UnsafeOnCompleted(Action continuation) + { + OnCompleted(continuation); + } + } + + public struct UvWriteResult + { + public int Status { get; } + public UvException Error { get; } + + public UvWriteResult(int status, UvException error) + { + Status = status; + Error = error; + } + } +} diff --git a/Projects/Server/LibUv/LibuvConnection.cs b/Projects/Server/LibUv/LibuvConnection.cs new file mode 100644 index 000000000..5839c4d6b --- /dev/null +++ b/Projects/Server/LibUv/LibuvConnection.cs @@ -0,0 +1,309 @@ +// 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)}."); + } + } + } +} diff --git a/Projects/Server/LibUv/LibuvConnectionListener.cs b/Projects/Server/LibUv/LibuvConnectionListener.cs new file mode 100644 index 000000000..d04da54c6 --- /dev/null +++ b/Projects/Server/LibUv/LibuvConnectionListener.cs @@ -0,0 +1,196 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Libuv.Internal; +using Microsoft.AspNetCore.Connections; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Server; + +namespace Libuv +{ + public class LibuvConnectionListener : IConnectionListener + { + private readonly List _listeners = new List(); + private IAsyncEnumerator _acceptEnumerator; + private bool _stopped; + private bool _disposed; + + public LibuvConnectionListener(LibuvTransportContext context, EndPoint endPoint) + : this(new LibuvFunctions(), context, endPoint) + { } + + // For testing + public LibuvConnectionListener(LibuvFunctions uv, LibuvTransportContext context, EndPoint endPoint) + { + Libuv = uv; + TransportContext = context; + + EndPoint = endPoint; + } + + public LibuvFunctions Libuv { get; } + public LibuvTransportContext TransportContext { get; } + public List Threads { get; } = new List(); + + public IHostApplicationLifetime AppLifetime => TransportContext.AppLifetime; + public ILibuvTrace Log => TransportContext.Log; + public LibuvTransportOptions TransportOptions => TransportContext.Options; + + public EndPoint EndPoint { get; set; } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + if (_disposed) throw new ObjectDisposedException(GetType().FullName); + + if (await _acceptEnumerator.MoveNextAsync()) return _acceptEnumerator.Current; + + // null means we're done... + return null; + } + + public async ValueTask UnbindAsync(CancellationToken cancellationToken = default) + { + if (_stopped) return; + + _stopped = true; + + var disposeTasks = _listeners.Select(listener => ((IAsyncDisposable)listener).DisposeAsync()).ToArray(); + + if (!await WaitAsync(Task.WhenAll(disposeTasks), TimeSpan.FromSeconds(5)).ConfigureAwait(false)) + Log.LogError(0, null, "Disposing listeners failed"); + } + + + public async ValueTask DisposeAsync() + { + if (_disposed) return; + + _disposed = true; + + await UnbindAsync().ConfigureAwait(false); + + foreach (var listener in _listeners) await listener.AbortQueuedConnectionAsync().ConfigureAwait(false); + + _listeners.Clear(); + + await StopThreadsAsync().ConfigureAwait(false); + } + + internal async Task StopThreadsAsync() + { + try + { + await Task.WhenAll(Threads.Select(thread => thread.StopAsync(TimeSpan.FromSeconds(5))).ToArray()) + .ConfigureAwait(false); + } + catch (AggregateException aggEx) + { + // An uncaught exception was likely thrown from the libuv event loop. + // The original error that crashed one loop may have caused secondary errors in others. + // Make sure that the stack trace of the original error is logged. + foreach (var ex in aggEx.InnerExceptions) Log.LogCritical("Failed to gracefully close Kestrel.", ex); + + throw; + } + + Threads.Clear(); +#if DEBUG && !INNER_LOOP + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); +#endif + } + + public async Task BindAsync() + { + // TODO: Move thread management to LibuvTransportFactory + // TODO: Split endpoint management from thread management + for (var index = 0; index < TransportOptions.ThreadCount; index++) Threads.Add(new LibuvThread(Libuv, TransportContext)); + + foreach (var thread in Threads) await thread.StartAsync().ConfigureAwait(false); + + try + { + if (TransportOptions.ThreadCount == 1) + { + var listener = new Listener(TransportContext); + _listeners.Add(listener); + await listener.StartAsync(EndPoint, Threads[0]).ConfigureAwait(false); + EndPoint = listener.EndPoint; + } + else + { + var pipeName = (Core.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); + var pipeMessage = Guid.NewGuid().ToByteArray(); + + var listenerPrimary = new ListenerPrimary(TransportContext); + _listeners.Add(listenerPrimary); + await listenerPrimary.StartAsync(pipeName, pipeMessage, EndPoint, Threads[0]).ConfigureAwait(false); + EndPoint = listenerPrimary.EndPoint; + + foreach (var thread in Threads.Skip(1)) + { + var listenerSecondary = new ListenerSecondary(TransportContext); + _listeners.Add(listenerSecondary); + await listenerSecondary.StartAsync(pipeName, pipeMessage, EndPoint, thread).ConfigureAwait(false); + } + } + _acceptEnumerator = AcceptConnections(); + } + catch (UvException ex) when (ex.StatusCode == LibuvConstants.EADDRINUSE) + { + await UnbindAsync().ConfigureAwait(false); + throw new AddressInUseException(ex.Message, ex); + } + catch + { + await UnbindAsync().ConfigureAwait(false); + throw; + } + } + + private async IAsyncEnumerator AcceptConnections() + { + var slots = new Task<(LibuvConnection, int)>[_listeners.Count]; + // This is the task we'll put in the slot when each listening completes. It'll prevent + // us from having to shrink the array. We'll just loop while there are active slots. + var incompleteTask = new TaskCompletionSource<(LibuvConnection, int)>().Task; + + var remainingSlots = slots.Length; + + // Issue parallel accepts on all listeners + for (int i = 0; i < remainingSlots; i++) slots[i] = AcceptAsync(_listeners[i], i); + + while (remainingSlots > 0) + { + // Calling GetAwaiter().GetResult() is safe because we know the task is completed + var (connection, slot) = (await Task.WhenAny(slots)).GetAwaiter().GetResult(); + + // If the connection is null then the listener was closed + if (connection == null) + { + remainingSlots--; + slots[slot] = incompleteTask; + } + else + { + // Fill that slot with another accept and yield the connection + slots[slot] = AcceptAsync(_listeners[slot], slot); + + yield return connection; + } + } + + static async Task<(LibuvConnection, int)> AcceptAsync(ListenerContext listener, int slot) => (await listener.AcceptAsync(), slot); + } + + private static async Task WaitAsync(Task task, TimeSpan timeout) => await Task.WhenAny(task, Task.Delay(timeout)).ConfigureAwait(false) == task; + } +} diff --git a/Projects/Server/LibUv/LibuvConstants.cs b/Projects/Server/LibUv/LibuvConstants.cs new file mode 100644 index 000000000..bce142a81 --- /dev/null +++ b/Projects/Server/LibUv/LibuvConstants.cs @@ -0,0 +1,98 @@ +// 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.Runtime.CompilerServices; +using Server; + +namespace Libuv +{ + public static class LibuvConstants + { + public const int EOF = -4095; + public static readonly int? ECONNRESET = GetECONNRESET(); + public static readonly int? EADDRINUSE = GetEADDRINUSE(); + public static readonly int? ENOTSUP = GetENOTSUP(); + public static readonly int? EPIPE = GetEPIPE(); + public static readonly int? ECANCELED = GetECANCELED(); + public static readonly int? ENOTCONN = GetENOTCONN(); + public static readonly int? EINVAL = GetEINVAL(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsConnectionReset(int errno) => errno == ECONNRESET || errno == EPIPE || errno == ENOTCONN || errno == EINVAL; + + private static int? GetECONNRESET() + { + if (Core.IsWindows) + return -4077; + if (Core.IsLinux) + return -104; + if (Core.IsDarwin) + return -5; + return null; + } + + private static int? GetEPIPE() + { + if (Core.IsWindows) + return -4047; + if (Core.IsLinux) + return -32; + if (Core.IsDarwin) + return -32; + return null; + } + + private static int? GetENOTCONN() + { + if (Core.IsWindows) + return -4053; + if (Core.IsLinux) + return -107; + if (Core.IsDarwin) + return -57; + return null; + } + + private static int? GetEINVAL() + { + if (Core.IsWindows) + return -4071; + if (Core.IsLinux) + return -22; + if (Core.IsDarwin) + return -22; + return null; + } + + private static int? GetEADDRINUSE() + { + if (Core.IsWindows) + return -4091; + if (Core.IsLinux) + return -98; + if (Core.IsDarwin) + return -48; + return null; + } + + private static int? GetENOTSUP() + { + if (Core.IsLinux) + return -95; + if (Core.IsDarwin) + return -45; + return null; + } + + private static int? GetECANCELED() + { + if (Core.IsWindows) + return -4081; + if (Core.IsLinux) + return -125; + if (Core.IsDarwin) + return -89; + return null; + } + } +} diff --git a/Projects/Server/LibUv/LibuvOutputConsumer.cs b/Projects/Server/LibUv/LibuvOutputConsumer.cs new file mode 100644 index 000000000..e9d42ca52 --- /dev/null +++ b/Projects/Server/LibUv/LibuvOutputConsumer.cs @@ -0,0 +1,111 @@ +// 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.IO.Pipelines; +using System.Threading.Tasks; +using Libuv.Internal; + +namespace Libuv +{ + public class LibuvOutputConsumer + { + private readonly LibuvThread _thread; + private readonly UvStreamHandle _socket; + private readonly string _connectionId; + private readonly ILibuvTrace _log; + private readonly PipeReader _pipe; + + public LibuvOutputConsumer( + PipeReader pipe, + LibuvThread thread, + UvStreamHandle socket, + string connectionId, + ILibuvTrace log) + { + _pipe = pipe; + _thread = thread; + _socket = socket; + _connectionId = connectionId; + _log = log; + } + + public async Task WriteOutputAsync() + { + var pool = _thread.WriteReqPool; + + while (true) + { + var result = await _pipe.ReadAsync(); + + var buffer = result.Buffer; + var consumed = buffer.End; + + try + { + if (result.IsCanceled) break; + + if (!buffer.IsEmpty) + { + var writeReq = pool.Allocate(); + + try + { + if (_socket.IsClosed) break; + + var writeResult = await writeReq.WriteAsync(_socket, buffer); + + LogWriteInfo(writeResult.Status, writeResult.Error); + + if (writeResult.Error != null) + { + consumed = buffer.Start; + throw writeResult.Error; + } + } + finally + { + // Make sure we return the writeReq to the pool + pool.Return(writeReq); + + // Null out writeReq so it doesn't get caught by CheckUvReqLeaks. + // It is rooted by a TestSink scope through Pipe continuations in + // ResponseTests.HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate + writeReq = null; + } + } + + if (result.IsCompleted) break; + } + finally + { + _pipe.AdvanceTo(consumed); + } + } + } + + private void LogWriteInfo(int status, Exception error) + { + if (error == null) + { + _log.ConnectionWriteCallback(_connectionId, status); + } + else + { + // Log connection resets at a lower (Debug) level. + if (status == LibuvConstants.ECANCELED) + { + // Connection was aborted. + } + else if (LibuvConstants.IsConnectionReset(status)) + { + _log.ConnectionReset(_connectionId); + } + else + { + _log.ConnectionError(_connectionId, error); + } + } + } + } +} diff --git a/Projects/Server/LibUv/LibuvThread.cs b/Projects/Server/LibUv/LibuvThread.cs new file mode 100644 index 000000000..d57d7f2dc --- /dev/null +++ b/Projects/Server/LibUv/LibuvThread.cs @@ -0,0 +1,437 @@ +// 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.Collections.Generic; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Runtime.ExceptionServices; +using System.Threading; +using System.Threading.Tasks; +using Libuv.Internal; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Libuv +{ + public class LibuvThread : PipeScheduler + { + // maximum times the work queues swapped and are processed in a single pass + // as completing a task may immediately have write data to put on the network + // otherwise it needs to wait till the next pass of the libuv loop + private readonly int _maxLoops; + + private readonly LibuvFunctions _libuv; + private readonly IHostApplicationLifetime _appLifetime; + private readonly Thread _thread; + private readonly TaskCompletionSource _threadTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly UvLoopHandle _loop; + private readonly UvAsyncHandle _post; + private Queue _workAdding = new Queue(1024); + private Queue _workRunning = new Queue(1024); + private Queue _closeHandleAdding = new Queue(256); + private Queue _closeHandleRunning = new Queue(256); + private readonly object _workSync = new object(); + private readonly object _closeHandleSync = new object(); + private readonly object _startSync = new object(); + private bool _stopImmediate; + private bool _initCompleted; + private Exception _closeError; + private readonly ILibuvTrace _log; + + public LibuvThread(LibuvFunctions libuv, LibuvTransportContext libuvTransportContext, int maxLoops = 8) + : this(libuv, libuvTransportContext.AppLifetime, libuvTransportContext.Options.MemoryPoolFactory(), libuvTransportContext.Log, maxLoops) + { + } + + public LibuvThread(LibuvFunctions libuv, IHostApplicationLifetime appLifetime, MemoryPool pool, ILibuvTrace log, int maxLoops = 8) + { + _libuv = libuv; + _appLifetime = appLifetime; + _log = log; + _loop = new UvLoopHandle(_log); + _post = new UvAsyncHandle(_log); + _maxLoops = maxLoops; + + _thread = new Thread(ThreadStart); +#if !INNER_LOOP + _thread.Name = nameof(LibuvThread); +#endif + +#if !DEBUG + // Mark the thread as being as unimportant to keeping the process alive. + // Don't do this for debug builds, so we know if the thread isn't terminating. + _thread.IsBackground = true; +#endif + QueueCloseHandle = PostCloseHandle; + QueueCloseAsyncHandle = EnqueueCloseHandle; + MemoryPool = pool; + WriteReqPool = new WriteReqPool(this, _log); + } + + public UvLoopHandle Loop => _loop; + + public MemoryPool MemoryPool { get; } + + public WriteReqPool WriteReqPool { get; } + +#if DEBUG + public List Requests { get; } = new List(); +#endif + + public Exception FatalError => _closeError; + + public Action, IntPtr> QueueCloseHandle { get; } + + private Action, IntPtr> QueueCloseAsyncHandle { get; } + + public Task StartAsync() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _thread.Start(tcs); + return tcs.Task; + } + + public async Task StopAsync(TimeSpan timeout) + { + lock (_startSync) + { + if (!_initCompleted) return; + } + + Debug.Assert(!_threadTcs.Task.IsCompleted, "The loop thread was completed before calling uv_unref on the post handle."); + + var stepTimeout = TimeSpan.FromTicks(timeout.Ticks / 3); + + try + { + Post(t => t.AllowStop()); + if (!await WaitAsync(_threadTcs.Task, stepTimeout).ConfigureAwait(false)) + { + _log.LogWarning($"{nameof(LibuvThread)}.{nameof(StopAsync)} failed to terminate libuv thread, {nameof(AllowStop)}"); + + Post(t => t.OnStopRude()); + if (!await WaitAsync(_threadTcs.Task, stepTimeout).ConfigureAwait(false)) + { + _log.LogCritical($"{nameof(LibuvThread)}.{nameof(StopAsync)} failed to terminate libuv thread, {nameof(OnStopRude)}."); + + Post(t => t.OnStopImmediate()); + if (!await WaitAsync(_threadTcs.Task, stepTimeout).ConfigureAwait(false)) _log.LogCritical($"{nameof(LibuvThread)}.{nameof(StopAsync)} failed to terminate libuv thread, {nameof(OnStopImmediate)}."); + } + } + } + catch (ObjectDisposedException) + { + if (!await WaitAsync(_threadTcs.Task, stepTimeout).ConfigureAwait(false)) _log.LogCritical($"{nameof(LibuvThread)}.{nameof(StopAsync)} failed to terminate libuv thread."); + } + + if (_closeError != null) ExceptionDispatchInfo.Capture(_closeError).Throw(); + } + +#if DEBUG && !INNER_LOOP + private void CheckUvReqLeaks() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + // Detect leaks in UvRequest objects + foreach (var request in Requests) Debug.Assert(request.Target == null, $"{request.Target?.GetType()} object is still alive."); + } +#endif + + private void AllowStop() + { + _post.Unreference(); + } + + private void OnStopRude() + { + Walk(ptr => + { + var handle = UvMemory.FromIntPtr(ptr); + if (handle != _post) + // handle can be null because UvMemory.FromIntPtr looks up a weak reference + handle?.Dispose(); + }); + } + + private void OnStopImmediate() + { + _stopImmediate = true; + _loop.Stop(); + } + + public void Post(Action callback, T state) + { + // Handle is closed to don't bother scheduling anything + if (_post.IsClosed) return; + + var work = new Work + { + CallbackAdapter = CallbackAdapter.PostCallbackAdapter, + Callback = callback, + // TODO: This boxes + State = state + }; + + lock (_workSync) + { + _workAdding.Enqueue(work); + } + + try + { + _post.Send(); + } + catch (ObjectDisposedException) + { + // There's an inherent race here where we're in the middle of shutdown + } + } + + private void Post(Action callback) + { + Post(callback, this); + } + + public Task PostAsync(Action callback, T state) + { + // Handle is closed to don't bother scheduling anything + if (_post.IsClosed) return Task.CompletedTask; + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var work = new Work + { + CallbackAdapter = CallbackAdapter.PostAsyncCallbackAdapter, + Callback = callback, + State = state, + Completion = tcs + }; + + lock (_workSync) + { + _workAdding.Enqueue(work); + } + + try + { + _post.Send(); + } + catch (ObjectDisposedException) + { + // There's an inherent race here where we're in the middle of shutdown + } + return tcs.Task; + } + + public void Walk(Action callback) + { + Walk((ptr, arg) => callback(ptr), IntPtr.Zero); + } + + private void Walk(LibuvFunctions.uv_walk_cb callback, IntPtr arg) + { + _libuv.walk( + _loop, + callback, + arg + ); + } + + private void PostCloseHandle(Action callback, IntPtr handle) + { + EnqueueCloseHandle(callback, handle); + _post.Send(); + } + + private void EnqueueCloseHandle(Action callback, IntPtr handle) + { + var closeHandle = new CloseHandle { Callback = callback, Handle = handle }; + lock (_closeHandleSync) + { + _closeHandleAdding.Enqueue(closeHandle); + } + } + + private void ThreadStart(object parameter) + { + lock (_startSync) + { + var tcs = (TaskCompletionSource)parameter; + try + { + _loop.Init(_libuv); + _post.Init(_loop, OnPost, EnqueueCloseHandle); + _initCompleted = true; + tcs.SetResult(0); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + } + + try + { + _loop.Run(); + if (_stopImmediate) + // thread-abort form of exit, resources will be leaked + return; + + // run the loop one more time to delete the open handles + _post.Reference(); + _post.Dispose(); + + // We need this walk because we call ReadStop on accepted connections when there's back pressure + // Calling ReadStop makes the handle as in-active which means the loop can + // end while there's still valid handles around. This makes loop.Dispose throw + // with an EBUSY. To avoid that, we walk all of the handles and dispose them. + Walk(ptr => + { + var handle = UvMemory.FromIntPtr(ptr); + // handle can be null because UvMemory.FromIntPtr looks up a weak reference + handle?.Dispose(); + }); + + // Ensure the Dispose operations complete in the event loop. + _loop.Run(); + + _loop.Dispose(); + } + catch (Exception ex) + { + _closeError = ex; + // Request shutdown so we can rethrow this exception + // in Stop which should be observable. + _appLifetime.StopApplication(); + } + finally + { + try + { + MemoryPool.Dispose(); + } + catch (Exception ex) + { + _closeError = _closeError == null ? ex : new AggregateException(_closeError, ex); + } + WriteReqPool.Dispose(); + _threadTcs.SetResult(null); + +#if DEBUG && !INNER_LOOP + // Check for handle leaks after disposing everything + CheckUvReqLeaks(); +#endif + } + } + + private void OnPost() + { + var loopsRemaining = _maxLoops; + bool wasWork; + do + { + wasWork = DoPostWork(); + wasWork = DoPostCloseHandle() || wasWork; + loopsRemaining--; + } while (wasWork && loopsRemaining > 0); + } + + private bool DoPostWork() + { + Queue queue; + lock (_workSync) + { + queue = _workAdding; + _workAdding = _workRunning; + _workRunning = queue; + } + + bool wasWork = queue.Count > 0; + + while (queue.Count != 0) + { + var work = queue.Dequeue(); + try + { + work.CallbackAdapter(work.Callback, work.State); + work.Completion?.TrySetResult(null); + } + catch (Exception ex) + { + if (work.Completion != null) + { + work.Completion.TrySetException(ex); + } + else + { + _log.LogError(0, ex, $"{nameof(LibuvThread)}.{nameof(DoPostWork)}"); + throw; + } + } + } + + return wasWork; + } + + private bool DoPostCloseHandle() + { + Queue queue; + lock (_closeHandleSync) + { + queue = _closeHandleAdding; + _closeHandleAdding = _closeHandleRunning; + _closeHandleRunning = queue; + } + + bool wasWork = queue.Count > 0; + + while (queue.Count != 0) + { + var closeHandle = queue.Dequeue(); + try + { + closeHandle.Callback(closeHandle.Handle); + } + catch (Exception ex) + { + _log.LogError(0, ex, $"{nameof(LibuvThread)}.{nameof(DoPostCloseHandle)}"); + throw; + } + } + + return wasWork; + } + + private static async Task WaitAsync(Task task, TimeSpan timeout) => + await Task.WhenAny(task, Task.Delay(timeout)).ConfigureAwait(false) == task; + + public override void Schedule(Action action, object state) + { + Post(action, state); + } + + private struct Work + { + public Action CallbackAdapter; + public object Callback; + public object State; + public TaskCompletionSource Completion; + } + + private struct CloseHandle + { + public Action Callback; + public IntPtr Handle; + } + + private class CallbackAdapter + { + public static readonly Action PostCallbackAdapter = (callback, state) => ((Action)callback).Invoke((T)state); + public static readonly Action PostAsyncCallbackAdapter = (callback, state) => ((Action)callback).Invoke((T)state); + } + } +} diff --git a/Projects/Server/LibUv/LibuvTrace.cs b/Projects/Server/LibUv/LibuvTrace.cs new file mode 100644 index 000000000..ebc090316 --- /dev/null +++ b/Projects/Server/LibUv/LibuvTrace.cs @@ -0,0 +1,94 @@ +// 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 Microsoft.Extensions.Logging; + +namespace Libuv +{ + public class LibuvTrace : ILibuvTrace + { + // ConnectionRead: Reserved: 3 + + private static readonly Action _connectionPause = + LoggerMessage.Define(LogLevel.Debug, new EventId(4, nameof(ConnectionPause)), @"Connection id ""{ConnectionId}"" paused."); + + private static readonly Action _connectionResume = + LoggerMessage.Define(LogLevel.Debug, new EventId(5, nameof(ConnectionResume)), @"Connection id ""{ConnectionId}"" resumed."); + + private static readonly Action _connectionReadFin = + LoggerMessage.Define(LogLevel.Debug, new EventId(6, nameof(ConnectionReadFin)), @"Connection id ""{ConnectionId}"" received FIN."); + + private static readonly Action _connectionWriteFin = + LoggerMessage.Define(LogLevel.Debug, new EventId(7, nameof(ConnectionWriteFin)), @"Connection id ""{ConnectionId}"" sending FIN because: ""{Reason}"""); + + // ConnectionWrite: Reserved: 11 + + // ConnectionWriteCallback: Reserved: 12 + + private static readonly Action _connectionError = + LoggerMessage.Define(LogLevel.Debug, new EventId(14, nameof(ConnectionError)), @"Connection id ""{ConnectionId}"" communication error."); + + private static readonly Action _connectionReset = + LoggerMessage.Define(LogLevel.Debug, new EventId(19, nameof(ConnectionReset)), @"Connection id ""{ConnectionId}"" reset."); + + private readonly ILogger _logger; + + public LibuvTrace(ILogger logger) => _logger = logger; + + public void ConnectionRead(string connectionId, int count) + { + // Don't log for now since this could be *too* verbose. + // Reserved: Event ID 3 + } + + public void ConnectionReadFin(string connectionId) + { + _connectionReadFin(_logger, connectionId, null); + } + + public void ConnectionWriteFin(string connectionId, string reason) + { + _connectionWriteFin(_logger, connectionId, reason, null); + } + + public void ConnectionWrite(string connectionId, int count) + { + // Don't log for now since this could be *too* verbose. + // Reserved: Event ID 11 + } + + public void ConnectionWriteCallback(string connectionId, int status) + { + // Don't log for now since this could be *too* verbose. + // Reserved: Event ID 12 + } + + public void ConnectionError(string connectionId, Exception ex) + { + _connectionError(_logger, connectionId, ex); + } + + public void ConnectionReset(string connectionId) + { + _connectionReset(_logger, connectionId, null); + } + + public void ConnectionPause(string connectionId) + { + _connectionPause(_logger, connectionId, null); + } + + public void ConnectionResume(string connectionId) + { + _connectionResume(_logger, connectionId, null); + } + + public IDisposable BeginScope(TState state) => _logger.BeginScope(state); + + public bool IsEnabled(LogLevel logLevel) => _logger.IsEnabled(logLevel); + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + => _logger.Log(logLevel, eventId, state, exception, formatter); + } +} diff --git a/Projects/Server/LibUv/LibuvTransportContext.cs b/Projects/Server/LibUv/LibuvTransportContext.cs new file mode 100644 index 000000000..730f31eb3 --- /dev/null +++ b/Projects/Server/LibUv/LibuvTransportContext.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.Extensions.Hosting; + +namespace Libuv +{ + public class LibuvTransportContext + { + public LibuvTransportOptions Options { get; set; } + + public IHostApplicationLifetime AppLifetime { get; set; } + + public ILibuvTrace Log { get; set; } + } +} diff --git a/Projects/Server/LibUv/LibuvTransportOptions.cs b/Projects/Server/LibUv/LibuvTransportOptions.cs new file mode 100644 index 000000000..9de07cc83 --- /dev/null +++ b/Projects/Server/LibUv/LibuvTransportOptions.cs @@ -0,0 +1,51 @@ +// 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; + +namespace Libuv +{ + /// + /// Provides programmatic configuration of Libuv transport features. + /// + public class LibuvTransportOptions + { + /// + /// The number of libuv I/O threads used to process requests. + /// + /// + /// Defaults to half of rounded down and clamped between 1 and 16. + /// + public int ThreadCount { get; set; } = ProcessorThreadCount; + + /// + /// The maximum length of the pending connection queue. + /// + /// + /// Defaults to 128. + /// + public int Backlog { get; set; } = 128; + + public long? MaxReadBufferSize { get; set; } = 1024 * 1024; + + public long? MaxWriteBufferSize { get; set; } = 64 * 1024; + + internal Func> MemoryPoolFactory { get; set; } = SlabMemoryPoolFactory.Create; + + private static int ProcessorThreadCount + { + get + { + // Actual core count would be a better number + // rather than logical cores which includes hyper-threaded cores. + // Divide by 2 for hyper-threading, and good defaults (still need threads for the game thread). + var threadCount = Environment.ProcessorCount >> 1; + + // Receive Side Scaling RSS Processor count currently maxes out at 16 + // would be better to check the NIC's current hardware queues; but xplat... + return threadCount < 1 ? 1 : threadCount > 16 ? 16 : threadCount; + } + } + } +} diff --git a/Projects/Server/LibUv/Listener.cs b/Projects/Server/LibUv/Listener.cs new file mode 100644 index 000000000..4ed799b78 --- /dev/null +++ b/Projects/Server/LibUv/Listener.cs @@ -0,0 +1,205 @@ +// 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.Net; +using System.Net.Sockets; +using System.Threading.Tasks; +using Libuv.Internal; +using Microsoft.AspNetCore.Connections; +using Microsoft.Extensions.Logging; + +namespace Libuv +{ + /// + /// Base class for listeners in Kestrel. Listens for incoming connections + /// + public class Listener : ListenerContext, IAsyncDisposable + { + // REVIEW: This needs to be bounded and we need a strategy for what to do when the queue is full + private bool _closed; + + public Listener(LibuvTransportContext transportContext) : base(transportContext) + { + } + + protected UvStreamHandle ListenSocket { get; private set; } + + public ILibuvTrace Log => TransportContext.Log; + + public Task StartAsync( + EndPoint endPoint, + LibuvThread thread) + { + EndPoint = endPoint; + Thread = thread; + + return Thread.PostAsync(listener => + { + listener.ListenSocket = listener.CreateListenSocket(); + listener.ListenSocket.Listen(TransportContext.Options.Backlog, ConnectionCallback, listener); + }, this); + } + + /// + /// Creates the socket used to listen for incoming connections + /// + private UvStreamHandle CreateListenSocket() + { + return EndPoint switch + { + IPEndPoint _ => ListenTcp(false), + UnixDomainSocketEndPoint _ => ListenPipe(false), + FileHandleEndPoint _ => ListenHandle(), + _ => throw new NotSupportedException() + }; + } + + private UvTcpHandle ListenTcp(bool useFileHandle) + { + var socket = new UvTcpHandle(Log); + + try + { + socket.Init(Thread.Loop, Thread.QueueCloseHandle); + socket.NoDelay(true); + + if (!useFileHandle) + { + socket.Bind((IPEndPoint)EndPoint); + + // If requested port was "0", replace with assigned dynamic port. + EndPoint = socket.GetSockIPEndPoint(); + } + else + { + socket.Open((IntPtr)((FileHandleEndPoint)EndPoint).FileHandle); + } + } + catch + { + socket.Dispose(); + throw; + } + + return socket; + } + + private UvPipeHandle ListenPipe(bool useFileHandle) + { + var pipe = new UvPipeHandle(Log); + + try + { + pipe.Init(Thread.Loop, Thread.QueueCloseHandle, false); + + if (!useFileHandle) + // UnixDomainSocketEndPoint.ToString() returns the path + pipe.Bind(EndPoint.ToString()); + else + pipe.Open((IntPtr)((FileHandleEndPoint)EndPoint).FileHandle); + } + catch + { + pipe.Dispose(); + throw; + } + + return pipe; + } + + private UvStreamHandle ListenHandle() + { + var handleEndPoint = (FileHandleEndPoint)EndPoint; + + switch (handleEndPoint.FileHandleType) + { + case FileHandleType.Auto: + break; + case FileHandleType.Tcp: + return ListenTcp(true); + case FileHandleType.Pipe: + return ListenPipe(true); + default: + throw new NotSupportedException(); + } + + UvStreamHandle handle; + try + { + handle = ListenTcp(true); + EndPoint = new FileHandleEndPoint(handleEndPoint.FileHandle, FileHandleType.Tcp); + return handle; + } + catch (UvException exception) when (exception.StatusCode == LibuvConstants.ENOTSUP) + { + Log.LogDebug(0, exception, "Listener.ListenHandle"); + } + + handle = ListenPipe(true); + EndPoint = new FileHandleEndPoint(handleEndPoint.FileHandle, FileHandleType.Pipe); + return handle; + } + + private static void ConnectionCallback(UvStreamHandle stream, int status, UvException error, object state) + { + var listener = (Listener)state; + + if (error != null) + listener.Log.LogError(0, error, "Listener.ConnectionCallback"); + else if (!listener._closed) listener.OnConnection(stream, status); + } + + /// + /// Handles an incoming connection + /// + /// Socket being used to listen on + /// Connection status + private void OnConnection(UvStreamHandle listenSocket, int status) + { + UvStreamHandle acceptSocket = null; + + try + { + acceptSocket = CreateAcceptSocket(); + listenSocket.Accept(acceptSocket); + DispatchConnection(acceptSocket); + } + catch (UvException ex) when (LibuvConstants.IsConnectionReset(ex.StatusCode)) + { + Log.ConnectionReset("(null)"); + acceptSocket?.Dispose(); + } + catch (UvException ex) + { + Log.LogError(0, ex, "Listener.OnConnection"); + acceptSocket?.Dispose(); + } + } + + protected virtual void DispatchConnection(UvStreamHandle socket) + { + HandleConnection(socket); + } + + public virtual async Task DisposeAsync() + { + // Ensure the event loop is still running. + // If the event loop isn't running and we try to wait on this Post + // to complete, then LibuvTransport will never be disposed and + // the exception that stopped the event loop will never be surfaced. + if (Thread.FatalError == null && ListenSocket != null) + await Thread.PostAsync(listener => + { + listener.ListenSocket.Dispose(); + + listener._closed = true; + + listener.StopAcceptingConnections(); + + }, this).ConfigureAwait(false); + + ListenSocket = null; + } + } +} diff --git a/Projects/Server/LibUv/ListenerContext.cs b/Projects/Server/LibUv/ListenerContext.cs new file mode 100644 index 000000000..52b5b9149 --- /dev/null +++ b/Projects/Server/LibUv/ListenerContext.cs @@ -0,0 +1,168 @@ +// 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.Diagnostics; +using System.IO.Pipelines; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Libuv.Internal; +using Microsoft.AspNetCore.Connections; +using Microsoft.Extensions.Logging; + +namespace Libuv +{ + public class ListenerContext + { + // Single reader, single writer queue since all writes happen from the uv thread and reads happen sequentially + private readonly Channel _acceptQueue = Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = true + }); + + public ListenerContext(LibuvTransportContext transportContext) => TransportContext = transportContext; + + public LibuvTransportContext TransportContext { get; set; } + + public EndPoint EndPoint { get; set; } + + public LibuvThread Thread { get; set; } + + public PipeOptions InputOptions { get; set; } + + public PipeOptions OutputOptions { get; set; } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + while (await _acceptQueue.Reader.WaitToReadAsync()) + while (_acceptQueue.Reader.TryRead(out var connection)) + return connection; + + return null; + } + + /// + /// Aborts all unaccepted connections in the queue + /// + /// + public async Task AbortQueuedConnectionAsync() + { + while (await _acceptQueue.Reader.WaitToReadAsync()) + while (_acceptQueue.Reader.TryRead(out var connection)) + // REVIEW: Pass an abort reason? + connection.Abort(); + } + + /// + /// Creates a socket which can be used to accept an incoming connection. + /// + protected UvStreamHandle CreateAcceptSocket() + { + switch (EndPoint) + { + case IPEndPoint _: + return AcceptTcp(); + case UnixDomainSocketEndPoint _: + return AcceptPipe(); + case FileHandleEndPoint _: + return AcceptHandle(); + default: + throw new InvalidOperationException(); + } + } + + protected internal void HandleConnection(UvStreamHandle socket) + { + try + { + IPEndPoint remoteEndPoint = null; + IPEndPoint localEndPoint = null; + + if (socket is UvTcpHandle tcpHandle) + try + { + remoteEndPoint = tcpHandle.GetPeerIPEndPoint(); + localEndPoint = tcpHandle.GetSockIPEndPoint(); + } + catch (UvException ex) when (LibuvConstants.IsConnectionReset(ex.StatusCode)) + { + TransportContext.Log.ConnectionReset("(null)"); + socket.Dispose(); + return; + } + + var options = TransportContext.Options; + var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint, InputOptions, OutputOptions, options.MaxReadBufferSize, options.MaxWriteBufferSize); + connection.Start(); + + bool accepted = _acceptQueue.Writer.TryWrite(connection); + Debug.Assert(accepted, "The connection was not written to the channel!"); + } + catch (Exception ex) + { + TransportContext.Log.LogCritical(ex, $"Unexpected exception in {nameof(ListenerContext)}.{nameof(HandleConnection)}."); + } + } + + private UvTcpHandle AcceptTcp() + { + var socket = new UvTcpHandle(TransportContext.Log); + + try + { + socket.Init(Thread.Loop, Thread.QueueCloseHandle); + socket.NoDelay(true); + } + catch + { + socket.Dispose(); + throw; + } + + return socket; + } + + private UvPipeHandle AcceptPipe() + { + var pipe = new UvPipeHandle(TransportContext.Log); + + try + { + pipe.Init(Thread.Loop, Thread.QueueCloseHandle); + } + catch + { + pipe.Dispose(); + throw; + } + + return pipe; + } + + protected void StopAcceptingConnections() + { + _acceptQueue.Writer.TryComplete(); + } + + private UvStreamHandle AcceptHandle() + { + var fileHandleEndPoint = (FileHandleEndPoint)EndPoint; + + switch (fileHandleEndPoint.FileHandleType) + { + case FileHandleType.Auto: + throw new InvalidOperationException("Cannot accept on a non-specific file handle, listen should be performed first."); + case FileHandleType.Tcp: + return AcceptTcp(); + case FileHandleType.Pipe: + return AcceptPipe(); + default: + throw new NotSupportedException(); + } + } + } +} diff --git a/Projects/Server/LibUv/ListenerPrimary.cs b/Projects/Server/LibUv/ListenerPrimary.cs new file mode 100644 index 000000000..da006b7fe --- /dev/null +++ b/Projects/Server/LibUv/ListenerPrimary.cs @@ -0,0 +1,265 @@ +// 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.Collections.Generic; +using System.IO; +using System.Net; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Libuv.Internal; +using Microsoft.Extensions.Logging; +using Server; + +#pragma warning disable 169 + +namespace Libuv +{ + /// + /// A primary listener waits for incoming connections on a specified socket. Incoming + /// connections may be passed to a secondary listener to handle. + /// + public class ListenerPrimary : Listener + { + // The list of pipes that can be dispatched to (where we've confirmed the _pipeMessage) + private readonly List _dispatchPipes = new List(); + // The list of pipes we've created but may not be part of _dispatchPipes + private readonly List _createdPipes = new List(); + private int _dispatchIndex; + private string _pipeName; + private byte[] _pipeMessage; + private IntPtr _fileCompletionInfoPtr; + private bool _tryDetachFromIOCP = Core.IsWindows; + + // this message is passed to write2 because it must be non-zero-length, + // but it has no other functional significance + private readonly ArraySegment> _dummyMessage = new ArraySegment>(new[] { new ArraySegment(new byte[] { 1, 2, 3, 4 }) }); + + public ListenerPrimary(LibuvTransportContext transportContext) : base(transportContext) + { + } + + /// + /// For testing purposes. + /// + public int UvPipeCount => _dispatchPipes.Count; + + private UvPipeHandle ListenPipe { get; set; } + + public async Task StartAsync( + string pipeName, + byte[] pipeMessage, + EndPoint endPoint, + LibuvThread thread) + { + _pipeName = pipeName; + _pipeMessage = pipeMessage; + + if (_fileCompletionInfoPtr == IntPtr.Zero) + { + var fileCompletionInfo = new FILE_COMPLETION_INFORMATION { Key = IntPtr.Zero, Port = IntPtr.Zero }; + _fileCompletionInfoPtr = Marshal.AllocHGlobal(Marshal.SizeOf(fileCompletionInfo)); + Marshal.StructureToPtr(fileCompletionInfo, _fileCompletionInfoPtr, false); + } + + await StartAsync(endPoint, thread).ConfigureAwait(false); + + await Thread.PostAsync(listener => listener.PostCallback(), this).ConfigureAwait(false); + } + + private void PostCallback() + { + ListenPipe = new UvPipeHandle(Log); + ListenPipe.Init(Thread.Loop, Thread.QueueCloseHandle, false); + ListenPipe.Bind(_pipeName); + ListenPipe.Listen(TransportContext.Options.Backlog, + (pipe, status, error, state) => ((ListenerPrimary)state).OnListenPipe(pipe, status, error), this); + } + + private void OnListenPipe(UvStreamHandle pipe, int status, UvException error) + { + if (status < 0) return; + + var dispatchPipe = new UvPipeHandle(Log); + // Add to the list of created pipes for disposal tracking + _createdPipes.Add(dispatchPipe); + + try + { + dispatchPipe.Init(Thread.Loop, Thread.QueueCloseHandle, true); + pipe.Accept(dispatchPipe); + + // Ensure client sends _pipeMessage before adding pipe to _dispatchPipes. + var readContext = new PipeReadContext(this); + dispatchPipe.ReadStart( + (handle, status2, state) => ((PipeReadContext)state).AllocCallback(handle, status2), + (handle, status2, state) => ((PipeReadContext)state).ReadCallback(handle, status2), + readContext); + } + catch (UvException ex) + { + dispatchPipe.Dispose(); + Log.LogError(0, ex, "ListenerPrimary.OnListenPipe"); + } + } + + protected override void DispatchConnection(UvStreamHandle socket) + { + var index = _dispatchIndex++ % (_dispatchPipes.Count + 1); + if (index == _dispatchPipes.Count) + { + base.DispatchConnection(socket); + } + else + { + DetachFromIOCP(socket); + var dispatchPipe = _dispatchPipes[index]; + var write = new UvWriteReq(Log); + try + { + write.Init(Thread); + write.Write2( + dispatchPipe, + _dummyMessage, + socket, + (write2, status, error, state) => + { + write2.Dispose(); + ((UvStreamHandle)state).Dispose(); + }, + socket); + } + catch (UvException) + { + write.Dispose(); + throw; + } + } + } + + private void DetachFromIOCP(UvHandle handle) + { + if (!_tryDetachFromIOCP) return; + + // https://msdn.microsoft.com/en-us/library/windows/hardware/ff728840(v=vs.85).aspx + const int FileReplaceCompletionInformation = 61; + // https://msdn.microsoft.com/en-us/library/cc704588.aspx + const uint STATUS_INVALID_INFO_CLASS = 0xC0000003; + + var statusBlock = new IO_STATUS_BLOCK(); + var socket = IntPtr.Zero; + Thread.Loop.Libuv.uv_fileno(handle, ref socket); + + if (NtSetInformationFile(socket, out statusBlock, _fileCompletionInfoPtr, + (uint)Marshal.SizeOf(), FileReplaceCompletionInformation) == STATUS_INVALID_INFO_CLASS) + // Replacing IOCP information is only supported on Windows 8.1 or newer + _tryDetachFromIOCP = false; + } + + private struct IO_STATUS_BLOCK + { + uint status; + ulong information; + } + + private struct FILE_COMPLETION_INFORMATION + { + public IntPtr Port; + public IntPtr Key; + } + + [DllImport("NtDll.dll")] + private static extern uint NtSetInformationFile(IntPtr FileHandle, + out IO_STATUS_BLOCK IoStatusBlock, IntPtr FileInformation, uint Length, + int FileInformationClass); + + public override async Task DisposeAsync() + { + // Call base first so the ListenSocket gets closed and doesn't + // try to dispatch connections to closed pipes. + await base.DisposeAsync().ConfigureAwait(false); + + if (_fileCompletionInfoPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_fileCompletionInfoPtr); + _fileCompletionInfoPtr = IntPtr.Zero; + } + + if (Thread.FatalError == null && ListenPipe != null) + await Thread.PostAsync(listener => + { + listener.ListenPipe.Dispose(); + + foreach (var pipe in listener._createdPipes) pipe.Dispose(); + }, this).ConfigureAwait(false); + } + + private class PipeReadContext + { + private const int _bufferLength = 16; + + private readonly ListenerPrimary _listener; + private readonly byte[] _buf = new byte[_bufferLength]; + private readonly IntPtr _bufPtr; + private GCHandle _bufHandle; + private int _bytesRead; + + public PipeReadContext(ListenerPrimary listener) + { + _listener = listener; + _bufHandle = GCHandle.Alloc(_buf, GCHandleType.Pinned); + _bufPtr = _bufHandle.AddrOfPinnedObject(); + } + + public LibuvFunctions.uv_buf_t AllocCallback(UvStreamHandle dispatchPipe, int suggestedSize) => dispatchPipe.Libuv.buf_init(_bufPtr + _bytesRead, _bufferLength - _bytesRead); + + public void ReadCallback(UvStreamHandle dispatchPipe, int status) + { + if (status == LibuvConstants.EOF && _bytesRead == 0) + { + // This is an unexpected immediate termination of the dispatch pipe most likely caused by an + // external process scanning the pipe, so don't we don't log it too severely. + // https://github.com/aspnet/AspNetCore/issues/4741 + + dispatchPipe.Dispose(); + _bufHandle.Free(); + _listener.Log.LogDebug("An internal pipe was opened unexpectedly."); + return; + } + + try + { + dispatchPipe.Libuv.ThrowIfErrored(status); + + _bytesRead += status; + + if (_bytesRead == _bufferLength) + { + var correctMessage = true; + + for (var i = 0; i < _bufferLength; i++) + if (_buf[i] != _listener._pipeMessage[i]) + correctMessage = false; + + if (correctMessage) + { + _listener._dispatchPipes.Add((UvPipeHandle)dispatchPipe); + dispatchPipe.ReadStop(); + _bufHandle.Free(); + } + else + { + throw new IOException("Bad data sent over an internal pipe."); + } + } + } + catch (Exception ex) + { + dispatchPipe.Dispose(); + _bufHandle.Free(); + _listener.Log.LogError(0, ex, "ListenerPrimary.ReadCallback"); + } + } + } + } +} diff --git a/Projects/Server/LibUv/ListenerSecondary.cs b/Projects/Server/LibUv/ListenerSecondary.cs new file mode 100644 index 000000000..4377a4e9c --- /dev/null +++ b/Projects/Server/LibUv/ListenerSecondary.cs @@ -0,0 +1,192 @@ +// 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.Net; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Libuv.Internal; +using Microsoft.Extensions.Logging; + +namespace Libuv +{ + /// + /// A secondary listener is delegated requests from a primary listener via a named pipe or + /// UNIX domain socket. + /// + public class ListenerSecondary : ListenerContext, IAsyncDisposable + { + private string _pipeName; + private byte[] _pipeMessage; + private IntPtr _ptr; + private LibuvFunctions.uv_buf_t _buf; + private bool _closed; + + public ListenerSecondary(LibuvTransportContext transportContext) : base(transportContext) => _ptr = Marshal.AllocHGlobal(4); + + UvPipeHandle DispatchPipe { get; set; } + + public ILibuvTrace Log => TransportContext.Log; + + public Task StartAsync( + string pipeName, + byte[] pipeMessage, + EndPoint endPoint, + LibuvThread thread) + { + _pipeName = pipeName; + _pipeMessage = pipeMessage; + _buf = thread.Loop.Libuv.buf_init(_ptr, 4); + + EndPoint = endPoint; + Thread = thread; + DispatchPipe = new UvPipeHandle(Log); + + var tcs = new TaskCompletionSource(this, TaskCreationOptions.RunContinuationsAsynchronously); + Thread.Post(StartCallback, tcs); + return tcs.Task; + } + + private static void StartCallback(TaskCompletionSource tcs) + { + var listener = (ListenerSecondary)tcs.Task.AsyncState; + listener.StartedCallback(tcs); + } + + private void StartedCallback(TaskCompletionSource tcs) + { + var connect = new UvConnectRequest(Log); + try + { + DispatchPipe.Init(Thread.Loop, Thread.QueueCloseHandle, true); + connect.Init(Thread); + connect.Connect( + DispatchPipe, + _pipeName, + (connect2, status, error, state) => ConnectCallback(connect2, status, error, (TaskCompletionSource)state), + tcs); + } + catch (Exception ex) + { + DispatchPipe.Dispose(); + connect.Dispose(); + tcs.SetException(ex); + } + } + + private static void ConnectCallback(UvConnectRequest connect, int status, UvException error, TaskCompletionSource tcs) + { + var listener = (ListenerSecondary)tcs.Task.AsyncState; + _ = listener.ConnectedCallback(connect, status, error, tcs); + } + + private async Task ConnectedCallback(UvConnectRequest connect, int status, UvException error, TaskCompletionSource tcs) + { + connect.Dispose(); + if (error != null) + { + tcs.SetException(error); + return; + } + + var writeReq = new UvWriteReq(Log); + + try + { + DispatchPipe.ReadStart( + (handle, status2, state) => ((ListenerSecondary)state)._buf, + (handle, status2, state) => ((ListenerSecondary)state).ReadStartCallback(handle, status2), + this); + + writeReq.Init(Thread); + var result = await writeReq.WriteAsync( + DispatchPipe, + new ArraySegment>(new[] { new ArraySegment(_pipeMessage) })); + + if (result.Error != null) + tcs.SetException(result.Error); + else + tcs.SetResult(0); + } + catch (Exception ex) + { + DispatchPipe.Dispose(); + tcs.SetException(ex); + } + finally + { + writeReq.Dispose(); + } + } + + private void ReadStartCallback(UvStreamHandle handle, int status) + { + if (status < 0) + { + if (status != LibuvConstants.EOF) + { + Thread.Loop.Libuv.Check(status, out var ex); + Log.LogError(0, ex, "DispatchPipe.ReadStart"); + } + + DispatchPipe.Dispose(); + return; + } + + if (_closed || DispatchPipe.PendingCount() == 0) return; + + var acceptSocket = CreateAcceptSocket(); + + try + { + DispatchPipe.Accept(acceptSocket); + + HandleConnection(acceptSocket); + } + catch (UvException ex) when (LibuvConstants.IsConnectionReset(ex.StatusCode)) + { + Log.ConnectionReset("(null)"); + acceptSocket.Dispose(); + } + catch (UvException ex) + { + Log.LogError(0, ex, "DispatchPipe.Accept"); + acceptSocket.Dispose(); + } + } + + private void FreeBuffer() + { + var ptr = Interlocked.Exchange(ref _ptr, IntPtr.Zero); + if (ptr != IntPtr.Zero) Marshal.FreeHGlobal(ptr); + } + + public async Task DisposeAsync() + { + // Ensure the event loop is still running. + // If the event loop isn't running and we try to wait on this Post + // to complete, then LibuvTransport will never be disposed and + // the exception that stopped the event loop will never be surfaced. + if (Thread.FatalError == null) + { + await Thread.PostAsync(listener => + { + listener.DispatchPipe.Dispose(); + listener.FreeBuffer(); + + listener._closed = true; + + listener.StopAcceptingConnections(); + + }, this).ConfigureAwait(false); + } + else + { + FreeBuffer(); + + StopAcceptingConnections(); + } + } + } +} diff --git a/Projects/Server/LibUv/WriteReqPool.cs b/Projects/Server/LibUv/WriteReqPool.cs new file mode 100644 index 000000000..745874344 --- /dev/null +++ b/Projects/Server/LibUv/WriteReqPool.cs @@ -0,0 +1,63 @@ +// 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.Collections.Generic; +using Libuv.Internal; + +namespace Libuv +{ + public class WriteReqPool + { + private const int _maxPooledWriteReqs = 1024; + + private readonly LibuvThread _thread; + private readonly Queue _pool = new Queue(_maxPooledWriteReqs); + private readonly ILibuvTrace _log; + private bool _disposed; + + public WriteReqPool(LibuvThread thread, ILibuvTrace log) + { + _thread = thread; + _log = log; + } + + public UvWriteReq Allocate() + { + if (_disposed) throw new ObjectDisposedException(GetType().Name); + + UvWriteReq req; + if (_pool.Count > 0) + { + req = _pool.Dequeue(); + } + else + { + req = new UvWriteReq(_log); + req.Init(_thread); + } + + return req; + } + + public void Return(UvWriteReq req) + { + if (_disposed) throw new ObjectDisposedException(GetType().Name); + + if (_pool.Count < _maxPooledWriteReqs) + _pool.Enqueue(req); + else + req.Dispose(); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + + while (_pool.Count > 0) _pool.Dequeue().Dispose(); + } + } + } +} diff --git a/Projects/Server/Main.cs b/Projects/Server/Main.cs index 53a975885..bb1b29bc9 100644 --- a/Projects/Server/Main.cs +++ b/Projects/Server/Main.cs @@ -143,9 +143,13 @@ namespace Server public static int ProcessorCount{ get; private set; } - public static bool Unix{ get; private set; } + public static bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + public static bool IsDarwin = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + public static bool IsFreeBSD = RuntimeInformation.IsOSPlatform(OSPlatform.FreeBSD); + public static bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || IsFreeBSD; + public static bool Unix = IsDarwin || IsFreeBSD || IsLinux; - public static string ExePath => m_ExePath ?? (m_ExePath = Assembly.Location); + public static string ExePath => m_ExePath ??= Assembly.Location; public static string BaseDirectory { @@ -261,8 +265,9 @@ namespace Server { try { - foreach (Listener l in MessagePump.Listeners) - l.Dispose(); + Task.WhenAll( + MessagePump.Listeners.Select(listener => listener.Dispose()) + ).Wait(); } catch { @@ -413,14 +418,7 @@ namespace Server Console.WriteLine("Core: Optimizing for {0} {2}processor{1}", ProcessorCount, ProcessorCount == 1 ? "" : "s", Is64Bit ? "64-bit " : ""); - int platform = (int)Environment.OSVersion.Platform; - if (platform == 4 || platform == 128) - { - // MS 4, MONO 128 - Unix = true; - Console.WriteLine("Core: Unix environment detected"); - } - else + if (IsWindows) { m_ConsoleEventHandler = OnConsoleEvent; UnsafeNativeMethods.SetConsoleCtrlHandler(m_ConsoleEventHandler, true); diff --git a/Projects/Server/Mobile.cs b/Projects/Server/Mobile.cs index 0fe1f420e..5a000073c 100644 --- a/Projects/Server/Mobile.cs +++ b/Projects/Server/Mobile.cs @@ -1445,7 +1445,7 @@ namespace Server [CommandProperty(AccessLevel.GameMaster, AccessLevel.Owner)] public NetState NetState { - get => m_NetState?.Socket != null && !m_NetState.IsDisposing ? m_NetState : null; + get => m_NetState?.Connection != null && !m_NetState.IsDisposing ? m_NetState : null; set { if (m_NetState != value) diff --git a/Projects/Server/Network/Listener.cs b/Projects/Server/Network/Listener.cs index 2146f9e16..f0b929748 100644 --- a/Projects/Server/Network/Listener.cs +++ b/Projects/Server/Network/Listener.cs @@ -24,53 +24,54 @@ using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using Libuv; +using Libuv.Internal; +using Microsoft.AspNetCore.Connections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Logging; namespace Server.Network { public class Listener { - private Socket m_Socket; - private IPEndPoint m_EndPoint; + private static readonly LibuvFunctions functions = new LibuvFunctions(); + + private readonly IPEndPoint m_EndPoint; + private LibuvConnectionListener m_Listener; + public Listener(IPEndPoint ipep) { -#pragma warning disable IDE0068 // Use recommended dispose pattern - m_Socket = new Socket(ipep.AddressFamily, SocketType.Stream, ProtocolType.Tcp); -#pragma warning restore IDE0068 // Use recommended dispose pattern - - m_Socket.LingerState.Enabled = false; - m_Socket.ExclusiveAddressUse = false; m_EndPoint = ipep; + LibuvTransportContext transport = new LibuvTransportContext + { + Options = new LibuvTransportOptions(), + AppLifetime = new ApplicationLifetime( + LoggerFactory.Create(builder => { builder.AddConsole(); }).CreateLogger() + ), + Log = new LibuvTrace(LoggerFactory.Create(builder => { builder.AddConsole(); }).CreateLogger("network")) + }; + + m_Listener = new LibuvConnectionListener(functions, transport, ipep); } public virtual async Task Start(MessagePump pump) { try { - m_Socket.Bind(m_EndPoint); - m_Socket.Listen(8); + await m_Listener.BindAsync(); + } + catch (AddressInUseException) + { + Console.WriteLine("Listener Failed: {0}:{1} (In Use)", m_EndPoint.Address, m_EndPoint.Port); + m_Listener = null; + return; } catch (Exception e) { - if (e is SocketException se) - { - if (se.ErrorCode == 10048) - { - // WSAEADDRINUSE - Console.WriteLine("Listener Failed: {0}:{1} (In Use)", m_EndPoint.Address, m_EndPoint.Port); - } - else if (se.ErrorCode == 10049) - { - // WSAEADDRNOTAVAIL - Console.WriteLine("Listener Failed: {0}:{1} (Unavailable)", m_EndPoint.Address, m_EndPoint.Port); - } - else - { - Console.WriteLine("Listener Exception:"); - Console.WriteLine(e); - } - } + Console.WriteLine("Listener Exception:"); + Console.WriteLine(e); - m_Socket = null; + m_Listener = null; return; } @@ -78,10 +79,10 @@ namespace Server.Network while (true) { - Socket s; + ConnectionContext context; try { - s = await m_Socket.AcceptAsync().ConfigureAwait(false); + context = await m_Listener.AcceptAsync(); } catch (SocketException ex) { @@ -89,16 +90,16 @@ namespace Server.Network continue; } - if (VerifySocket(s)) - _ = new NetState(s, pump); + if (VerifySocket(context)) + _ = new NetState(context, pump); else - Release(s); + Release(context); } } private void DisplayListener() { - if (!(m_Socket.LocalEndPoint is IPEndPoint ipep)) + if (!(m_Listener.EndPoint is IPEndPoint ipep)) return; if (ipep.Address.Equals(IPAddress.Any) || ipep.Address.Equals(IPAddress.IPv6Any)) @@ -113,16 +114,14 @@ namespace Server.Network } } else - { Console.WriteLine("Listening: {0}:{1}", ipep.Address, ipep.Port); - } } - private bool VerifySocket(Socket socket) + private static bool VerifySocket(ConnectionContext context) { try { - SocketConnectEventArgs args = new SocketConnectEventArgs(socket); + SocketConnectEventArgs args = new SocketConnectEventArgs(context); EventSink.InvokeSocketConnect(args); @@ -131,44 +130,47 @@ namespace Server.Network catch (Exception ex) { NetState.TraceException(ex); - return false; } } - private void Release(Socket socket) + private static void Release(ConnectionContext context) { try { - socket.Shutdown(SocketShutdown.Both); + context.Abort(new ConnectionAbortedException("Failed socket verification.")); } - catch (SocketException ex) + catch (Exception ex) { NetState.TraceException(ex); } try { - socket.Close(); + // TODO: Is this needed? + context.DisposeAsync(); } - catch (SocketException ex) - { - NetState.TraceException(ex); - } - - try - { - socket.Dispose(); - } - catch (SocketException ex) + catch (Exception ex) { NetState.TraceException(ex); } } - public void Dispose() + public async Task Dispose() { - Interlocked.Exchange(ref m_Socket, null)?.Close(); + LibuvConnectionListener listener = Interlocked.Exchange(ref m_Listener, null); + if (listener != null) + try + { + await listener.UnbindAsync(); + await listener.DisposeAsync(); + } + catch (Exception ex) + { + Console.WriteLine("Listener: Failed to dispose."); + Console.WriteLine(ex); + } + GC.SuppressFinalize(this); } } diff --git a/Projects/Server/Network/MessagePump.cs b/Projects/Server/Network/MessagePump.cs index 0919630d9..3904d73aa 100644 --- a/Projects/Server/Network/MessagePump.cs +++ b/Projects/Server/Network/MessagePump.cs @@ -23,6 +23,8 @@ using System; using System.Buffers; using System.Collections.Concurrent; using System.Net; +using System.Threading.Tasks; +using SignalR; namespace Server.Network { @@ -40,9 +42,9 @@ namespace Server.Network listeners[Listeners.Length] = listener; } - public void QueueWork(NetState ns, in ReadOnlySequence seq, OnPacketReceive onReceive) + public void QueueWork(NetState ns, IMemoryOwner memOwner, OnPacketReceive onReceive) { - m_WorkQueue.Enqueue(new Work(ns, seq, onReceive)); + m_WorkQueue.Enqueue(new Work(ns, memOwner, onReceive)); Core.Set(); } @@ -54,21 +56,22 @@ namespace Server.Network if (!m_WorkQueue.TryDequeue(out Work work)) break; - work.OnReceive(work.State, new PacketReader(work.Sequence)); + work.OnReceive(work.State, new PacketReader(new ReadOnlySequence(work.MemoryOwner.Memory))); + work.MemoryOwner.Dispose(); } } - // TODO: Optimize this with a pool private class Work { - public NetState State; - public ReadOnlySequence Sequence; - public OnPacketReceive OnReceive; + public readonly NetState State; + // TODO: Force dispose? + public readonly IMemoryOwner MemoryOwner; + public readonly OnPacketReceive OnReceive; - public Work(NetState ns, in ReadOnlySequence seq, OnPacketReceive onReceive) + public Work(NetState ns, IMemoryOwner memOwner, OnPacketReceive onReceive) { State = ns; - Sequence = seq; + MemoryOwner = memOwner; OnReceive = onReceive; } } diff --git a/Projects/Server/Network/NetState.cs b/Projects/Server/Network/NetState.cs index 14355146d..290f6a6e9 100644 --- a/Projects/Server/Network/NetState.cs +++ b/Projects/Server/Network/NetState.cs @@ -28,6 +28,7 @@ using System.Net; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Connections; using Server.Accounting; using Server.Diagnostics; using Server.Gumps; @@ -93,7 +94,6 @@ namespace Server.Network { private string m_ToString; private ClientVersion m_Version; - private SendQueue m_SendQueue = new SendQueue(); public DateTime ConnectedOn { get; } @@ -266,11 +266,9 @@ namespace Server.Network return newTrade.From.Container; } - public bool Running { get; private set; } - public bool Seeded { get; set; } - public Socket Socket { get; private set; } + public ConnectionContext Connection { get; private set; } public bool CompressionEnabled { get; set; } @@ -282,12 +280,16 @@ namespace Server.Network public List Menus { get; private set; } + public PipeReader RecvPipe => Connection.Transport.Input; + public PipeWriter SendPipe => Connection.Transport.Output; + public static int GumpCap { get; set; } = 512; public static int HuePickerCap { get; set; } = 512; public static int MenuCap { get; set; } = 512; + public void WriteConsole(string text) { Console.WriteLine("Client: {0}: {1}", this, text); @@ -403,23 +405,24 @@ namespace Server.Network public static List Instances { get; } = new List(); - public NetState(Socket socket, MessagePump pump) + public void SetConnectionAlive() => m_NextCheckActivity = Core.TickCount + 60000; + + public NetState(ConnectionContext connection, MessagePump pump) { - Socket = socket; + Connection = connection; Seeded = false; - Running = false; Gumps = new List(); HuePickers = new List(); Menus = new List(); Trades = new List(); - m_NextCheckActivity = Core.TickCount + 30000; + SetConnectionAlive(); Instances.Add(this); try { - Address = Utility.Intern(((IPEndPoint)Socket.RemoteEndPoint).Address); + Address = Utility.Intern(((IPEndPoint)Connection.RemoteEndPoint).Address); m_ToString = Address.ToString(); } catch (Exception ex) @@ -430,10 +433,11 @@ namespace Server.Network } ConnectedOn = DateTime.UtcNow; - _ = Start(pump); + Console.WriteLine("Client: {0}: Connected. [{1} Online]", this, Instances.Count); + + _ = ProcessRecvs(pump); CreatedCallback?.Invoke(this); - Console.WriteLine("Client: {0}: Connected. [{1} Online]", this, NetState.Instances.Count); } public static void Pause() @@ -448,13 +452,38 @@ namespace Server.Network public virtual void Send(Packet p) { - if (Socket == null || BlockAllPackets) + if (Connection == null || BlockAllPackets) { p.OnSend(); return; } - m_SendQueue.Enqueue(p); + try + { + ReadOnlySpan buffer = p.Compile(CompressionEnabled, out int length); + + if (buffer.Length <= 0 || length <= 0) + { + p.OnSend(); + return; + } + + buffer.Slice(0, length).CopyTo(SendPipe.GetSpan(length)); + SendPipe.Advance(length); + SendPipe.FlushAsync().GetAwaiter().GetResult(); + + p.OnSend(); + } + catch (SocketException ex) + { + Console.WriteLine(ex); + TraceException(ex); + Dispose(); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } } public bool CheckEncrypted(int packetID) @@ -470,121 +499,31 @@ namespace Server.Network return false; } - private Pipe m_RecvdPipe; - - private async Task Start(MessagePump pump) + private async Task ProcessRecvs(MessagePump pump) { - m_RecvdPipe = new Pipe(); - Running = true; - - await Task.WhenAll(ProcessSends(), ProcessRecvs(), HandlePackets(pump)).ConfigureAwait(false); - } - - private async Task ProcessRecvs() - { - PipeWriter w = m_RecvdPipe.Writer; - while (true) { - if (m_AsyncState.Paused) - { - await Timer.Pause(50).ConfigureAwait(false); - continue; - } - - try - { - Memory memory = w.GetMemory(); - int bytesRead = await Socket.ReceiveAsync(memory, SocketFlags.None).ConfigureAwait(false); - if (bytesRead == 0) - break; - - Interlocked.Exchange(ref m_NextCheckActivity, Core.TickCount + 90000); - - w.Advance(bytesRead); - - FlushResult result = await w.FlushAsync().ConfigureAwait(false); - if (result.IsCompleted || result.IsCanceled) - break; - } - catch - { - break; - } - } - - w.Complete(); - Dispose(); - } - - private async Task ProcessSends() - { - while (true) - try - { - Packet p = await m_SendQueue?.DequeueAsync(); - if (p == null) - break; - - ReadOnlyMemory buffer = p.Compile(CompressionEnabled, out int length); - - if (buffer.Length <= 0 || length <= 0) - { - p.OnSend(); - return; - } - - buffer = buffer.Slice(0, length); - - PacketSendProfile prof = null; - - if (Core.Profiling) - prof = PacketSendProfile.Acquire(p.GetType()); - - prof?.Start(); - - await Socket.SendAsync(buffer, SocketFlags.None).ConfigureAwait(false); - - p.OnSend(); - - prof?.Finish(length); - } - catch (SocketException ex) - { - Console.WriteLine(ex); - TraceException(ex); - Dispose(); - break; - } - catch (Exception ex) - { - Console.WriteLine(ex); - } - } - - private async Task HandlePackets(MessagePump pump) - { - PipeReader pr = m_RecvdPipe.Reader; - - while (true) - { - ReadResult result = await pr.ReadAsync(); + ReadResult result = await RecvPipe.ReadAsync(); ReadOnlySequence seq = result.Buffer; - if (seq.Length == 0) + + if (seq.IsEmpty) break; - long pos = PacketHandlers.ProcessPacket(pump, this, seq); + SetConnectionAlive(); + + int pos = PacketHandlers.ProcessPacket(pump, this, seq); if (pos <= 0) break; - pr.AdvanceTo(seq.GetPosition(pos, seq.Start)); + RecvPipe.AdvanceTo(seq.Slice(0, pos).End); if (result.IsCompleted || result.IsCanceled) break; } - pr.Complete(); + RecvPipe.Complete(); + Dispose(); } public PacketHandler GetHandler(int packetID) => @@ -595,7 +534,7 @@ namespace Server.Network public void CheckAlive(long curTicks) { - if (Socket == null || m_NextCheckActivity - curTicks >= 0) + if (Connection == null || m_NextCheckActivity - curTicks >= 0) return; Console.WriteLine("Client: {0}: Disconnecting due to inactivity...", this); @@ -637,21 +576,18 @@ namespace Server.Network if (disposing == 1) return; - Task.Run(async () => + try { - m_RecvdPipe.Reader.CancelPendingRead(); - m_RecvdPipe.Reader.Complete(); - await m_RecvdPipe.Writer.FlushAsync().ConfigureAwait(false); - m_RecvdPipe.Writer.Complete(); + Connection.Abort(); + Task.Run(Connection.DisposeAsync).Wait(); + } + catch (Exception ex) + { + TraceException(ex); + } - try { Socket.Shutdown(SocketShutdown.Both); } catch (Exception ex) { TraceException(ex); } - try { Socket.Close(); } catch (Exception ex) { TraceException(ex); } - - Socket = null; - m_RecvdPipe = null; - m_SendQueue = null; - m_Disposed.Enqueue(this); - }); + Connection = null; + m_Disposed.Enqueue(this); } public static void Initialize() diff --git a/Projects/Server/Network/PacketHandler.cs b/Projects/Server/Network/PacketHandler.cs index 022405fd5..3b87df425 100644 --- a/Projects/Server/Network/PacketHandler.cs +++ b/Projects/Server/Network/PacketHandler.cs @@ -28,7 +28,7 @@ namespace Server.Network public class PacketHandler { - public PacketHandler(int packetID, long length, bool ingame, OnPacketReceive onReceive) + public PacketHandler(int packetID, int length, bool ingame, OnPacketReceive onReceive) { PacketID = packetID; Length = length; @@ -38,7 +38,7 @@ namespace Server.Network public int PacketID{ get; } - public long Length{ get; } + public int Length{ get; } public OnPacketReceive OnReceive{ get; } diff --git a/Projects/Server/Network/PacketHandlers.cs b/Projects/Server/Network/PacketHandlers.cs index 37ec26672..beda676b3 100644 --- a/Projects/Server/Network/PacketHandlers.cs +++ b/Projects/Server/Network/PacketHandlers.cs @@ -109,43 +109,32 @@ namespace Server.Network Register(0x01, 5, false, Disconnect); Register(0x02, 7, true, MovementReq); Register(0x03, 0, true, AsciiSpeech); - Register(0x04, 2, true, GodModeRequest); Register(0x05, 5, true, AttackReq); Register(0x06, 5, true, UseReq); Register(0x07, 7, true, LiftReq); Register(0x08, 14, true, DropReq); Register(0x09, 5, true, LookReq); - Register(0x0A, 11, true, Edit); Register(0x12, 0, true, TextCommand); Register(0x13, 10, true, EquipReq); - Register(0x14, 6, true, ChangeZ); Register(0x22, 3, true, Resynchronize); Register(0x2C, 2, true, DeathStatusResponse); Register(0x34, 10, true, MobileQuery); Register(0x3A, 0, true, ChangeSkillLock); Register(0x3B, 0, true, VendorBuyReply); - Register(0x47, 11, true, NewTerrain); - Register(0x48, 73, true, NewAnimData); - Register(0x58, 106, true, NewRegion); Register(0x5D, 73, false, PlayCharacter); - Register(0x61, 9, true, DeleteStatic); Register(0x6C, 19, true, TargetResponse); Register(0x6F, 0, true, SecureTrade); Register(0x72, 5, true, SetWarMode); Register(0x73, 2, false, PingReq); Register(0x75, 35, true, RenameRequest); - Register(0x79, 9, true, ResourceQuery); - Register(0x7E, 2, true, GodviewQuery); Register(0x7D, 13, true, MenuResponse); Register(0x80, 62, false, AccountLogin); Register(0x83, 39, false, DeleteCharacter); Register(0x91, 65, false, GameLogin); Register(0x95, 9, true, HuePickerResponse); - Register(0x96, 0, true, GameCentralMoniter); Register(0x98, 0, true, MobileNameRequest); Register(0x9A, 0, true, AsciiPromptResponse); Register(0x9B, 258, true, HelpRequest); - Register(0x9D, 51, true, GMSingle); Register(0x9F, 0, true, VendorSellReply); Register(0xA0, 3, false, PlayServer); Register(0xA4, 149, false, SystemInfo); @@ -161,8 +150,6 @@ namespace Server.Network Register(0xBF, 0, true, ExtendedCommand); Register(0xC2, 0, true, UnicodePromptResponse); Register(0xC8, 2, true, SetUpdateRange); - Register(0xC9, 6, true, TripTime); - Register(0xCA, 6, true, UTripTime); Register(0xCF, 0, false, AccountLogin); Register(0xD0, 0, true, ConfigurationFile); Register(0xD1, 2, true, LogoutReq); @@ -172,6 +159,7 @@ namespace Server.Network Register(0xEF, 21, false, LoginServerSeed); Register(0xF4, 0, false, CrashReport); Register(0xF8, 106, false, CreateCharacter70160); + Register(0xFB, 2, false, ShowPublicHouseContent); Register6017(0x08, 15, true, DropReq6017); @@ -214,9 +202,7 @@ namespace Server.Network public static void Register(int packetID, int length, bool ingame, OnPacketReceive onReceive) { Handlers[packetID] = new PacketHandler(packetID, length, ingame, onReceive); - - if (m_6017Handlers[packetID] == null) - m_6017Handlers[packetID] = new PacketHandler(packetID, length, ingame, onReceive); + m_6017Handlers[packetID] ??= new PacketHandler(packetID, length, ingame, onReceive); } public static PacketHandler GetHandler(int packetID) => Handlers[packetID]; @@ -291,7 +277,9 @@ namespace Server.Network ph.ThrottleCallback = t; } - public static long ProcessPacket(MessagePump pump, NetState ns, in ReadOnlySequence seq) + private static MemoryPool _memoryPool = SlabMemoryPoolFactory.Create(); + + public static int ProcessPacket(MessagePump pump, NetState ns, in ReadOnlySequence seq) { PacketReader r = new PacketReader(seq); @@ -342,7 +330,7 @@ namespace Server.Network return -1; } - long packetLength = handler.Length; + int packetLength = handler.Length; if (handler.Length <= 0 && r.Length >= 3) { packetLength = r.ReadUInt16(); @@ -373,16 +361,12 @@ namespace Server.Network if (throttled > TimeSpan.Zero) ns.ThrottledUntil = DateTime.UtcNow + throttled; - PacketReceiveProfile prof = null; + ReadOnlySequence packet = seq.Slice(r.Position); + IMemoryOwner memOwner = _memoryPool.Rent((int)packet.Length); - if (Core.Profiling) - prof = PacketReceiveProfile.Acquire(packetId); + packet.CopyTo(memOwner.Memory.Span); - prof?.Start(); - - pump.QueueWork(ns, seq.Slice(r.Position), handler.OnReceive); - - prof?.Finish(packetLength); + pump.QueueWork(ns, memOwner, handler.OnReceive); return packetLength; } @@ -413,9 +397,9 @@ namespace Server.Network public static void EncodedCommand(NetState state, PacketReader pvSrc) { IEntity e = World.FindEntity(pvSrc.ReadUInt32()); - int packetID = pvSrc.ReadUInt16(); + int packetId = pvSrc.ReadUInt16(); - EncodedPacketHandler ph = GetEncodedHandler(packetID); + EncodedPacketHandler ph = GetEncodedHandler(packetId); if (ph != null) { @@ -423,7 +407,7 @@ namespace Server.Network { Console.WriteLine( "Client: {0}: Sent ingame packet (0xD7x{1:X2}) before having been attached to a mobile", state, - packetID); + packetId); state.Dispose(); } else if (ph.Ingame && state.Mobile.Deleted) @@ -609,39 +593,6 @@ namespace Server.Network EventSink.InvokeDeleteRequest(new DeleteRequestEventArgs(state, index)); } - public static void ResourceQuery(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - { - } - } - - public static void GameCentralMoniter(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - { - int type = pvSrc.ReadByte(); - int num1 = pvSrc.ReadInt32(); - - Console.WriteLine("God Client: {0}: Game central moniter", state); - Console.WriteLine(" - Type: {0}", type); - Console.WriteLine(" - Number: {0}", num1); - - pvSrc.Trace(state); - } - } - - public static void GodviewQuery(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) Console.WriteLine("God Client: {0}: Godview query 0x{1:X}", state, pvSrc.ReadByte()); - } - - public static void GMSingle(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - pvSrc.Trace(state); - } - public static void DeathStatusResponse(NetState state, PacketReader pvSrc) { // Ignored @@ -711,35 +662,7 @@ namespace Server.Network huePicker.OnResponse(hue); break; - } } - - public static void TripTime(NetState state, PacketReader pvSrc) - { - int unk1 = pvSrc.ReadByte(); - int unk2 = pvSrc.ReadInt32(); - - state.Send(new TripTimeResponse(unk1)); - } - - public static void UTripTime(NetState state, PacketReader pvSrc) - { - int unk1 = pvSrc.ReadByte(); - int unk2 = pvSrc.ReadInt32(); - - state.Send(new UTripTimeResponse(unk1)); - } - - public static void ChangeZ(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - { - int x = pvSrc.ReadInt16(); - int y = pvSrc.ReadInt16(); - int z = pvSrc.ReadSByte(); - - Console.WriteLine("God Client: {0}: Change Z ({1}, {2}, {3})", state, x, y, z); - } } public static void SystemInfo(NetState state, PacketReader pvSrc) @@ -758,101 +681,10 @@ namespace Server.Network int v8 = pvSrc.ReadInt32(); } - public static void Edit(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - { - int type = pvSrc.ReadByte(); // 10 = static, 7 = npc, 4 = dynamic - int x = pvSrc.ReadInt16(); - int y = pvSrc.ReadInt16(); - int id = pvSrc.ReadInt16(); - int z = pvSrc.ReadSByte(); - int hue = pvSrc.ReadUInt16(); - - Console.WriteLine("God Client: {0}: Edit {6} ({1}, {2}, {3}) 0x{4:X} (0x{5:X})", state, x, y, z, id, hue, - type); - } - } - - public static void DeleteStatic(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - { - int x = pvSrc.ReadInt16(); - int y = pvSrc.ReadInt16(); - int z = pvSrc.ReadInt16(); - int id = pvSrc.ReadUInt16(); - - Console.WriteLine("God Client: {0}: Delete Static ({1}, {2}, {3}) 0x{4:X}", state, x, y, z, id); - } - } - - public static void NewAnimData(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - { - Console.WriteLine("God Client: {0}: New tile animation", state); - - pvSrc.Trace(state); - } - } - - public static void NewTerrain(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - { - int x = pvSrc.ReadInt16(); - int y = pvSrc.ReadInt16(); - int id = pvSrc.ReadUInt16(); - int width = pvSrc.ReadInt16(); - int height = pvSrc.ReadInt16(); - - Console.WriteLine("God Client: {0}: New Terrain ({1}, {2})+({3}, {4}) 0x{5:X4}", state, x, y, width, height, - id); - } - } - - public static void NewRegion(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - { - string name = pvSrc.ReadString(40); - int unk = pvSrc.ReadInt32(); - int x = pvSrc.ReadInt16(); - int y = pvSrc.ReadInt16(); - int width = pvSrc.ReadInt16(); - int height = pvSrc.ReadInt16(); - int zStart = pvSrc.ReadInt16(); - int zEnd = pvSrc.ReadInt16(); - string desc = pvSrc.ReadString(40); - int soundFX = pvSrc.ReadInt16(); - int music = pvSrc.ReadInt16(); - int nightFX = pvSrc.ReadInt16(); - int dungeon = pvSrc.ReadByte(); - int light = pvSrc.ReadInt16(); - - Console.WriteLine("God Client: {0}: New Region '{1}' ('{2}')", state, name, desc); - } - } - public static void AccountID(NetState state, PacketReader pvSrc) { } - public static bool VerifyGC(NetState state) - { - if (state.Mobile == null || state.Mobile.AccessLevel <= AccessLevel.Counselor) - { - if (state.Running) - Console.WriteLine("Warning: {0}: Player using godclient, disconnecting", state); - - state.Dispose(); - return false; - } - - return true; - } - public static void TextCommand(NetState state, PacketReader pvSrc) { int type = pvSrc.ReadByte(); @@ -862,34 +694,6 @@ namespace Server.Network switch (type) { - case 0x00: // Go - { - if (VerifyGC(state)) - try - { - string[] split = command.Split(' '); - - int x = Utility.ToInt32(split[0]); - int y = Utility.ToInt32(split[1]); - - int z; - - if (split.Length >= 3) - z = Utility.ToInt32(split[2]); - else if (m.Map != null) - z = m.Map.GetAverageZ(x, y); - else - z = 0; - - m.Location = new Point3D(x, y, z); - } - catch - { - // ignored - } - - break; - } case 0xC7: // Animate { EventSink.InvokeAnimateRequest(new AnimateRequestEventArgs(m, command)); @@ -970,12 +774,6 @@ namespace Server.Network } } - public static void GodModeRequest(NetState state, PacketReader pvSrc) - { - if (VerifyGC(state)) - state.Send(new GodModeReply(pvSrc.ReadBoolean())); - } - public static void AsciiPromptResponse(NetState state, PacketReader pvSrc) { uint serial = pvSrc.ReadUInt32(); @@ -1550,9 +1348,7 @@ namespace Server.Network uint value = pvSrc.ReadUInt32(); if ((value & ~0x7FFFFFFF) != 0) - { from.OnPaperdollRequest(); - } else { Serial s = value; @@ -1576,9 +1372,7 @@ namespace Server.Network from.NextActionTime = Core.TickCount + Mobile.ActionDelay; } else - { from.SendActionMessage(); - } } public static void LookReq(NetState state, PacketReader pvSrc) @@ -1594,9 +1388,7 @@ namespace Server.Network if (m != null && from.CanSee(m) && Utility.InUpdateRange(from, m)) { if (SingleClickProps) - { m.OnAosSingleClick(from); - } else { if (from.Region.OnSingleClick(from, m)) @@ -1612,9 +1404,7 @@ namespace Server.Network Utility.InUpdateRange(from.Location, item.GetWorldLocation())) { if (SingleClickProps) - { item.OnAosSingleClick(from); - } else if (from.Region.OnSingleClick(from, item)) { if (item.Parent is Item item1) @@ -2062,13 +1852,6 @@ namespace Server.Network if (m != null) switch (type) { - case 0x00: // Unknown, sent by godclient - { - if (VerifyGC(state)) - Console.WriteLine("God Client: {0}: Query 0x{1:X2} on {2} '{3}'", state, type, m.Serial, m.Name); - - break; - } case 0x04: // Stats { m.OnStatsQuery(from); @@ -2094,44 +1877,10 @@ namespace Server.Network string name = pvSrc.ReadString(30); pvSrc.Seek(2, SeekOrigin.Current); + int flags = pvSrc.ReadInt32(); -/* if (FeatureProtection.DisabledFeatures != 0 && ThirdPartyAuthCallback != null) - { - bool authOK = false; - - ulong razorFeatures = ((ulong)pvSrc.ReadUInt32() << 32) | pvSrc.ReadUInt32(); - - if (razorFeatures == (ulong)FeatureProtection.DisabledFeatures) - { - bool doesNotMatch = false; - for (int i = 0; !doesNotMatch && i < m_ThirdPartyAuthKey.Length; i++) - doesNotMatch = pvSrc.ReadByte() != m_ThirdPartyAuthKey[i]; - - if (!doesNotMatch) - authOK = true; - } - else - { - pvSrc.Seek(16, SeekOrigin.Current); - } - - ThirdPartyAuthCallback(state, authOK); - }*/ -/* else - {*/ - pvSrc.Seek(24, SeekOrigin.Current); -/* }*/ - -/* if (ThirdPartyHackedCallback != null) - { - pvSrc.Seek(-2, SeekOrigin.Current); - if (pvSrc.ReadUInt16() == 0xDEAD) - ThirdPartyHackedCallback(state, true); - }*/ - - if (!state.Running) - return; + pvSrc.Seek(24, SeekOrigin.Current); int charSlot = pvSrc.ReadInt32(); int clientIP = pvSrc.ReadInt32(); @@ -2181,6 +1930,11 @@ namespace Server.Network } } + public static void ShowPublicHouseContent(NetState state, PacketReader pvSrc) + { + bool showPublicHouseContent = pvSrc.ReadBoolean(); + } + public static void DoLogin(NetState state, Mobile m) { state.Send(new LoginConfirm(m)); @@ -2614,9 +2368,7 @@ namespace Server.Network state.Send(new CharacterListOld(state.Account, state.CityInfo)); } else - { state.Dispose(); - } } public static void PlayServer(NetState state, PacketReader pvSrc) @@ -2626,9 +2378,7 @@ namespace Server.Network IAccount a = state.Account; if (info == null || a == null || index < 0 || index >= info.Length) - { state.Dispose(); - } else { ServerInfo si = info[index]; diff --git a/Projects/Server/Network/PacketReader.cs b/Projects/Server/Network/PacketReader.cs index de6159bb1..49625d649 100644 --- a/Projects/Server/Network/PacketReader.cs +++ b/Projects/Server/Network/PacketReader.cs @@ -175,13 +175,13 @@ namespace Server.Network return sb.ToString(); } - public bool IsSafeChar(int c) => c >= 0x20 && c < 0xFFFE; + private static bool IsSafeChar(int c) => c >= 0x20 && c < 0xFFFE; public string ReadUTF8StringSafe(int fixedLength) { string s; - if (m_Reader.TryReadTo(out ReadOnlySpan span, (byte)'\0', true)) + if (m_Reader.TryReadTo(out ReadOnlySpan span, (byte)'\0')) s = Utility.UTF8.GetString(span.Length > fixedLength ? span.Slice(0, fixedLength) : span); else { @@ -203,7 +203,7 @@ namespace Server.Network { string s; - if (m_Reader.TryReadTo(out ReadOnlySpan span, (byte)'\0', true)) + if (m_Reader.TryReadTo(out ReadOnlySpan span, (byte)'\0')) s = Utility.UTF8.GetString(span); else { @@ -222,7 +222,7 @@ namespace Server.Network public string ReadUTF8String() => Utility.UTF8.GetString( - m_Reader.TryReadTo(out ReadOnlySpan span, (byte)'\0', true) ? span : + m_Reader.TryReadTo(out ReadOnlySpan span, (byte)'\0') ? span : m_Reader.Sequence.Slice(m_Reader.Position, m_Reader.Remaining).ToArray() ); diff --git a/Projects/Server/Network/Packets.cs b/Projects/Server/Network/Packets.cs index 330f3d3fe..7cf9e74ef 100644 --- a/Projects/Server/Network/Packets.cs +++ b/Projects/Server/Network/Packets.cs @@ -4142,14 +4142,6 @@ namespace Server.Network } } - public sealed class GodModeReply : Packet - { - public GodModeReply(bool reply) : base(0x2B, 2) - { - m_Stream.Write(reply); - } - } - public sealed class PlayServerAck : Packet { internal static int m_AuthID = -1; diff --git a/Projects/Server/Network/SocketExtensions.cs b/Projects/Server/Network/SocketExtensions.cs index e03a79243..3fdf9d3b2 100644 --- a/Projects/Server/Network/SocketExtensions.cs +++ b/Projects/Server/Network/SocketExtensions.cs @@ -1,22 +1,22 @@ -/*************************************************************************** - * SocketExtensions.cs - * ------------------- - * begin : August 2, 2019 - * copyright : (C) The ModernUO Team - * email : hi@modernuo.com - * - * $Id$ - * - ***************************************************************************/ - -/*************************************************************************** - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - ***************************************************************************/ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SocketExtensions.cs - Created: 2019/08/02 - Updated: 2019/12/24 * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ using System; using System.Buffers; @@ -34,7 +34,7 @@ namespace Server.Network public static ArraySegment GetArray(this ReadOnlyMemory memory) { - if (MemoryMarshal.TryGetArray(memory, out var result)) + if (MemoryMarshal.TryGetArray(memory, out ArraySegment result)) return result; throw new InvalidOperationException("Buffer backed by array was expected"); @@ -42,7 +42,7 @@ namespace Server.Network public static ArraySegment GetArray(this ReadOnlySequence memory) { - if (SequenceMarshal.TryGetArray(memory, out var result)) + if (SequenceMarshal.TryGetArray(memory, out ArraySegment result)) return result; throw new InvalidOperationException("Buffer backed by array was expected"); diff --git a/Projects/Server/Network/StaticPacketHandlers.cs b/Projects/Server/Network/StaticPacketHandlers.cs index b2c710581..186de7840 100644 --- a/Projects/Server/Network/StaticPacketHandlers.cs +++ b/Projects/Server/Network/StaticPacketHandlers.cs @@ -1,22 +1,23 @@ -/*************************************************************************** - * StaticPacketHandlers.cs - * ------------------- - * begin : March 15, 2019 - * copyright : (C) The ModernUO Team - * email : hi@modernuo.com - * - * $Id$ - * - ***************************************************************************/ - -/*************************************************************************** - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - ***************************************************************************/ +/************************************************************************* + * ModernUO * + * Copyright (C) 2019 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: StaticPacketHandlers.cs * + * Created: 2019/03/15 - Updated: 2019/12/24 * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program. If not, see . * + *************************************************************************/ using System.Collections.Concurrent; diff --git a/Projects/Server/Pipelines/DuplexPipe.cs b/Projects/Server/Pipelines/DuplexPipe.cs new file mode 100644 index 000000000..089591cc9 --- /dev/null +++ b/Projects/Server/Pipelines/DuplexPipe.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace System.IO.Pipelines +{ + public class DuplexPipe : IDuplexPipe + { + public DuplexPipe(PipeReader reader, PipeWriter writer) + { + Input = reader; + Output = writer; + } + + public PipeReader Input { get; } + + public PipeWriter Output { get; } + + public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions) + { + var input = new Pipe(inputOptions); + var output = new Pipe(outputOptions); + + var transportToApplication = new DuplexPipe(output.Reader, input.Writer); + var applicationToTransport = new DuplexPipe(input.Reader, output.Writer); + + return new DuplexPipePair(applicationToTransport, transportToApplication); + } + + // This class exists to work around issues with value tuple on .NET Framework + public readonly struct DuplexPipePair + { + public IDuplexPipe Transport { get; } + public IDuplexPipe Application { get; } + + public DuplexPipePair(IDuplexPipe transport, IDuplexPipe application) + { + Transport = transport; + Application = application; + } + } + } +} diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index e7ce12400..94457e180 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -7,7 +7,7 @@ ModernUO - 0.0.1 + 0.1.2 Kamron Batman ModernUO ModernUO @@ -39,20 +39,38 @@ 8.0 - + + libuv.dll + Always + + + libuv.so + Always + + + libuv.so + libuv.so + Always + + zlib.dll - PreserveNewest + Always - + rdrand.dll - PreserveNewest + Always - + rdrand.so - PreserveNewest + Always + + + + + diff --git a/Projects/Server/SignalR/AsyncDisposableExtensions.cs b/Projects/Server/SignalR/AsyncDisposableExtensions.cs new file mode 100644 index 000000000..ac2ab7c2d --- /dev/null +++ b/Projects/Server/SignalR/AsyncDisposableExtensions.cs @@ -0,0 +1,19 @@ +// 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.Threading.Tasks; + +namespace SignalR +{ + public static class AsyncDisposableExtensions + { + // Does a light up check to see if a type is IAsyncDisposable and calls DisposeAsync if it is + public static ValueTask DisposeAsync(this IDisposable disposable) + { + if (disposable is IAsyncDisposable asyncDisposable) return asyncDisposable.DisposeAsync(); + disposable.Dispose(); + return default; + } + } +}