diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 22cf4d08df..692e4c3f89 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -90,6 +90,7 @@ jobs: dotnet test ./tests/Neo.Plugins.RpcServer.Tests --output ./bin/tests/Neo.Plugins.RpcServer.Tests dotnet test ./tests/Neo.Plugins.Storage.Tests --output ./bin/tests/Neo.Plugins.Storage.Tests dotnet test ./tests/Neo.Plugins.ApplicationLogs.Tests --output ./bin/tests/Neo.Plugins.ApplicationLogs.Tests + dotnet test ./tests/Neo.Plugins.NamedPipeService.Tests --output ./bin/tests/Neo.Plugins.NamedPipeService.Tests - name: Coveralls if: matrix.os == 'ubuntu-latest' @@ -109,6 +110,7 @@ jobs: ${{ github.workspace }}/tests/Neo.Plugins.Storage.Tests/TestResults/coverage.info ${{ github.workspace }}/tests/Neo.Plugins.ApplicationLogs.Tests/TestResults/coverage.info ${{ github.workspace }}/tests/Neo.Extensions.Tests/TestResults/coverage.info + ${{ github.workspace }}/tests/Neo.Plugins.NamedPipeService.Tests/TestResults/coverage.info PublishPackage: if: github.ref == 'refs/heads/master' && startsWith(github.repository, 'neo-project/') diff --git a/neo.sln b/neo.sln index 5e7f8c7dda..e05c76b469 100644 --- a/neo.sln +++ b/neo.sln @@ -80,7 +80,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RpcClient", "src\Plugins\Rp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Plugins.ApplicationLogs.Tests", "tests\Neo.Plugins.ApplicationLogs.Tests\Neo.Plugins.ApplicationLogs.Tests.csproj", "{8C866DC8-2E55-4399-9563-2F47FD4602EC}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Extensions.Tests", "tests\Neo.Extensions.Tests\Neo.Extensions.Tests.csproj", "{77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Neo.Extensions.Tests", "tests\Neo.Extensions.Tests\Neo.Extensions.Tests.csproj", "{77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NamedPipeService", "src\Plugins\NamedPipeService\NamedPipeService.csproj", "{DACD4CFA-3D24-4329-A1D3-D5EE9E268CE3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Neo.Plugins.NamedPipeService.Tests", "tests\Neo.Plugins.NamedPipeService.Tests\Neo.Plugins.NamedPipeService.Tests.csproj", "{38599EC3-D97C-408C-BD3B-E712831955D0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -228,6 +232,14 @@ Global {77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Debug|Any CPU.Build.0 = Debug|Any CPU {77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Release|Any CPU.ActiveCfg = Release|Any CPU {77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F}.Release|Any CPU.Build.0 = Release|Any CPU + {DACD4CFA-3D24-4329-A1D3-D5EE9E268CE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DACD4CFA-3D24-4329-A1D3-D5EE9E268CE3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DACD4CFA-3D24-4329-A1D3-D5EE9E268CE3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DACD4CFA-3D24-4329-A1D3-D5EE9E268CE3}.Release|Any CPU.Build.0 = Release|Any CPU + {38599EC3-D97C-408C-BD3B-E712831955D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38599EC3-D97C-408C-BD3B-E712831955D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38599EC3-D97C-408C-BD3B-E712831955D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38599EC3-D97C-408C-BD3B-E712831955D0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -269,6 +281,8 @@ Global {185ADAFC-BFC6-413D-BC2E-97F9FB0A8AF0} = {C2DC830A-327A-42A7-807D-295216D30DBB} {8C866DC8-2E55-4399-9563-2F47FD4602EC} = {7F257712-D033-47FF-B439-9D4320D06599} {77FDEE2E-9381-4BFC-B9E6-741EDBD6B90F} = {EDE05FA8-8E73-4924-BC63-DD117127EEE1} + {DACD4CFA-3D24-4329-A1D3-D5EE9E268CE3} = {C2DC830A-327A-42A7-807D-295216D30DBB} + {38599EC3-D97C-408C-BD3B-E712831955D0} = {7F257712-D033-47FF-B439-9D4320D06599} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BCBA19D9-F868-4C6D-8061-A2B91E06E3EC} diff --git a/src/Neo.Extensions/ObjectExtensions.cs b/src/Neo.Extensions/ObjectExtensions.cs new file mode 100644 index 0000000000..4663347f55 --- /dev/null +++ b/src/Neo.Extensions/ObjectExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// ObjectExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.Extensions +{ + public static class ObjectExtensions + { + public static TResult TryCatch(this TSource obj, Func func, TResult defaultOnError) + { + try + { + return func(obj); + } + catch + { + return defaultOnError; + } + } + } +} diff --git a/src/Neo.Extensions/TaskExtensions.cs b/src/Neo.Extensions/TaskExtensions.cs new file mode 100644 index 0000000000..2f6c1e26d4 --- /dev/null +++ b/src/Neo.Extensions/TaskExtensions.cs @@ -0,0 +1,83 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TaskExtensions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Threading.Tasks; + +namespace Neo.Extensions +{ + public static class TaskExtensions + { + private const int DefaultTimeoutSeconds = 10; + + public static ValueTask DefaultTimeout(this ValueTask valueTask) => + TimeoutAfter(valueTask, TimeSpan.FromSeconds(DefaultTimeoutSeconds)); + + public static ValueTask DefaultTimeout(this ValueTask valueTask) => + TimeoutAfter(valueTask, TimeSpan.FromSeconds(DefaultTimeoutSeconds)); + + public static Task DefaultTimeout(this Task task) => + TimeoutAfter(task, TimeSpan.FromSeconds(DefaultTimeoutSeconds)); + + public static Task DefaultTimeout(this Task task) + => TimeoutAfter(task, TimeSpan.FromSeconds(DefaultTimeoutSeconds)); + + public static async ValueTask TimeoutAfter(this ValueTask valueTask, TimeSpan timeout) + { +#if NET5_0_OR_GREATER + return await valueTask.AsTask().WaitAsync(timeout).ConfigureAwait(false); +#else + var task = valueTask.AsTask(); + if (task.Wait(timeout)) + return await task.ConfigureAwait(false); + else + throw new TimeoutException(); +#endif + } + + public static async ValueTask TimeoutAfter(this ValueTask valueTask, TimeSpan timeout) + { +#if NET5_0_OR_GREATER + await valueTask.AsTask().WaitAsync(timeout).ConfigureAwait(false); +#else + var task = valueTask.AsTask(); + if (task.Wait(timeout)) + await task.ConfigureAwait(false); + else + throw new TimeoutException(); +#endif + } + + public static async Task TimeoutAfter(this Task task, TimeSpan timeout) + { +#if NET5_0_OR_GREATER + return await task.WaitAsync(timeout).ConfigureAwait(false); +#else + if (task.Wait(timeout)) + return await task.ConfigureAwait(false); + else + throw new TimeoutException(); +#endif + } + + public static async Task TimeoutAfter(this Task task, TimeSpan timeout) + { +#if NET5_0_OR_GREATER + await task.WaitAsync(timeout).ConfigureAwait(false); +#else + if (task.Wait(timeout)) + await task.ConfigureAwait(false); + else + throw new TimeoutException(); +#endif + } + } +} diff --git a/src/Neo/Cryptography/Crc32.cs b/src/Neo/Cryptography/Crc32.cs new file mode 100644 index 0000000000..09464ef793 --- /dev/null +++ b/src/Neo/Cryptography/Crc32.cs @@ -0,0 +1,116 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Crc32.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections.Generic; +using System.Security.Cryptography; + +namespace Neo.Cryptography +{ + public sealed class Crc32 : HashAlgorithm + { + public static readonly uint DefaultPolynomial = 0xedb88320u; + public static readonly uint DefaultSeed = 0xffffffffu; + + private static uint[] s_defaultTable; + + public override int HashSize => 32; + + private readonly uint _seed; + private readonly uint[] _table; + private uint _hash; + + public Crc32() : this(DefaultPolynomial, DefaultSeed) + { + + } + + public Crc32( + uint polynomial, + uint seed) + { + if (BitConverter.IsLittleEndian) + throw new PlatformNotSupportedException("Not supported on Big Endian processors"); + + _table = InitializeTable(polynomial); + _seed = seed; + } + + public override void Initialize() + { + _hash = _seed; + } + + protected override void HashCore(byte[] array, int ibStart, int cbSize) + { + _hash = CalculateHash(_table, _hash, array, ibStart, cbSize); + } + + protected override byte[] HashFinal() + { + var hashBuffer = UInt32ToBigEndianBytes(~_hash); + HashValue = hashBuffer; + return hashBuffer; + } + + public static uint Compute(byte[] buffer) => + Compute(DefaultSeed, buffer); + + public static uint Compute(uint seed, byte[] buffer) => + Compute(DefaultPolynomial, seed, buffer); + + public static uint Compute(uint polynomial, uint seed, byte[] buffer) => + ~CalculateHash(InitializeTable(polynomial), seed, buffer, 0, buffer.Length); + + private static uint[] InitializeTable(uint polynomial) + { + if (polynomial == DefaultPolynomial && s_defaultTable != null) + return s_defaultTable; + + var createTable = new uint[256]; + for (var i = 0u; i < 256u; i++) + { + var entry = i; + for (var j = 0; j < 8; j++) + { + if ((entry & 1) == 1) + entry = (entry >> 1) ^ polynomial; + else + entry >>= 1; + } + createTable[i] = entry; + } + + if (polynomial == DefaultPolynomial) + s_defaultTable = createTable; + + return createTable; + } + + private static uint CalculateHash(uint[] table, uint seed, IList buffer, int start, int size) + { + var hash = seed; + for (var i = start; i < start + size; i++) + hash = (hash >> 8) ^ table[buffer[i] ^ hash & 0xff]; + return hash; + } + + private static byte[] UInt32ToBigEndianBytes(uint value) + { + var result = BitConverter.GetBytes(value); + + if (BitConverter.IsLittleEndian) + Array.Reverse(result); + + return result; + } + } +} diff --git a/src/Plugins/NamedPipeService/Buffers/MemoryPoolBlock.cs b/src/Plugins/NamedPipeService/Buffers/MemoryPoolBlock.cs new file mode 100644 index 0000000000..f31e079f7d --- /dev/null +++ b/src/Plugins/NamedPipeService/Buffers/MemoryPoolBlock.cs @@ -0,0 +1,44 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// MemoryPoolBlock.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Buffers; +using System.Runtime.InteropServices; + +namespace Neo.Plugins.Buffers +{ + internal sealed class MemoryPoolBlock : IMemoryOwner + { + public PinnedBlockMemoryPool Pool { get; } + + internal MemoryPoolBlock( + PinnedBlockMemoryPool pool, + int length) + { + Pool = pool; + + var pinnedArray = GC.AllocateUninitializedArray(length, pinned: true); + + Memory = MemoryMarshal.CreateFromPinnedArray(pinnedArray, 0, pinnedArray.Length); + } + + #region IMemoryOwner + + public Memory Memory { get; } + + void IDisposable.Dispose() + { + Pool.Return(this); + } + + #endregion + } +} diff --git a/src/Plugins/NamedPipeService/Buffers/PinnedBlockMemoryPool.cs b/src/Plugins/NamedPipeService/Buffers/PinnedBlockMemoryPool.cs new file mode 100644 index 0000000000..aba7b0283f --- /dev/null +++ b/src/Plugins/NamedPipeService/Buffers/PinnedBlockMemoryPool.cs @@ -0,0 +1,68 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PinnedBlockMemoryPool.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Buffers; +using System.Collections.Concurrent; + +namespace Neo.Plugins.Buffers +{ + internal sealed class PinnedBlockMemoryPool : MemoryPool + { + private const int AnySize = -1; + + private static readonly int s_blockSize = 4096; + + public static int BlockSize => s_blockSize; + + public override int MaxBufferSize { get; } = s_blockSize; + + private readonly ConcurrentQueue _blocks = new(); + private readonly object _disposedSync = new(); + + private bool _isDisposed; + + public override IMemoryOwner Rent(int size = AnySize) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(size, s_blockSize); + ObjectDisposedException.ThrowIf(_isDisposed, this); + + if (_blocks.TryDequeue(out var block)) + return block; + + return new MemoryPoolBlock(this, BlockSize); + } + + internal void Return(MemoryPoolBlock block) + { + if (_isDisposed == false) + _blocks.Enqueue(block); + } + + protected override void Dispose(bool disposing) + { + if (_isDisposed) + return; + + lock (_disposedSync) + { + _isDisposed = true; + + if (disposing) + { + while (_blocks.TryDequeue(out _)) + { + } + } + } + } + } +} diff --git a/src/Plugins/NamedPipeService/Buffers/Struffer.cs b/src/Plugins/NamedPipeService/Buffers/Struffer.cs new file mode 100644 index 0000000000..8103192e38 --- /dev/null +++ b/src/Plugins/NamedPipeService/Buffers/Struffer.cs @@ -0,0 +1,133 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// Struffer.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Neo.Plugins.Buffers +{ + internal sealed class Stuffer : IEnumerable + { + private static readonly UTF8Encoding s_utf8NoBom = new(false, true); + + private byte[] _data; + + public int Position { get; set; } + + public Stuffer() + { + _data = []; + Position = 0; + } + + public Stuffer(byte[] buffer) + { + _data = buffer; + } + + public Stuffer(int capacity) + { + _data = GC.AllocateUninitializedArray(capacity); + Position = 0; + } + + public static int SizeOf(string value) => + s_utf8NoBom.GetByteCount(value) + sizeof(int); + + public Stuffer Write(T value) + where T : unmanaged + { + var typeSize = Unsafe.SizeOf(); + + if (Position + typeSize > _data.Length) + Array.Resize(ref _data, _data.Length + typeSize); + + Unsafe.As(ref _data[Position]) = value; + + Position += typeSize; + return this; + } + + public Stuffer Write(T[] values) + where T : unmanaged + { + Write(values.Length); + foreach (var value in values) + Write(value); + return this; + } + + public Stuffer Write(string value) + { + var strByteCount = s_utf8NoBom.GetByteCount(value); + Write(strByteCount); + + if (Position + strByteCount > _data.Length) + Array.Resize(ref _data, _data.Length + strByteCount); + + Position += s_utf8NoBom.GetBytes(value, _data.AsSpan(Position)); + return this; + } + + public T Read() + where T : unmanaged + { + var typeSize = Unsafe.SizeOf(); + + if (Position + typeSize > _data.Length) + throw new IndexOutOfRangeException(); + + var value = Unsafe.As(ref _data[Position]); + Position += typeSize; + + return value; + } + + public T[] ReadArray() + where T : unmanaged + { + var length = Read(); + var values = new T[length]; + for (var i = 0; i < length; i++) + values[i] = Read(); + return values; + } + + public string ReadString() + { + var strByteCount = Read(); + + if (Position + strByteCount > _data.Length) + throw new IndexOutOfRangeException(); + + var value = s_utf8NoBom.GetString(_data, Position, strByteCount); + Position += strByteCount; + + return value; + } + + #region IEnumerable + + public IEnumerator GetEnumerator() + { + foreach (var b in _data) + yield return b; + } + + IEnumerator IEnumerable.GetEnumerator() => + GetEnumerator(); + + #endregion + } +} diff --git a/src/Plugins/NamedPipeService/Configuration/NamedPipeServerTransportOptions.cs b/src/Plugins/NamedPipeService/Configuration/NamedPipeServerTransportOptions.cs new file mode 100644 index 0000000000..11f2f31740 --- /dev/null +++ b/src/Plugins/NamedPipeService/Configuration/NamedPipeServerTransportOptions.cs @@ -0,0 +1,26 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeServerTransportOptions.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Plugins.Buffers; +using System; +using System.Buffers; + +namespace Neo.Plugins.Configuration +{ + internal class NamedPipeServerTransportOptions + { + public int ListenerQueueCount { get; set; } = Math.Min(Environment.ProcessorCount, 16); + public long MaxReadBufferSize { get; set; } = 1024 * 1024; + public long MaxWriteBufferSize { get; set; } = 64 * 1024; + public bool CurrentUserOnly { get; set; } = true; + internal Func> MemoryPoolFactory { get; set; } = () => new PinnedBlockMemoryPool(); + } +} diff --git a/src/Plugins/NamedPipeService/DuplexPipe.cs b/src/Plugins/NamedPipeService/DuplexPipe.cs new file mode 100644 index 0000000000..43684613e0 --- /dev/null +++ b/src/Plugins/NamedPipeService/DuplexPipe.cs @@ -0,0 +1,43 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// DuplexPipe.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.IO.Pipelines; + +namespace Neo.Plugins +{ + internal sealed class DuplexPipe( + PipeReader reader, + PipeWriter writer) : IDuplexPipe + { + public PipeReader Input { get; } = reader; + public PipeWriter Output { get; } = writer; + + public static DuplexPipePair CreateConnectionPair(PipeOptions inputOptions, PipeOptions outputOptions) + { + var input = new Pipe(inputOptions); + var output = new Pipe(outputOptions); + + // Use Transport for Input and Output for Application + var transportToApplication = new DuplexPipe(output.Reader, input.Writer); + var applicationToTransport = new DuplexPipe(input.Reader, output.Writer); + + return new(applicationToTransport, transportToApplication); + } + + public readonly struct DuplexPipePair( + IDuplexPipe transport, + IDuplexPipe application) + { + public IDuplexPipe Transport { get; } = transport; + public IDuplexPipe Application { get; } = application; + } + } +} diff --git a/src/Plugins/NamedPipeService/ITransport.cs b/src/Plugins/NamedPipeService/ITransport.cs new file mode 100644 index 0000000000..cef7c4c32b --- /dev/null +++ b/src/Plugins/NamedPipeService/ITransport.cs @@ -0,0 +1,23 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// ITransport.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.IO.Pipelines; +using System.Net; + +namespace Neo.Plugins +{ + internal interface ITransport : IAsyncDisposable + { + EndPoint LocalEndPoint { get; } + IDuplexPipe Transport { get; } + } +} diff --git a/src/Plugins/NamedPipeService/Models/IPipeMessage.cs b/src/Plugins/NamedPipeService/Models/IPipeMessage.cs new file mode 100644 index 0000000000..8a8b8d585b --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/IPipeMessage.cs @@ -0,0 +1,20 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// IPipeMessage.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +namespace Neo.Plugins.Models +{ + internal interface IPipeMessage + { + int Size { get; } + void FromArray(byte[] buffer); + byte[] ToArray(); + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeExceptionPayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeExceptionPayload.cs new file mode 100644 index 0000000000..1b81e0761a --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeExceptionPayload.cs @@ -0,0 +1,68 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeExceptionPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Plugins.Buffers; +using System; + +namespace Neo.Plugins.Models.Payloads +{ + internal sealed class PipeExceptionPayload : IPipeMessage + { + public string Type { get; set; } + + public string Message { get; set; } + + public string StackTrace { get; set; } + + public PipeExceptionPayload() + { + Type = nameof(PipeExceptionPayload); + Message = string.Empty; + StackTrace = string.Empty; + } + + public PipeExceptionPayload( + Exception exception) + { + Type = exception.GetType().Name; + Message = exception.Message; + StackTrace = exception.StackTrace ?? string.Empty; + } + + public bool IsEmpty => + string.IsNullOrEmpty(Message) && + string.IsNullOrEmpty(StackTrace); + public int Size => + Stuffer.SizeOf(Type) + + Stuffer.SizeOf(Message) + + Stuffer.SizeOf(StackTrace); + + public void FromArray(byte[] buffer) + { + var wrapper = new Stuffer(buffer); + + Type = wrapper.ReadString(); + Message = wrapper.ReadString(); + StackTrace = wrapper.ReadString(); + } + + public byte[] ToArray() + { + var wrapper = new Stuffer(Size); + + wrapper.Write(Type); + wrapper.Write(Message); + wrapper.Write(StackTrace); + + return [.. wrapper]; + } + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeNullPayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeNullPayload.cs new file mode 100644 index 0000000000..fddec7e0ca --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeNullPayload.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeNullPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System.IO; + +namespace Neo.Plugins.Models.Payloads +{ + internal sealed class PipeNullPayload : IPipeMessage + { + public int Size => 0; + + public void CopyFrom(Stream stream) { } + + public void FromArray(byte[] buffer) { } + + public void CopyTo(Stream stream) { } + + public void CopyTo(byte[] buffer) { } + + public byte[] ToArray() => []; + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeSerializablePayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeSerializablePayload.cs new file mode 100644 index 0000000000..5f4e8af6de --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeSerializablePayload.cs @@ -0,0 +1,52 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeSerializablePayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Extensions; +using Neo.IO; +using Neo.Plugins.Buffers; +using System.Linq; + +namespace Neo.Plugins.Models.Payloads +{ + internal class PipeSerializablePayload : IPipeMessage + where T : ISerializable, new() + { + public T? Value { get; set; } + + public int Size => + sizeof(int) + // Array length + Value?.Size ?? 0; // Block size in bytes + + public void FromArray(byte[] buffer) + { + var wrapper = new Stuffer(buffer); + + var blockBytes = wrapper.ReadArray(); + + Value = blockBytes.TryCatch(t => t.AsSerializable(), default); + } + + public byte[] ToArray() + { + try + { + var wrapper = new Stuffer(Size); + + wrapper.Write(Value?.ToArray() ?? []); + + return [.. wrapper]; + } + catch { } + + return [0, 0, 0, 0]; + } + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeUnmanagedPayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeUnmanagedPayload.cs new file mode 100644 index 0000000000..5bb9af3b6b --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeUnmanagedPayload.cs @@ -0,0 +1,41 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeUnmanagedPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Plugins.Buffers; +using System.Runtime.CompilerServices; + +namespace Neo.Plugins.Models.Payloads +{ + internal class PipeUnmanagedPayload : IPipeMessage + where T : unmanaged + { + public T Value { get; set; } + + public int Size => + Unsafe.SizeOf(); + + public void FromArray(byte[] buffer) + { + var wrapper = new Stuffer(buffer); + + Value = wrapper.Read(); + } + + public byte[] ToArray() + { + var wrapper = new Stuffer(Size); + + wrapper.Write(Value); + + return [.. wrapper]; + } + } +} diff --git a/src/Plugins/NamedPipeService/Models/PipeMessage.cs b/src/Plugins/NamedPipeService/Models/PipeMessage.cs new file mode 100644 index 0000000000..63c510a1b8 --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/PipeMessage.cs @@ -0,0 +1,124 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeMessage.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Cryptography; +using Neo.Plugins.Buffers; +using Neo.Plugins.Models.Payloads; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Reflection; + +namespace Neo.Plugins.Models +{ + internal sealed class PipeMessage : IPipeMessage + { + public const ulong Magic = 0x314547415353454dul; // MESSAGE1 + public const byte Version = 0x01; + + public static readonly IPipeMessage Null = new PipeNullPayload(); + + private static readonly ConcurrentDictionary s_commandTypes = new(); + + public int RequestId { get; private set; } + public PipeCommand Command { get; private set; } + + public IPipeMessage Payload { get; private set; } + + public PipeMessage() + { + Payload = new PipeNullPayload(); + Command = PipeCommand.Nack; + } + + static PipeMessage() + { + foreach (var pipeProtocolField in typeof(PipeCommand).GetFields(BindingFlags.Public | BindingFlags.Static)) + { + var attr = pipeProtocolField.GetCustomAttribute(); + if (attr is null) continue; + + _ = s_commandTypes.TryAdd((PipeCommand)pipeProtocolField.GetValue(null)!, attr.Type); + } + } + + public int Size => + sizeof(ulong) + // Magic + sizeof(byte) + // Version + sizeof(uint) + // CRC32 + sizeof(PipeCommand) + // Command + sizeof(int) + // RequestId + sizeof(int) + // Payload Size in bytes + Payload.Size; // Payload + + public static PipeMessage Create(int requestId, PipeCommand command, IPipeMessage payload) => + new() + { + RequestId = requestId, + Command = command, + Payload = payload, + }; + + public static PipeMessage Create(ReadOnlyMemory memory) + { + var message = new PipeMessage(); + message.FromArray(memory.ToArray()); + return message; + } + + public static IPipeMessage? CreateEmptyPayload(PipeCommand command) => + s_commandTypes.TryGetValue(command, out var t) + ? Activator.CreateInstance(t) as IPipeMessage + : null; + + public void FromArray(byte[] buffer) + { + var wrapper = new Stuffer(buffer); + + var magic = wrapper.Read(); + if (magic != Magic) + throw new FormatException($"Magic number is incorrect: {magic}"); + + var version = wrapper.Read(); + if (version != Version) + throw new FormatException($"Version number is incorrect: {version}"); + + var crc32 = wrapper.Read(); + RequestId = wrapper.Read(); + + var command = wrapper.Read(); + var payloadBytes = wrapper.ReadArray(); + + if (crc32 != Crc32.Compute(payloadBytes)) + throw new InvalidDataException("CRC32 mismatch"); + + Command = command; + Payload = CreateEmptyPayload(command) ?? throw new InvalidDataException($"Unknown command: {command}"); + Payload.FromArray(payloadBytes); + } + + public byte[] ToArray() + { + var wrapper = new Stuffer(Size); + + var payloadBytes = Payload.ToArray(); + + wrapper.Write(Magic); + wrapper.Write(Version); + wrapper.Write(Crc32.Compute(payloadBytes)); + wrapper.Write(RequestId); + wrapper.Write(Command); + wrapper.Write(payloadBytes); + + return [.. wrapper]; + } + } +} diff --git a/src/Plugins/NamedPipeService/NamedPipeEndPoint.cs b/src/Plugins/NamedPipeService/NamedPipeEndPoint.cs new file mode 100644 index 0000000000..a07a715684 --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeEndPoint.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeEndPoint.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net; + +namespace Neo.Plugins +{ + internal class NamedPipeEndPoint( + string pipeName, + string serverName) : EndPoint + { + internal const string LocalComputerServerName = "."; + + public string ServerName { get; } = serverName; + public string PipeName { get; set; } = pipeName; + + public NamedPipeEndPoint(string pipeName) : this(pipeName, LocalComputerServerName) { } + + public override string ToString() => + $@"\\{ServerName}\pipe\{PipeName}"; + + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is NamedPipeEndPoint other && + other.ServerName == ServerName && + other.PipeName == PipeName; + + public override int GetHashCode() => + HashCode.Combine(ServerName.GetHashCode(), PipeName.GetHashCode()); + } +} diff --git a/src/Plugins/NamedPipeService/NamedPipeServerConnection.cs b/src/Plugins/NamedPipeService/NamedPipeServerConnection.cs new file mode 100644 index 0000000000..a1ee8af911 --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeServerConnection.cs @@ -0,0 +1,355 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeServerConnection.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Plugins.Models; +using System; +using System.IO.Pipelines; +using System.IO.Pipes; +using System.Net; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using PipeOptions = System.IO.Pipelines.PipeOptions; + +namespace Neo.Plugins +{ + internal sealed class NamedPipeServerConnection + { + internal const int MinAllocBufferSize = 4096; + internal const int MaxMessageCapacity = 64; + + private readonly CancellationTokenSource _connectionClosedTokenSource = new(); + private readonly CancellationToken _connectionClosedToken = default; + + private readonly NamedPipeServerStream _serverStream; + private readonly Channel _messageQueue; + private readonly NamedPipeServerListener _listener; + private readonly NamedPipeEndPoint _endPoint; + + private readonly IDuplexPipe _originalTransport; + private readonly object _shutdownLock = new(); + + private Task _receivingTask = Task.CompletedTask; + private Task _sendingTask = Task.CompletedTask; + private Task _processMessageTask = Task.CompletedTask; + + private Exception? _shutdownReason; + + private bool _connectionClosed; + private bool _connectionShutdown; + private bool _streamDisconnected; + + internal PipeWriter Input => Application.Output; + + internal PipeReader Output => Application.Input; + + internal PipeWriter Writer => Transport.Output; + + internal PipeReader Reader => Transport.Input; + + internal IDuplexPipe Application { get; private set; } + + public IDuplexPipe Transport { get; private set; } + + public EndPoint LocalEndPoint => _endPoint; + + public Exception? ShutdownReason => _shutdownReason; + + public int MessageQueueCount => _messageQueue.Reader.Count; + + internal NamedPipeServerConnection( + NamedPipeServerListener listener, + NamedPipeEndPoint endPoint, + NamedPipeServerStream serverStream, + PipeOptions inputOptions, + PipeOptions outputOptions) + { + _listener = listener; + _endPoint = endPoint; + _serverStream = serverStream; + + _connectionClosedToken = _connectionClosedTokenSource.Token; + _messageQueue = Channel.CreateBounded( + new BoundedChannelOptions(MaxMessageCapacity) + { + SingleReader = true, + FullMode = BoundedChannelFullMode.Wait, + }); + + var pair = DuplexPipe.CreateConnectionPair(inputOptions, outputOptions); + + Transport = _originalTransport = pair.Transport; + Application = pair.Application; + } + + public async ValueTask DisposeAsync() + { + _originalTransport.Input.Complete(); + _originalTransport.Output.Complete(); + + try + { + await _receivingTask; + await _sendingTask; + await _processMessageTask; + } + catch (Exception ex) + { + Utility.Log(nameof(NamedPipeServerConnection), LogLevel.Error, ex.ToString()); + _serverStream.Dispose(); + } + + if (_streamDisconnected == false) + _serverStream.Dispose(); + else + _listener.ReturnStream(_serverStream); + } + + public void Abort(Exception abortReason) + { + Shutdown(abortReason); + + Output.CancelPendingRead(); + Reader.CancelPendingRead(); + } + + public async ValueTask ReadAsync(CancellationToken cancellationToken = default) + { + while (await _messageQueue.Reader.WaitToReadAsync(cancellationToken)) + { + if (_messageQueue.Reader.TryRead(out var message)) + return message; + } + + return null; + } + + internal void Start() + { + try + { + _receivingTask = DoReceiveAsync(); + _sendingTask = DoSendAsync(); + _processMessageTask = ProcessMessagesAsync(); + } + catch (Exception ex) + { + Utility.Log(nameof(NamedPipeServerConnection), LogLevel.Error, ex.ToString()); + } + } + + private async Task DoReceiveAsync() + { + Exception? error = null; + + try + { + var input = Input; + + while (true) + { + var buffer = input.GetMemory(MinAllocBufferSize); + var bytesReceived = await _serverStream.ReadAsync(buffer); + + if (bytesReceived == 0) + break; + + input.Advance(bytesReceived); + + var result = await input.FlushAsync(); + + if (result.IsCompleted || result.IsCanceled) + break; + } + } + catch (Exception ex) + { + error = ex; + } + finally + { + Input.Complete(_shutdownReason ?? error); + FireConnectionClosed(); + } + } + + private async Task DoSendAsync() + { + Exception? shutdownReason = null; + Exception? unexpectedError = null; + + try + { + while (true) + { + var result = await Output.ReadAsync(); + + if (result.IsCanceled) + break; + + var buffer = result.Buffer; + if (buffer.IsSingleSegment) + await _serverStream.WriteAsync(buffer.First); + else + { + foreach (var segment in buffer) + await _serverStream.WriteAsync(segment); + } + + Output.AdvanceTo(buffer.End); + + if (result.IsCompleted) + break; + } + } + catch (ObjectDisposedException ex) + { + shutdownReason = ex; + } + catch (Exception ex) + { + shutdownReason = ex; + unexpectedError = ex; + } + finally + { + Shutdown(shutdownReason); + + Output.Complete(unexpectedError); + Input.CancelPendingFlush(); + } + } + + private async Task ProcessMessagesAsync() + { + Exception? unexpectedError = null; + + try + { + while (true) + { + var result = await Reader.ReadAsync(); + + if (result.IsCanceled) + break; + + var buffer = result.Buffer; + if (buffer.IsSingleSegment) + await QueueMessageAsync(buffer.First); + else + { + foreach (var segment in buffer) + await QueueMessageAsync(segment); + } + + Reader.AdvanceTo(buffer.End); + + if (result.IsCompleted) + break; + } + } + catch (Exception ex) + { + unexpectedError = ex; + + Utility.Log(nameof(NamedPipeServerConnection), LogLevel.Error, ex.ToString()); + } + finally + { + Shutdown(unexpectedError); + + Reader.Complete(unexpectedError); + Output.CancelPendingRead(); + + _messageQueue.Writer.Complete(unexpectedError); + } + } + + private async Task QueueMessageAsync(ReadOnlyMemory buffer) + { + try + { + if (buffer.IsEmpty) + return; + + var message = PipeMessage.Create(buffer); + + if (message is null) + return; + + if (_messageQueue.Writer.TryWrite(message) == false) + { + if (await _messageQueue.Writer.WaitToWriteAsync(_connectionClosedToken) == false) + throw new InvalidOperationException("Message queue writer was unexpectedly closed."); + } + } + catch (IndexOutOfRangeException) // NULL message or Empty message + { + + } + catch (FormatException) // Normally invalid or corrupt message + { + + } + catch (Exception ex) + { + Utility.Log(nameof(NamedPipeServerConnection), LogLevel.Error, ex.ToString()); + } + } + + private void Shutdown(Exception? shutdownReason) + { + lock (_shutdownLock) + { + if (_connectionShutdown) + return; + + _connectionShutdown = true; + + _shutdownReason = shutdownReason; + + try + { + _serverStream.Disconnect(); + _streamDisconnected = true; + } + catch + { + } + } + } + + private void FireConnectionClosed() + { + lock (_shutdownLock) + { + if (_connectionClosed) + return; + + _connectionClosed = true; + } + + CancelConnectionClosedToken(); + } + + private void CancelConnectionClosedToken() + { + try + { + _connectionClosedTokenSource.Cancel(); + } + catch (Exception ex) + { + Utility.Log(nameof(NamedPipeServerConnection), LogLevel.Error, ex.ToString()); + } + } + } +} diff --git a/src/Plugins/NamedPipeService/NamedPipeServerConnectionThread.cs b/src/Plugins/NamedPipeService/NamedPipeServerConnectionThread.cs new file mode 100644 index 0000000000..38475b4674 --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeServerConnectionThread.cs @@ -0,0 +1,93 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeServerConnectionThread.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Plugins.Models; +using Neo.Plugins.Models.Payloads; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.Plugins +{ + internal sealed class NamedPipeServerConnectionThread( + NeoSystem system, + NamedPipeServerConnection connection) : IThreadPoolWorkItem, IAsyncDisposable + { + private readonly NeoSystem _system = system; + private readonly NamedPipeServerConnection _connection = connection; + + private Exception? _shutdownException; + + public ValueTask DisposeAsync() + { + if (_shutdownException is not null) + _connection.Abort(_shutdownException); + return _connection.DisposeAsync(); + } + + public void Execute() + { + _ = ProcessRequests(); + } + + private async Task ProcessRequests() + { + try + { + PipeMessage? message; + + while ((message = await _connection.ReadAsync()) != null) + { + await OnRequestMessageAsync(message); + } + + } + catch (TimeoutException ex) + { + _shutdownException = ex; + Utility.Log(nameof(NamedPipeServicePlugin), LogLevel.Error, "Connection timed out while writing to the client."); + } + catch (Exception ex) + { + _shutdownException = ex; + Utility.Log(nameof(NamedPipeServicePlugin), LogLevel.Error, "Connection has stopped unexpectedly."); + } + finally + { + await DisposeAsync(); + } + } + + private async Task OnRequestMessageAsync(PipeMessage message) + { + var responseMessage = message.Command switch + { + _ => CreateErrorResponse(message.RequestId, new InvalidDataException()), + }; + + await WriteAsync(responseMessage); + } + + private async Task WriteAsync(PipeMessage message) + { + var memory = message.ToArray().AsMemory(); + + _ = await _connection.Writer.WriteAsync(memory); + } + + private PipeMessage CreateErrorResponse(int requestId, Exception exception) + { + var error = new PipeExceptionPayload(exception); + return PipeMessage.Create(requestId, PipeCommand.Exception, error); + } + } +} diff --git a/src/Plugins/NamedPipeService/NamedPipeServerListener.cs b/src/Plugins/NamedPipeService/NamedPipeServerListener.cs new file mode 100644 index 0000000000..89ba80dddc --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeServerListener.cs @@ -0,0 +1,178 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeServerListener.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.Extensions.ObjectPool; +using Neo.Plugins.Configuration; +using System; +using System.Buffers; +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using PipeOptions = System.IO.Pipelines.PipeOptions; + +namespace Neo.Plugins +{ + internal sealed class NamedPipeServerListener + { + private readonly NamedPipeServerTransportOptions _options; + private readonly Channel _acceptedQueue; + private readonly NamedPipeServerStreamPoolPolicy _poolPolicy; + private readonly ObjectPool _namedPipeServerStreamPool; + private readonly MemoryPool _memoryPool; + + private readonly CancellationTokenSource _listeningTokenSource = new(); + private readonly CancellationToken _listeningToken; + + private readonly Mutex _mutex; + + private readonly PipeOptions _inputOptions; + private readonly PipeOptions _outputOptions; + + private Task? _completeListeningTask; + private int _disposed; + + public NamedPipeEndPoint LocalEndPoint { get; } + + public NamedPipeServerListener( + NamedPipeEndPoint endPoint, + NamedPipeServerTransportOptions? options) + { + _mutex = new Mutex(false, $"NamedPipe-{endPoint.PipeName}", out var createdNew); + if (!createdNew) + { + _mutex.Dispose(); + throw new ApplicationException($"Named pipe '{endPoint.PipeName}' is already in use."); + } + + LocalEndPoint = endPoint; + _options = options ?? new(); + _poolPolicy = new NamedPipeServerStreamPoolPolicy(LocalEndPoint, _options); + _memoryPool = _options.MemoryPoolFactory(); + _listeningToken = _listeningTokenSource.Token; + + var objectPoolProvider = new DefaultObjectPoolProvider(); + _namedPipeServerStreamPool = objectPoolProvider.Create(_poolPolicy); + + _acceptedQueue = Channel.CreateBounded(capacity: 1); + + var maxReadBufferSize = _options.MaxReadBufferSize; + var maxWriteBufferSize = _options.MaxWriteBufferSize; + + _inputOptions = new PipeOptions(_memoryPool, PipeScheduler.ThreadPool, PipeScheduler.Inline, maxReadBufferSize, maxReadBufferSize / 2, useSynchronizationContext: false); + _outputOptions = new PipeOptions(_memoryPool, PipeScheduler.Inline, PipeScheduler.ThreadPool, maxWriteBufferSize, maxWriteBufferSize / 2, useSynchronizationContext: false); + } + + public async ValueTask DisposeAsync() + { + if (Interlocked.Exchange(ref _disposed, 1) == 0) + _listeningTokenSource.Cancel(); + + _listeningTokenSource.Dispose(); + _mutex.Dispose(); + + if (_completeListeningTask is not null) + await _completeListeningTask; + + (_namedPipeServerStreamPool as IDisposable)?.Dispose(); + } + + internal void ReturnStream(NamedPipeServerStream stream) + { + Debug.Assert(stream.IsConnected == false, "Stream should have been successfully disconnected to reach this point."); + + _namedPipeServerStreamPool.Return(stream); + } + + public void Start() + { + Debug.Assert(_completeListeningTask == null, "Already started"); + + var listeningTasks = new Task[_options.ListenerQueueCount]; + + for (var i = 0; i < listeningTasks.Length; i++) + { + var initialStream = _namedPipeServerStreamPool.Get(); + _poolPolicy.SetFirstPipeStarted(); + + listeningTasks[i] = Task.Run(() => StartAsync(initialStream)); + } + + _completeListeningTask = Task.Run(async () => + { + try + { + await Task.WhenAll(listeningTasks); + _acceptedQueue.Writer.TryComplete(); + } + catch (Exception ex) + { + Utility.Log(nameof(NamedPipeServerListener), LogLevel.Error, "Named pipe listener aborted."); + _acceptedQueue.Writer.TryComplete(ex); + } + }); + } + + public async ValueTask AcceptAsync(CancellationToken cancellationToken = default) + { + while (await _acceptedQueue.Reader.WaitToReadAsync(cancellationToken)) + { + if (_acceptedQueue.Reader.TryRead(out var connection)) + return connection; + } + + return null; + } + + public ValueTask UnbindAsync(CancellationToken cancellationToken = default) => + DisposeAsync(); + + private async Task StartAsync(NamedPipeServerStream nextStream) + { + while (true) + { + try + { + var stream = nextStream; + + await stream.WaitForConnectionAsync(_listeningToken); + + var connection = new NamedPipeServerConnection(this, LocalEndPoint, stream, _inputOptions, _outputOptions); + connection.Start(); + + nextStream = _namedPipeServerStreamPool.Get(); + + while (_acceptedQueue.Writer.TryWrite(connection) == false) + { + if (await _acceptedQueue.Writer.WaitToWriteAsync(_listeningToken) == false) + throw new InvalidOperationException("Accept queue writer was unexpectedly closed."); + } + } + catch (IOException) when (_listeningToken.IsCancellationRequested == false) + { + Utility.Log(nameof(NamedPipeServerListener), LogLevel.Error, "Named pipe listener received broken pipe while waiting for a connection."); + + nextStream.Dispose(); + nextStream = _namedPipeServerStreamPool.Get(); + } + catch (OperationCanceledException) when (_listeningToken.IsCancellationRequested) + { + break; + } + } + + nextStream.Dispose(); + } + } +} diff --git a/src/Plugins/NamedPipeService/NamedPipeServerStreamPoolPolicy.cs b/src/Plugins/NamedPipeService/NamedPipeServerStreamPoolPolicy.cs new file mode 100644 index 0000000000..c790d8fa7d --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeServerStreamPoolPolicy.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeServerStreamPoolPolicy.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.Extensions.ObjectPool; +using Neo.Plugins.Configuration; +using System.IO.Pipes; +using NamedPipeOptions = System.IO.Pipes.PipeOptions; + +namespace Neo.Plugins +{ + internal sealed class NamedPipeServerStreamPoolPolicy( + NamedPipeEndPoint endPoint, + NamedPipeServerTransportOptions options) : IPooledObjectPolicy + { + private readonly NamedPipeEndPoint _endPoint = endPoint; + private readonly NamedPipeServerTransportOptions _options = options; + private bool _hasFirstPipeStarted; + + public void SetFirstPipeStarted() => + _hasFirstPipeStarted = true; + + #region IPooledObjectPolicy + + NamedPipeServerStream IPooledObjectPolicy.Create() + { + var pipeOptions = NamedPipeOptions.Asynchronous | NamedPipeOptions.WriteThrough; + if (_hasFirstPipeStarted == false) + pipeOptions |= NamedPipeOptions.FirstPipeInstance; + if (_options.CurrentUserOnly) + pipeOptions |= NamedPipeOptions.CurrentUserOnly; + return new(_endPoint.PipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, pipeOptions, inBufferSize: 0, outBufferSize: 0); + } + + bool IPooledObjectPolicy.Return(NamedPipeServerStream obj) => + obj.IsConnected == false; + + #endregion + } +} diff --git a/src/Plugins/NamedPipeService/NamedPipeService.csproj b/src/Plugins/NamedPipeService/NamedPipeService.csproj new file mode 100644 index 0000000000..5ced12aee7 --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeService.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + Neo.Plugins.NamedPipeService + Neo.Plugins + enable + ../../../bin/$(PackageId) + + + + + + + + + PreserveNewest + + + + + + + diff --git a/src/Plugins/NamedPipeService/NamedPipeService.json b/src/Plugins/NamedPipeService/NamedPipeService.json new file mode 100644 index 0000000000..0b7812e4d1 --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeService.json @@ -0,0 +1,6 @@ +{ + "PluginConfiguration": { + "PipeName": "NeoNodeService", + "PipeCount": 4 + } +} diff --git a/src/Plugins/NamedPipeService/NamedPipeServicePlugin.cs b/src/Plugins/NamedPipeService/NamedPipeServicePlugin.cs new file mode 100644 index 0000000000..5eb81ba17f --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeServicePlugin.cs @@ -0,0 +1,135 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeServicePlugin.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; +using System.Threading; +using System.Threading.Tasks; +using static System.IO.Path; + +namespace Neo.Plugins +{ + public sealed class NamedPipeServicePlugin : Plugin + { + private readonly SemaphoreSlim _bindSemaphore = new(1); + private readonly CancellationTokenSource _stopTokenSource = new(); + private readonly TaskCompletionSource _stoppedCompletionSource = new(TaskCreationOptions.RunContinuationsAsynchronously); + + private NeoSystem? _system; + private NamedPipeServerListener? _listener; + private NamedPipeServiceSettings _settings = NamedPipeServiceSettings.Default; + + private bool _hasStarted; + private int _stopping; + + #region Overrides + + public override string ConfigFile => Combine(RootPath, "NamedPipeService.json"); + + public override string Name => "NamedPipeService"; + + public override string Description => "Allows communication with the node over NamedPipes"; + + protected override UnhandledExceptionPolicy ExceptionPolicy { get; init; } = UnhandledExceptionPolicy.Ignore; + + + #endregion + + public override void Dispose() + { + StopAsync(new CancellationToken(true)).GetAwaiter().GetResult(); + } + + protected override void Configure() + { + _settings = NamedPipeServiceSettings.Load(GetConfiguration()); + } + + protected override void OnSystemLoaded(NeoSystem system) + { + if (_hasStarted) + throw new InvalidOperationException($"{nameof(NamedPipeServicePlugin)} has already been started."); + + _hasStarted = true; + _system ??= system; + _listener ??= new(_settings.PipeName, _settings.TransportOptions); + + _listener.Start(); + + _ = ProcessClientsAsync(); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (Interlocked.Exchange(ref _stopping, 1) == 1) + { + await _stoppedCompletionSource.Task.ConfigureAwait(false); + return; + } + + _stopTokenSource.Cancel(); + +#pragma warning disable CA2016 // Don't use cancellationToken when acquiring the semaphore. Dispose calls this with a pre-canceled token. + await _bindSemaphore.WaitAsync().ConfigureAwait(false); +#pragma warning restore CA2016 + + try + { + await _listener!.UnbindAsync(new CancellationToken(true)).ConfigureAwait(false); + } + catch (Exception ex) + { + _stoppedCompletionSource.TrySetException(ex); + throw; + } + finally + { + _stopTokenSource.Dispose(); + _bindSemaphore.Release(); + } + + _stoppedCompletionSource.TrySetResult(); + } + + private async Task ProcessClientsAsync() + { + var stoppingToken = _stopTokenSource.Token; + await _bindSemaphore.WaitAsync().ConfigureAwait(false); + + try + { + if (_stopping == 1) + throw new InvalidOperationException($"{nameof(NamedPipeServicePlugin)} has already been stopped."); + + if (_listener is null) + throw new InvalidOperationException($"{nameof(NamedPipeServicePlugin)} has not been started."); + + while (true) + { + var connection = await _listener.AcceptAsync(stoppingToken).ConfigureAwait(false); + + if (stoppingToken.IsCancellationRequested) + break; + + if (connection is null) + continue; + + var threadPoolItem = new NamedPipeServerConnectionThread(_system, connection); + ThreadPool.UnsafeQueueUserWorkItem(threadPoolItem, preferLocal: false); + } + } + finally + { + + _bindSemaphore.Release(); + } + } + } +} diff --git a/src/Plugins/NamedPipeService/NamedPipeServiceSettings.cs b/src/Plugins/NamedPipeService/NamedPipeServiceSettings.cs new file mode 100644 index 0000000000..09496e9efd --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeServiceSettings.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeServiceSettings.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.Extensions.Configuration; +using Neo.Plugins.Configuration; +using System; + +namespace Neo.Plugins +{ + internal class NamedPipeServiceSettings + { + public NamedPipeEndPoint PipeName { get; private init; } + + public NamedPipeServerTransportOptions TransportOptions { get; private init; } + + public static NamedPipeServiceSettings Default { get; private set; } = new() + { + PipeName = new("NeoNodeService"), + TransportOptions = new(), + }; + + public NamedPipeServiceSettings() + { + PipeName = Default.PipeName; + TransportOptions = Default.TransportOptions; + } + + private NamedPipeServiceSettings(IConfigurationSection section) + { + PipeName = section.GetValue(nameof(PipeName), Default.PipeName)!; + TransportOptions = new() + { + ListenerQueueCount = Math.Min(section.GetValue("PipeCount", Environment.ProcessorCount), 16), + }; + } + + public static NamedPipeServiceSettings Load(IConfigurationSection section) => + new(section); + } +} diff --git a/src/Plugins/NamedPipeService/PipeCommand.cs b/src/Plugins/NamedPipeService/PipeCommand.cs new file mode 100644 index 0000000000..ac177b1a73 --- /dev/null +++ b/src/Plugins/NamedPipeService/PipeCommand.cs @@ -0,0 +1,24 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeCommand.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Plugins.Models.Payloads; + +namespace Neo.Plugins +{ + internal enum PipeCommand : byte + { + [PipeProtocol(typeof(PipeExceptionPayload))] + Exception = 0xe0, + + [PipeProtocol(typeof(PipeNullPayload))] + Nack = 0xf0, // NULL ACK + } +} diff --git a/src/Plugins/NamedPipeService/PipeProtocolAttribute.cs b/src/Plugins/NamedPipeService/PipeProtocolAttribute.cs new file mode 100644 index 0000000000..c18453ca26 --- /dev/null +++ b/src/Plugins/NamedPipeService/PipeProtocolAttribute.cs @@ -0,0 +1,22 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeProtocolAttribute.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using System; + +namespace Neo.Plugins +{ + [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)] + internal sealed class PipeProtocolAttribute + (Type type) : Attribute + { + public Type Type { get; } = type; + } +} diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/NamedPipeFactory.cs b/tests/Neo.Plugins.NamedPipeService.Tests/NamedPipeFactory.cs new file mode 100644 index 0000000000..0771186208 --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/NamedPipeFactory.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeFactory.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Neo.Plugins.Configuration; +using System; +using System.IO; +using System.IO.Pipes; +using System.Net; + +namespace Neo.Plugins.NamedPipeService.Tests +{ + internal static class NamedPipeFactory + { + public const string LocalComputerServerName = "."; + + public static NamedPipeEndPoint GetUniquePipeName() => + new(Path.GetRandomFileName()); + + public static bool CanBind(EndPoint endPoint) => + endPoint is NamedPipeEndPoint; + + public static NamedPipeClientStream CreateClientStream(NamedPipeEndPoint remoteEndPoint) => + new(remoteEndPoint.ServerName, remoteEndPoint.PipeName, PipeDirection.InOut, + PipeOptions.WriteThrough | PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + public static NamedPipeEndPoint CreateEndPoint(string pipeName) => + new(pipeName, LocalComputerServerName); + + public static NamedPipeServerListener CreateListener( + NamedPipeEndPoint endPoint, + NamedPipeServerTransportOptions? options = null) + { + if (endPoint.ServerName != LocalComputerServerName) + throw new NotSupportedException($"Server name '{endPoint.ServerName}' is invalid. The Server name must be \"{LocalComputerServerName}\"."); + + var listener = new NamedPipeServerListener(endPoint, options ?? new()); + + return listener; + } + } +} diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/Neo.Plugins.NamedPipeService.Tests.csproj b/tests/Neo.Plugins.NamedPipeService.Tests/Neo.Plugins.NamedPipeService.Tests.csproj new file mode 100644 index 0000000000..0ca13d3612 --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/Neo.Plugins.NamedPipeService.Tests.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + Neo.Plugins.NamedPipeService.Tests + + + + + + + + + + + + + diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeExceptionPayload.cs b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeExceptionPayload.cs new file mode 100644 index 0000000000..f118065ad4 --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeExceptionPayload.cs @@ -0,0 +1,66 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_PipeExceptionPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Plugins.Models.Payloads; +using System; + +namespace Neo.Plugins.NamedPipeService.Tests.Payloads +{ + [TestClass] + public class UT_PipeExceptionPayload + { + private static readonly string s_exceptionMessage = "Hello"; + private static readonly string s_exceptionStackTrace = "World"; + + [TestMethod] + public void IPipeMessage_FromArray_Data() + { + var exception1 = new PipeExceptionPayload() + { + Message = s_exceptionMessage, + StackTrace = s_exceptionStackTrace + }; + var expectedBytes = exception1.ToArray(); + + var exception2 = new PipeExceptionPayload(); + exception2.FromArray(expectedBytes); + + var actualBytes = exception2.ToArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(exception1.IsEmpty, exception2.IsEmpty); + Assert.AreEqual(exception1.Message, exception2.Message); + Assert.AreEqual(exception1.StackTrace, exception2.StackTrace); + } + + [TestMethod] + public void IPipeMessage_ToArray_Data() + { + var exception1 = new PipeExceptionPayload() + { + Message = s_exceptionMessage, + StackTrace = s_exceptionStackTrace + }; + var expectedBytes = exception1.ToArray(); + + var exception2 = new PipeExceptionPayload() + { + Message = s_exceptionMessage, + StackTrace = s_exceptionStackTrace + }; + var actualBytes = exception2.ToArray(); + var actualBytesWithoutHeader = actualBytes; + + CollectionAssert.AreEqual(expectedBytes, actualBytesWithoutHeader); + } + } +} diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeSerializablePayload.cs b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeSerializablePayload.cs new file mode 100644 index 0000000000..83f66129bc --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeSerializablePayload.cs @@ -0,0 +1,105 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_PipeSerializablePayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Network.P2P.Payloads; +using Neo.Plugins.Models.Payloads; +using System; + +namespace Neo.Plugins.NamedPipeService.Tests.Payloads +{ + [TestClass] + public class UT_PipeSerializablePayload + { + [TestMethod] + public void IPipeMessage_ToArray_Null() + { + var block1 = new PipeSerializablePayload() { Value = null }; + var expectedBytes = block1.ToArray(); + + var block2 = new PipeSerializablePayload() { Value = null }; + var actualBytes = block2.ToArray(); + var actualBytesWithoutHeader = actualBytes; + + CollectionAssert.AreEqual(expectedBytes, actualBytesWithoutHeader); + } + + [TestMethod] + public void IPipeMessage_FromArray_Null() + { + var block1 = new PipeSerializablePayload() { Value = null }; + var expectedBytes = block1.ToArray(); + + var block2 = new PipeSerializablePayload(); + block2.FromArray(expectedBytes); + + var actualBytes = block2.ToArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.IsNull(block2.Value); + } + + [TestMethod] + public void IPipeMessage_FromArray_Data() + { + var block1 = CreateEmptyPipeBlock(); + var expectedBytes = block1.ToArray(); + + var block2 = new PipeSerializablePayload(); + block2.FromArray(expectedBytes); + + var actualBytes = block2.ToArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(block1.Size, block2.Size); + Assert.AreEqual(block1.Value.Hash, block2.Value.Hash); + } + + [TestMethod] + public void IPipeMessage_ToArray_Data() + { + var block1 = CreateEmptyPipeBlock(); + var expectedBytes = block1.ToArray(); + + var block2 = CreateEmptyPipeBlock(); + var actualBytes = block2.ToArray(); + var actualBytesWithoutHeader = actualBytes; + + CollectionAssert.AreEqual(expectedBytes, actualBytesWithoutHeader); + } + + private static PipeSerializablePayload CreateEmptyPipeBlock() => + new() + { + Value = new Block() + { + Header = new Header() + { + Version = 0, + PrevHash = UInt256.Zero, + MerkleRoot = UInt256.Zero, + Timestamp = 0, + Index = 0, + Nonce = 0, + PrimaryIndex = 0, + NextConsensus = UInt160.Zero, + Witness = new Witness() + { + InvocationScript = Memory.Empty, + VerificationScript = Memory.Empty, + }, + }, + Transactions = [], + } + }; + + } +} diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeUnmanagedPayload.cs b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeUnmanagedPayload.cs new file mode 100644 index 0000000000..b862b486a9 --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeUnmanagedPayload.cs @@ -0,0 +1,40 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_PipeUnmanagedPayload.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Plugins.Models.Payloads; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Neo.Plugins.NamedPipeService.Tests.Payloads +{ + [TestClass] + public class UT_PipeUnmanagedPayload + { + [TestMethod] + public void IPipeMessage_FromArray_And_ToArray_Int32() + { + var expected = new PipeUnmanagedPayload() { Value = 1 }; + var expectedBytes = expected.ToArray(); + + var actual = new PipeUnmanagedPayload(); + actual.FromArray(expectedBytes); + + var actualBytes = actual.ToArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(1, actual.Value); + } + } +} diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/UT_NamedPipeConnectionListener.cs b/tests/Neo.Plugins.NamedPipeService.Tests/UT_NamedPipeConnectionListener.cs new file mode 100644 index 0000000000..436ae54c68 --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/UT_NamedPipeConnectionListener.cs @@ -0,0 +1,60 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UnitTest1.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using Microsoft.Testing.Platform.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Extensions; +using Neo.Plugins.Models; +using System.Threading.Tasks; + +namespace Neo.Plugins.NamedPipeService.Tests +{ + [TestClass] + public class UT_NamedPipeConnectionListener + { + private static readonly IPipeMessage s_testPipeMessage = PipeMessage.Create(1, PipeCommand.Nack, PipeMessage.Null); + + [TestMethod] + public async Task BidirectionalStream_ServerReadsDataAndCompletes_GracefullyClosed() + { + await using var connectionListener = NamedPipeFactory.CreateListener(NamedPipeFactory.GetUniquePipeName()); + var clientConnection = NamedPipeFactory.CreateClientStream(connectionListener.LocalEndPoint); + + // Server startup + connectionListener.Start(); + + // Client connecting + await clientConnection.ConnectAsync().DefaultTimeout(); + + // Server accepting stream + var serverConnectionTask = connectionListener.AcceptAsync(); + + // Client sending data + var bytes = s_testPipeMessage.ToArray(); + var writeTask = clientConnection.WriteAsync(bytes); + + var serverConnection = await serverConnectionTask.DefaultTimeout(); + await writeTask.DefaultTimeout(); + + // Server reading data + var readResult = await serverConnection.ReadAsync().DefaultTimeout(); + Assert.IsNotNull(readResult); + + clientConnection.Close(); + + var countResult = serverConnection.MessageQueueCount; + Assert.AreEqual(0, countResult); + + // Server disposing connection + await serverConnection.DisposeAsync(); + } + } +} diff --git a/tests/Neo.UnitTests/Cryptography/UT_Crc32.cs b/tests/Neo.UnitTests/Cryptography/UT_Crc32.cs new file mode 100644 index 0000000000..f72f31a5b8 --- /dev/null +++ b/tests/Neo.UnitTests/Cryptography/UT_Crc32.cs @@ -0,0 +1,48 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_Crc32.cs file belongs to the neo project and is free +// software distributed under the MIT software license, see the +// accompanying file LICENSE in the main directory of the +// repository or http://www.opensource.org/licenses/mit-license.php +// for more details. +// +// Redistribution and use in source and binary forms with or without +// modifications are permitted. + +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Cryptography; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Neo.UnitTests.Cryptography +{ + [TestClass] + public class UT_Crc32 + { + private const string SimpleString = @"The quick brown fox jumps over the lazy dog."; + private readonly byte[] _simpleBytesAscii = Encoding.ASCII.GetBytes(SimpleString); + + private const string SimpleString2 = @"Life moves pretty fast. If you don't stop and look around once in a while, you could miss it."; + private readonly byte[] _simpleBytes2Ascii = Encoding.ASCII.GetBytes(SimpleString2); + + [TestMethod] + public void StaticDefaultSeedAndPolynomialWithShortAsciiString() + { + var actual = Crc32.Compute(_simpleBytesAscii); + + actual.Should().Be(0x519025e9U); + } + + [TestMethod] + public void StaticDefaultSeedAndPolynomialWithShortAsciiString2() + { + var actual = Crc32.Compute(_simpleBytes2Ascii); + + actual.Should().Be(0x6ee3ad88U); + } + } +}