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..0903c26193 --- /dev/null +++ b/src/Plugins/NamedPipeService/Buffers/Struffer.cs @@ -0,0 +1,139 @@ +// 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(byte[] buffer, int position) + { + _data = buffer; + Position = position; + } + + 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..0b5f20509e --- /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 FromByteArray(byte[] buffer, int position = 0); + byte[] ToByteArray(); + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeArrayPayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeArrayPayload.cs new file mode 100644 index 0000000000..79b17b27b0 --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeArrayPayload.cs @@ -0,0 +1,56 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeArrayPayload.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.Linq; + +namespace Neo.Plugins.Models.Payloads +{ + internal class PipeArrayPayload : IPipeMessage + where T : IPipeMessage, new() + { + public T[] Value { get; set; } = []; + + public int Size => + sizeof(int) + // Item Count + Value.Sum(s => s.Size); // Size in bytes for item as ByteArray + + public void FromByteArray(byte[] buffer, int position = 0) + { + var wrapper = new Stuffer(buffer, position); + + var size = wrapper.Read(); + Value = new T[size]; + + var pos = wrapper.Position; + for (var i = 0; i < size; i++) + { + Value[i] = new T(); + Value[i].FromByteArray(buffer, pos); + pos += Value[i].Size; + } + + } + + public byte[] ToByteArray() + { + var wrapper = new Stuffer(); + wrapper.Write(Value.Length); + + byte[] bytes = [.. wrapper]; + + foreach (var item in Value) + bytes = [.. bytes, .. item.ToByteArray()]; + + return bytes; + } + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeContractState.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeContractState.cs new file mode 100644 index 0000000000..1ad73cf188 --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeContractState.cs @@ -0,0 +1,90 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeContractState.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 Neo.SmartContract; +using Neo.SmartContract.Manifest; + +namespace Neo.Plugins.Models.Payloads +{ + internal class PipeContractState : IPipeMessage + { + public int Id { get; set; } + + public ushort UpdateCounter { get; set; } + + public UInt160 Hash { get; set; } + + public NefFile? Nef { get; set; } + + public ContractManifest? Manifest { get; set; } + + private byte[]? manifestBytes => Manifest? + .ToJson() + .ToByteArray(false); + + public PipeContractState() + { + Id = 0; + UpdateCounter = 0; + Hash = new(); + } + + public PipeContractState(ContractState state) + { + Id = state.Id; + UpdateCounter = state.UpdateCounter; + Hash = state.Hash; + Nef = state.Nef; + Manifest = state.Manifest; + } + + public int Size => + sizeof(int) + // ID + sizeof(ushort) + // UpdateCounter + (sizeof(int) * 3) + // Array Buffers + UInt160.Length + // Hash + (Nef?.Size ?? 0) + // Script + (manifestBytes?.Length ?? 0); // Manifest + + public void FromByteArray(byte[] buffer, int position = 0) + { + var wrapper = new Stuffer(buffer, position); + + Id = wrapper.Read(); + UpdateCounter = wrapper.Read(); + + var hashBytes = wrapper.ReadArray(); + Hash = hashBytes.TryCatch(t => t.AsSerializable(), new()); + + var nefBytes = wrapper.TryCatch(t => t.ReadArray(), default); + Nef = nefBytes.TryCatch(t => t.AsSerializable(), default); + + var manifestBytes = wrapper.TryCatch(t => t.ReadArray(), default); + Manifest = manifestBytes.TryCatch(t => ContractManifest.Parse(t), default); + } + + public byte[] ToByteArray() + { + var wrapper = new Stuffer(Size); + + wrapper.Write(Id); + wrapper.Write(UpdateCounter); + wrapper.Write(Hash.ToArray()); + _ = wrapper.TryCatch(t => t.Write(Nef.ToArray()), default); + _ = wrapper.TryCatch(t => t.Write(manifestBytes!), default); + + return [.. wrapper]; + } + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeExceptionPayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeExceptionPayload.cs new file mode 100644 index 0000000000..85c2ae1e4f --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeExceptionPayload.cs @@ -0,0 +1,69 @@ +// 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 FromByteArray(byte[] buffer, int position = 0) + { + var wrapper = new Stuffer(buffer, position); + + Type = wrapper.ReadString(); + Message = wrapper.ReadString(); + StackTrace = wrapper.ReadString(); + } + + public byte[] ToByteArray() + { + 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..c23c193193 --- /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 FromByteArray(byte[] buffer, int position = 0) { } + + public void CopyTo(Stream stream) { } + + public void CopyTo(byte[] buffer) { } + + public byte[] ToByteArray() => []; + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeSerializablePayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeSerializablePayload.cs new file mode 100644 index 0000000000..2fedd14208 --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeSerializablePayload.cs @@ -0,0 +1,36 @@ +// 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 System.Linq; + +namespace Neo.Plugins.Models.Payloads +{ + internal class PipeSerializablePayload : IPipeMessage + where T : ISerializable, new() + { + public T? Value { get; set; } + + public int Size => + Value?.Size ?? 0; // Block size in bytes + + public void FromByteArray(byte[] buffer, int position = 0) + { + Value = buffer.TryCatch(t => t.AsSerializable(position), default); + } + + public byte[] ToByteArray() + { + return Value?.ToArray() ?? []; + } + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeShowStatePayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeShowStatePayload.cs new file mode 100644 index 0000000000..2fe1096f2d --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeShowStatePayload.cs @@ -0,0 +1,78 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeShowStatePayload.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.Network.P2P.Payloads; +using Neo.Plugins.Buffers; +using System.Net; +using System.Text; + +namespace Neo.Plugins.Models.Payloads +{ + internal class PipeShowStatePayload : IPipeMessage + { + public IPEndPoint? RemoteEndPoint { get; set; } + + public int ListenerTcpPort { get; set; } + + public int ConnectedCount { get; set; } + + public int UnconnectedCount { get; set; } + + public VersionPayload? Version { get; set; } + + public uint Height { get; set; } + + public uint HeaderHeight { get; set; } + + public uint LastBlockIndex { get; set; } + + public int Size => + sizeof(int) + + Encoding.UTF8.GetByteCount($"{RemoteEndPoint}") + + sizeof(int) + + (Version?.Size ?? 0); + + public void FromByteArray(byte[] buffer, int position = 0) + { + var wrapper = new Stuffer(buffer, position); + + RemoteEndPoint = wrapper.TryCatch(t => IPEndPoint.Parse(t.ReadString()), default); + ListenerTcpPort = wrapper.Read(); + ConnectedCount = wrapper.Read(); + UnconnectedCount = wrapper.Read(); + Height = wrapper.Read(); + HeaderHeight = wrapper.Read(); + LastBlockIndex = wrapper.Read(); + + var bytes = wrapper.TryCatch(t => t.ReadArray(), default); + Version = bytes.TryCatch(t => t.AsSerializable(), default); + } + + public byte[] ToByteArray() + { + var wrapper = new Stuffer(Size); + + wrapper.Write($"{RemoteEndPoint}"); + wrapper.Write(ListenerTcpPort); + wrapper.Write(ConnectedCount); + wrapper.Write(UnconnectedCount); + wrapper.Write(Height); + wrapper.Write(HeaderHeight); + wrapper.Write(LastBlockIndex); + + _ = wrapper.TryCatch(t => t.Write(Version.ToArray()), default); + + return [.. wrapper]; + } + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeStringPayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeStringPayload.cs new file mode 100644 index 0000000000..0af9835813 --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeStringPayload.cs @@ -0,0 +1,41 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// PipeStringPayload.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.Text; + +namespace Neo.Plugins.Models.Payloads +{ + internal class PipeStringPayload : IPipeMessage + { + public string Value { get; set; } = string.Empty; + + public int Size => + sizeof(int) + // Size in bytes + Encoding.UTF8.GetByteCount(Value); // size of the string + + public void FromByteArray(byte[] buffer, int position = 0) + { + var wrapper = new Stuffer(buffer, position); + + Value = wrapper.ReadString(); + } + + public byte[] ToByteArray() + { + var wrapper = new Stuffer(Size); + + wrapper.Write(Value); + + return [.. wrapper]; + } + } +} diff --git a/src/Plugins/NamedPipeService/Models/Payloads/PipeUnmanagedPayload.cs b/src/Plugins/NamedPipeService/Models/Payloads/PipeUnmanagedPayload.cs new file mode 100644 index 0000000000..e165bb2f28 --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/Payloads/PipeUnmanagedPayload.cs @@ -0,0 +1,48 @@ +// 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; +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 PipeUnmanagedPayload() + { + if (typeof(T) == typeof(char)) + throw new InvalidOperationException("You can't use 'System.Char'."); + } + + public void FromByteArray(byte[] buffer, int position = 0) + { + var wrapper = new Stuffer(buffer, position); + + Value = wrapper.Read(); + } + + public byte[] ToByteArray() + { + 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..5081250d82 --- /dev/null +++ b/src/Plugins/NamedPipeService/Models/PipeMessage.cs @@ -0,0 +1,138 @@ +// 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.Linq; +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(); + private static readonly byte[] s_commandValues; + + 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() + { + s_commandValues = (byte[])Enum.GetValuesAsUnderlyingType(); + + 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 static int HeaderSize => + sizeof(ulong) + // Magic + sizeof(byte) + // Version + sizeof(uint) + // CRC32 + sizeof(PipeCommand) + // Command + sizeof(int) + // RequestId + sizeof(int); // Payload Size in bytes + + public int Size => + HeaderSize + // Header Size + 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.FromByteArray(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 FromByteArray(byte[] buffer, int position = 0) + { + 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(); + + if (s_commandValues.Contains((byte)command) == false) + throw new FormatException($"Pipe command is incorrect: {command}"); + + 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.FromByteArray(payloadBytes); + + if (wrapper.Position != Size) + throw new FormatException($"Message size is incorrect: {wrapper.Position}"); + } + + public byte[] ToByteArray() + { + var wrapper = new Stuffer(Size); + + var payloadBytes = Payload.ToByteArray(); + + 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..182c136d5f --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeServerConnection.cs @@ -0,0 +1,344 @@ +// 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) + { + if (buffer.IsEmpty) + return; + + var message = PipeMessage.Create(0, PipeCommand.NAck, PipeMessage.Null); + + try + { + message = PipeMessage.Create(buffer); + } + finally + { + if (_messageQueue.Writer.TryWrite(message) == false) + { + if (await _messageQueue.Writer.WaitToWriteAsync(_connectionClosedToken) == false) + Utility.Log(nameof(NamedPipeServerConnection), LogLevel.Error, "Message queue writer was unexpectedly closed."); + } + } + } + + 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.Messages.cs b/src/Plugins/NamedPipeService/NamedPipeServerConnectionThread.Messages.cs new file mode 100644 index 0000000000..ff8cb32192 --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeServerConnectionThread.Messages.cs @@ -0,0 +1,121 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// NamedPipeServerConnectionThread.Messages.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.Network.P2P.Payloads; +using Neo.Plugins.Models; +using Neo.Plugins.Models.Payloads; +using Neo.SmartContract.Native; +using System.IO; +using System.Linq; + +namespace Neo.Plugins +{ + internal partial class NamedPipeServerConnectionThread + { + private PipeMessage OnBlockHeight(PipeMessage message) + { + if (message.Payload is not PipeNullPayload) + return CreateErrorResponse(message.RequestId, new InvalidDataException()); + + var blockHeight = NativeContract.Ledger.CurrentIndex(_system.StoreView); + var payload = new PipeUnmanagedPayload() { Value = blockHeight }; + + return PipeMessage.Create(message.RequestId, PipeCommand.BlockHeight, payload); + } + + private PipeMessage OnBlock(PipeMessage message) + { + if (message.Payload is not PipeUnmanagedPayload blockIndex) + return CreateErrorResponse(message.RequestId, new InvalidDataException()); + + var block = NativeContract.Ledger.GetBlock(_system.StoreView, blockIndex.Value); + var payload = new PipeSerializablePayload() { Value = block }; + + return PipeMessage.Create(message.RequestId, PipeCommand.Block, payload); + } + + private PipeMessage OnTransaction(PipeMessage message) + { + if (message.Payload is not PipeSerializablePayload txHash) + return CreateErrorResponse(message.RequestId, new InvalidDataException()); + + var tx = NativeContract.Ledger.GetTransaction(_system.StoreView, txHash.Value); + var payload = new PipeSerializablePayload() { Value = tx }; + + return PipeMessage.Create(message.RequestId, PipeCommand.Transaction, payload); + } + + private PipeMessage OnMemoryPoolUnVerified(PipeMessage message) + { + if (message.Payload is not PipeNullPayload) + return CreateErrorResponse(message.RequestId, new InvalidDataException()); + + _system.MemPool.GetVerifiedAndUnverifiedTransactions(out _, out var utx); + var payload = new PipeArrayPayload>() + { + Value = [.. utx.Select(s => new PipeSerializablePayload() { Value = s })], + }; + + return PipeMessage.Create(message.RequestId, PipeCommand.MemoryPoolUnVerified, payload); + } + + private PipeMessage OnMemoryPoolVerified(PipeMessage message) + { + if (message.Payload is not PipeNullPayload) + return CreateErrorResponse(message.RequestId, new InvalidDataException()); + + _system.MemPool.GetVerifiedAndUnverifiedTransactions(out var vtx, out _); + var payload = new PipeArrayPayload>() + { + Value = [.. vtx.Select(s => new PipeSerializablePayload() { Value = s })], + }; + + return PipeMessage.Create(message.RequestId, PipeCommand.MemoryPoolVerified, payload); + } + + private PipeMessage OnShowState(PipeMessage message) + { + if (message.Payload is not PipeNullPayload) + return CreateErrorResponse(message.RequestId, new InvalidDataException()); + + var height = NativeContract.Ledger.CurrentIndex(_system.StoreView); + var remoteAddresses = _localNode.GetRemoteNodes().Select(s => new PipeShowStatePayload() + { + RemoteEndPoint = s.Remote, + ListenerTcpPort = s.ListenerTcpPort, + ConnectedCount = _localNode.ConnectedCount, + UnconnectedCount = _localNode.ConnectedCount, + Height = height, + HeaderHeight = _system.HeaderCache.Last?.Index ?? height, + LastBlockIndex = s.LastBlockIndex, + Version = s.Version, + }); + + var payload = new PipeArrayPayload() + { + Value = [.. remoteAddresses], + }; + + return PipeMessage.Create(message.RequestId, PipeCommand.State, payload); + } + + private PipeMessage OnShowContractState(PipeMessage message) + { + if (message.Payload is not PipeSerializablePayload hashPayload) + return CreateErrorResponse(message.RequestId, new InvalidDataException()); + + var contractState = NativeContract.ContractManagement.GetContract(_system.StoreView, hashPayload.Value); + var payload = new PipeContractState(contractState); + + return PipeMessage.Create(message.RequestId, PipeCommand.ContractState, payload); + } + } +} diff --git a/src/Plugins/NamedPipeService/NamedPipeServerConnectionThread.cs b/src/Plugins/NamedPipeService/NamedPipeServerConnectionThread.cs new file mode 100644 index 0000000000..4742ff9894 --- /dev/null +++ b/src/Plugins/NamedPipeService/NamedPipeServerConnectionThread.cs @@ -0,0 +1,103 @@ +// 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 Akka.Actor; +using Neo.Network.P2P; +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 partial class NamedPipeServerConnectionThread( + NeoSystem system, + NamedPipeServerConnection connection) : IThreadPoolWorkItem, IAsyncDisposable + { + private readonly NeoSystem _system = system; + private readonly NamedPipeServerConnection _connection = connection; + private readonly LocalNode _localNode = system.LocalNode.Ask(new LocalNode.GetInstance()).GetAwaiter().GetResult(); + + 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 + { + PipeCommand.GetBlockHeight => OnBlockHeight(message), + PipeCommand.GetBlock => OnBlock(message), + PipeCommand.GetTransaction => OnTransaction(message), + PipeCommand.GetMemoryPoolUnVerified => OnMemoryPoolUnVerified(message), + PipeCommand.GetMemoryPoolVerified => OnMemoryPoolVerified(message), + PipeCommand.GetState => OnShowState(message), + PipeCommand.GetContractState => OnShowContractState(message), + _ => CreateErrorResponse(message.RequestId, new InvalidDataException()), + }; + + await WriteAsync(responseMessage); + } + + private async Task WriteAsync(PipeMessage message) + { + var memory = message.ToByteArray().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..7a56383d93 --- /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 (_system is null || _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..66edb0ad1d --- /dev/null +++ b/src/Plugins/NamedPipeService/PipeCommand.cs @@ -0,0 +1,67 @@ +// 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.Network.P2P.Payloads; +using Neo.Plugins.Models.Payloads; + +namespace Neo.Plugins +{ + internal enum PipeCommand : byte + { + [PipeProtocol(typeof(PipeNullPayload))] + GetBlockHeight = 0x00, + + [PipeProtocol(typeof(PipeUnmanagedPayload))] + BlockHeight = 0x01, + + [PipeProtocol(typeof(PipeUnmanagedPayload))] + GetBlock = 0x02, + + [PipeProtocol(typeof(PipeSerializablePayload))] + Block = 0x03, + + [PipeProtocol(typeof(PipeSerializablePayload))] + GetTransaction = 0x04, + + [PipeProtocol(typeof(PipeSerializablePayload))] + Transaction = 0x05, + + [PipeProtocol(typeof(PipeNullPayload))] + GetMemoryPoolUnVerified = 0x06, + + [PipeProtocol(typeof(PipeArrayPayload>))] + MemoryPoolUnVerified = 0x07, + + [PipeProtocol(typeof(PipeNullPayload))] + GetMemoryPoolVerified = 0x08, + + [PipeProtocol(typeof(PipeArrayPayload>))] + MemoryPoolVerified = 0x09, + + [PipeProtocol(typeof(PipeNullPayload))] + GetState = 0x10, + + [PipeProtocol(typeof(PipeArrayPayload))] + State = 0x11, + + [PipeProtocol(typeof(PipeSerializablePayload))] + GetContractState = 0x12, + + [PipeProtocol(typeof(PipeContractState))] + ContractState = 0x13, + + [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_PipeArrayPayload.cs b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeArrayPayload.cs new file mode 100644 index 0000000000..899f75549f --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeArrayPayload.cs @@ -0,0 +1,131 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_PipeArrayPayload.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_PipeArrayPayload + { + [TestMethod] + public void IPipeMessage_FromArray_Null() + { + var expectedPayload = new PipeArrayPayload>() + { + Value = [], + }; + var expectedBytes = expectedPayload.ToByteArray(); + + var actualPayload = new PipeArrayPayload>(); + actualPayload.FromByteArray(expectedBytes); + + var actualBytes = actualPayload.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(expectedPayload.Size, actualPayload.Size); + Assert.AreEqual(expectedPayload.Size, actualBytes.Length); + Assert.AreEqual(expectedPayload.Size, expectedBytes.Length); + } + + [TestMethod] + public void IPipeMessage_FromArray_Data() + { + var expectedPayload = new PipeArrayPayload>() + { + Value = [new() { Value = 1, }, new() { Value = 2, }], + }; + var expectedBytes = expectedPayload.ToByteArray(); + + var actualPayload = new PipeArrayPayload>(); + actualPayload.FromByteArray(expectedBytes); + + var actualBytes = actualPayload.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(expectedPayload.Size, actualPayload.Size); + Assert.AreEqual(expectedPayload.Size, actualBytes.Length); + Assert.AreEqual(expectedPayload.Size, expectedBytes.Length); + } + + [TestMethod] + public void IPipeMessage_ToArray_Data() + { + var expectedPayload = new PipeArrayPayload>() + { + Value = [new() { Value = 1, }, new() { Value = 2, }], + }; + var expectedBytes = expectedPayload.ToByteArray(); + + var actualPayload = new PipeArrayPayload>() + { + Value = [new() { Value = 1, }, new() { Value = 2, }], + }; + var actualBytes = actualPayload.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(expectedPayload.Size, actualPayload.Size); + Assert.AreEqual(expectedPayload.Size, actualBytes.Length); + Assert.AreEqual(expectedPayload.Size, expectedBytes.Length); + } + + [TestMethod] + public void IPipeMessage_FromArray_AsExceptionPayload() + { + var expectedPayload = new PipeArrayPayload() + { + Value = [new(new Exception("Hello")), new(new Exception("World"))], + }; + var expectedBytes = expectedPayload.ToByteArray(); + + var actualPayload = new PipeArrayPayload(); + actualPayload.FromByteArray(expectedBytes); + + var actualBytes = actualPayload.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(expectedPayload.Size, actualPayload.Size); + Assert.AreEqual(expectedPayload.Size, actualBytes.Length); + Assert.AreEqual(expectedPayload.Size, expectedBytes.Length); + Assert.AreEqual("Hello", expectedPayload.Value[0].Message); + Assert.AreEqual("World", expectedPayload.Value[1].Message); + } + + [TestMethod] + public void IPipeMessage_ToArray_AsExceptionPayload() + { + var expectedPayload = new PipeArrayPayload() + { + Value = [new(new Exception("Hello")), new(new Exception("World"))], + }; + var expectedBytes = expectedPayload.ToByteArray(); + + var actualPayload = new PipeArrayPayload() + { + Value = [new(new Exception("Hello")), new(new Exception("World"))], + }; + var actualBytes = actualPayload.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(expectedPayload.Size, actualPayload.Size); + Assert.AreEqual(expectedPayload.Size, actualBytes.Length); + Assert.AreEqual(expectedPayload.Size, expectedBytes.Length); + Assert.AreEqual(expectedPayload.Value[0].Message, actualPayload.Value[0].Message); + Assert.AreEqual(expectedPayload.Value[1].Message, actualPayload.Value[1].Message); + } + } +} diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeContractState.cs b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeContractState.cs new file mode 100644 index 0000000000..cc9d2f2057 --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeContractState.cs @@ -0,0 +1,113 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_PipeContractState.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_PipeContractState + { + [TestMethod] + public void IPipeMessage_FromArray_Null() + { + var expectedPayload = new PipeContractState(); + var expectedBytes = expectedPayload.ToByteArray(); + + var actualPayload = new PipeContractState(); + actualPayload.FromByteArray(expectedBytes); + + var actualBytes = actualPayload.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(expectedPayload.Size, actualPayload.Size); + Assert.AreEqual(expectedPayload.Size, actualBytes.Length); + Assert.AreEqual(expectedPayload.Size, expectedBytes.Length); + } + + [TestMethod] + public void IPipeMessage_FromArray_Data() + { + var expectedPayload = new PipeContractState() + { + Id = 1, + UpdateCounter = 99, + Hash = UInt160.Parse("0x00AA00AA00AA00AA00AA00FF00FF00FF00FF00FF"), + Nef = null, + Manifest = null, + }; + var expectedBytes = expectedPayload.ToByteArray(); + + var actualPayload = new PipeContractState(); + actualPayload.FromByteArray(expectedBytes); + + var actualBytes = actualPayload.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(expectedPayload.Size, actualPayload.Size); + Assert.AreEqual(expectedPayload.Size, actualBytes.Length); + Assert.AreEqual(expectedPayload.Size, expectedBytes.Length); + Assert.AreEqual(1, actualPayload.Id); + Assert.AreEqual(expectedPayload.Id, actualPayload.Id); + Assert.AreEqual(99u, actualPayload.UpdateCounter); + Assert.AreEqual(expectedPayload.UpdateCounter, actualPayload.UpdateCounter); + Assert.AreEqual(UInt160.Parse("0x00AA00AA00AA00AA00AA00FF00FF00FF00FF00FF"), actualPayload.Hash); + Assert.AreEqual(expectedPayload.Hash, actualPayload.Hash); + Assert.IsNull(actualPayload.Nef); + Assert.IsNull(actualPayload.Manifest); + } + + [TestMethod] + public void IPipeMessage_ToArray_Data() + { + var expectedPayload = new PipeContractState() + { + Id = 1, + UpdateCounter = 99, + Hash = UInt160.Parse("0x00AA00AA00AA00AA00AA00FF00FF00FF00FF00FF"), + Nef = null, + Manifest = null, + }; + var expectedBytes = expectedPayload.ToByteArray(); + + var actualPayload = new PipeContractState() + { + Id = 1, + UpdateCounter = 99, + Hash = UInt160.Parse("0x00AA00AA00AA00AA00AA00FF00FF00FF00FF00FF"), + Nef = null, + Manifest = null, + }; + var actualBytes = actualPayload.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(expectedPayload.Size, actualPayload.Size); + Assert.AreEqual(expectedPayload.Size, actualBytes.Length); + Assert.AreEqual(expectedPayload.Size, expectedBytes.Length); + Assert.AreEqual(1, expectedPayload.Id); + Assert.AreEqual(99u, expectedPayload.UpdateCounter); + Assert.AreEqual(UInt160.Parse("0x00AA00AA00AA00AA00AA00FF00FF00FF00FF00FF"), expectedPayload.Hash); + Assert.AreEqual(1, actualPayload.Id); + Assert.AreEqual(99u, actualPayload.UpdateCounter); + Assert.AreEqual(UInt160.Parse("0x00AA00AA00AA00AA00AA00FF00FF00FF00FF00FF"), actualPayload.Hash); + Assert.IsNull(expectedPayload.Nef); + Assert.IsNull(expectedPayload.Manifest); + Assert.IsNull(actualPayload.Nef); + Assert.IsNull(actualPayload.Manifest); + } + } +} 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..92685e7aba --- /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.ToByteArray(); + + var exception2 = new PipeExceptionPayload(); + exception2.FromByteArray(expectedBytes); + + var actualBytes = exception2.ToByteArray(); + + 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.ToByteArray(); + + var exception2 = new PipeExceptionPayload() + { + Message = s_exceptionMessage, + StackTrace = s_exceptionStackTrace + }; + var actualBytes = exception2.ToByteArray(); + 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..b95b8698ff --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeSerializablePayload.cs @@ -0,0 +1,119 @@ +// 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; +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.ToByteArray(); + + var block2 = new PipeSerializablePayload() { Value = null }; + var actualBytes = block2.ToByteArray(); + var actualBytesWithoutHeader = actualBytes; + + CollectionAssert.AreEqual(expectedBytes, actualBytesWithoutHeader); + } + + [TestMethod] + public void IPipeMessage_FromArray_Null() + { + var block1 = new PipeSerializablePayload() { Value = null }; + var expectedBytes = block1.ToByteArray(); + + var block2 = new PipeSerializablePayload(); + block2.FromByteArray(expectedBytes); + + var actualBytes = block2.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.IsNull(block2.Value); + } + + [TestMethod] + public void IPipeMessage_FromArray_Data() + { + var block1 = CreateEmptyPipeBlock(); + var expectedBytes = block1.ToByteArray(); + + var block2 = new PipeSerializablePayload(); + block2.FromByteArray(expectedBytes); + + var actualBytes = block2.ToByteArray(); + + 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.ToByteArray(); + + var block2 = CreateEmptyPipeBlock(); + var actualBytes = block2.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + } + + [TestMethod] + public void IPipeMessage_Position() + { + var expected = CreateEmptyPipeBlock(); + var expectedMessage = PipeMessage.Create(0, PipeCommand.Block, expected); + var expectedBytes = expectedMessage.ToByteArray(); + + var actual = new PipeSerializablePayload(); + actual.FromByteArray(expectedBytes, PipeMessage.HeaderSize); + + Assert.AreEqual(expected.Size, actual.Size); + Assert.AreEqual(expected.Value.Hash, actual.Value.Hash); + } + + 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_PipeShowStatePayload.cs b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeShowStatePayload.cs new file mode 100644 index 0000000000..3263fa550b --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeShowStatePayload.cs @@ -0,0 +1,130 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_PipeRemoteNodePayload.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.Capabilities; +using Neo.Network.P2P.Payloads; +using Neo.Plugins.Models.Payloads; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +namespace Neo.Plugins.NamedPipeService.Tests.Payloads +{ + [TestClass] + public class UT_PipeShowStatePayload + { + [TestMethod] + public void IPipeMessage_ToArray_Null() + { + var payload1 = new PipeShowStatePayload(); + var expectedBytes = payload1.ToByteArray(); + + var payload2 = new PipeShowStatePayload(); + var actualBytes = payload2.ToByteArray(); + var actualBytesWithoutHeader = actualBytes; + + CollectionAssert.AreEqual(expectedBytes, actualBytesWithoutHeader); + } + + [TestMethod] + public void IPipeMessage_FromArray_Null() + { + var payload1 = new PipeShowStatePayload(); + var expectedBytes = payload1.ToByteArray(); + + var payload2 = new PipeShowStatePayload(); + payload2.FromByteArray(expectedBytes); + + var actualBytes = payload2.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(payload1.RemoteEndPoint, payload2.RemoteEndPoint); + Assert.AreEqual(payload1.LastBlockIndex, payload2.LastBlockIndex); + Assert.AreEqual(payload1.Version, payload2.Version); + } + + [TestMethod] + public void IPipeMessage_ToArray_Data() + { + var payload1 = new PipeShowStatePayload() + { + RemoteEndPoint = new(IPAddress.Loopback, 0), + ListenerTcpPort = 999, + ConnectedCount = 1, + UnconnectedCount = 20, + Height = 777u, + HeaderHeight = 1000u, + LastBlockIndex = 666u, + Version = VersionPayload.Create(123u, 456u, "neo3", new NodeCapability[] { new ServerCapability(NodeCapabilityType.TcpServer, 22) }), + }; + var expectedBytes = payload1.ToByteArray(); + + var payload2 = new PipeShowStatePayload() + { + RemoteEndPoint = new(IPAddress.Loopback, 0), + ListenerTcpPort = 999, + ConnectedCount = 1, + UnconnectedCount = 20, + Height = 777u, + HeaderHeight = 1000u, + LastBlockIndex = 666u, + Version = VersionPayload.Create(123u, 456u, "neo3", new NodeCapability[] { new ServerCapability(NodeCapabilityType.TcpServer, 22) }), + }; + var actualBytes = payload2.ToByteArray(); + var actualBytesWithoutHeader = actualBytes; + + CollectionAssert.AreEqual(expectedBytes, actualBytesWithoutHeader); + } + + [TestMethod] + public void IPipeMessage_FromArray_Data() + { + var payload1 = new PipeShowStatePayload() + { + RemoteEndPoint = new(IPAddress.Loopback, 0), + ListenerTcpPort = 999, + ConnectedCount = 1, + UnconnectedCount = 20, + Height = 777u, + HeaderHeight = 1000u, + LastBlockIndex = 666u, + Version = VersionPayload.Create(123u, 456u, "neo3", new NodeCapability[] { new ServerCapability(NodeCapabilityType.TcpServer, 22) }), + }; + var expectedBytes = payload1.ToByteArray(); + + var payload2 = new PipeShowStatePayload(); + payload2.FromByteArray(expectedBytes); + + var actualBytes = payload2.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(payload1.RemoteEndPoint.Address, payload2.RemoteEndPoint.Address); + Assert.AreEqual(payload1.RemoteEndPoint.Port, payload2.RemoteEndPoint.Port); + Assert.AreEqual(999, payload2.ListenerTcpPort); + Assert.AreEqual(1, payload2.ConnectedCount); + Assert.AreEqual(20, payload2.UnconnectedCount); + Assert.AreEqual(777u, payload2.Height); + Assert.AreEqual(1000u, payload2.HeaderHeight); + Assert.AreEqual(666u, payload2.LastBlockIndex); + Assert.AreEqual(payload1.LastBlockIndex, payload2.LastBlockIndex); + Assert.AreEqual(payload1.Version.Version, payload2.Version.Version); + Assert.AreEqual(payload1.Version.UserAgent, payload2.Version.UserAgent); + Assert.AreEqual(payload1.Version.Nonce, payload2.Version.Nonce); + Assert.AreEqual(payload1.Version.Timestamp, payload2.Version.Timestamp); + Assert.IsInstanceOfType(payload1.Version.Capabilities[0]); + Assert.AreEqual(payload1.Version.Capabilities[0].Type, payload2.Version.Capabilities[0].Type); + } + } +} diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeStringPayload.cs b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeStringPayload.cs new file mode 100644 index 0000000000..5b9efa015e --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/Payloads/UT_PipeStringPayload.cs @@ -0,0 +1,81 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_PipeStringPayload.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_PipeStringPayload + { + [TestMethod] + public void IPipeMessage_ToArray_Null() + { + var payload1 = new PipeStringPayload() { Value = string.Empty }; + var expectedBytes = payload1.ToByteArray(); + + var payload2 = new PipeStringPayload() { Value = string.Empty }; + var actualBytes = payload2.ToByteArray(); + var actualBytesWithoutHeader = actualBytes; + + CollectionAssert.AreEqual(expectedBytes, actualBytesWithoutHeader); + } + + [TestMethod] + public void IPipeMessage_FromArray_Null() + { + var payload1 = new PipeStringPayload() { Value = string.Empty }; + var expectedBytes = payload1.ToByteArray(); + + var payload2 = new PipeStringPayload(); + payload2.FromByteArray(expectedBytes); + + var actualBytes = payload2.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(string.Empty, payload2.Value); + } + + [TestMethod] + public void IPipeMessage_ToArray_Data() + { + var payload1 = new PipeStringPayload() { Value = "漢字文化圈" }; + var expectedBytes = payload1.ToByteArray(); + + var payload2 = new PipeStringPayload() { Value = "漢字文化圈" }; + var actualBytes = payload2.ToByteArray(); + var actualBytesWithoutHeader = actualBytes; + + CollectionAssert.AreEqual(expectedBytes, actualBytesWithoutHeader); + } + + [TestMethod] + public void IPipeMessage_FromArray_Data() + { + var payload1 = new PipeStringPayload() { Value = "漢字文化圈" }; + var expectedBytes = payload1.ToByteArray(); + + var payload2 = new PipeStringPayload(); + payload2.FromByteArray(expectedBytes); + + var actualBytes = payload2.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual("漢字文化圈", payload2.Value); + } + } +} 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..1e6e8278f6 --- /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.ToByteArray(); + + var actual = new PipeUnmanagedPayload(); + actual.FromByteArray(expectedBytes); + + var actualBytes = actual.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(1, actual.Value); + } + } +} diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/TestDefaults.cs b/tests/Neo.Plugins.NamedPipeService.Tests/TestDefaults.cs new file mode 100644 index 0000000000..ab35a277f8 --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/TestDefaults.cs @@ -0,0 +1,80 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// TestDefaults.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.ECC; +using Neo.Persistence; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Neo.Plugins.NamedPipeService.Tests +{ + internal class TestDefaults + { + private static readonly MemoryStore Store = new(); + + public class StoreProvider : IStoreProvider + { + public string Name => "TestProvider"; + + public IStore GetStore(string path) => Store; + } + + public static readonly ProtocolSettings DefaultProtocolSettings = new() + { + Network = 0x334F454Eu, + AddressVersion = ProtocolSettings.Default.AddressVersion, + StandbyCommittee = + [ + //Validators + ECPoint.Parse("03b209fd4f53a7170ea4444e0cb0a6bb6a53c2bd016926989cf85f9b0fba17a70c", ECCurve.Secp256r1), + ECPoint.Parse("02df48f60e8f3e01c48ff40b9b7f1310d7a8b2a193188befe1c2e3df740e895093", ECCurve.Secp256r1), + ECPoint.Parse("03b8d9d5771d8f513aa0869b9cc8d50986403b78c6da36890638c3d46a5adce04a", ECCurve.Secp256r1), + ECPoint.Parse("02ca0e27697b9c248f6f16e085fd0061e26f44da85b58ee835c110caa5ec3ba554", ECCurve.Secp256r1), + ECPoint.Parse("024c7b7fb6c310fccf1ba33b082519d82964ea93868d676662d4a59ad548df0e7d", ECCurve.Secp256r1), + ECPoint.Parse("02aaec38470f6aad0042c6e877cfd8087d2676b0f516fddd362801b9bd3936399e", ECCurve.Secp256r1), + ECPoint.Parse("02486fd15702c4490a26703112a5cc1d0923fd697a33406bd5a1c00e0013b09a70", ECCurve.Secp256r1), + //Other Members + ECPoint.Parse("023a36c72844610b4d34d1968662424011bf783ca9d984efa19a20babf5582f3fe", ECCurve.Secp256r1), + ECPoint.Parse("03708b860c1de5d87f5b151a12c2a99feebd2e8b315ee8e7cf8aa19692a9e18379", ECCurve.Secp256r1), + ECPoint.Parse("03c6aa6e12638b36e88adc1ccdceac4db9929575c3e03576c617c49cce7114a050", ECCurve.Secp256r1), + ECPoint.Parse("03204223f8c86b8cd5c89ef12e4f0dbb314172e9241e30c9ef2293790793537cf0", ECCurve.Secp256r1), + ECPoint.Parse("02a62c915cf19c7f19a50ec217e79fac2439bbaad658493de0c7d8ffa92ab0aa62", ECCurve.Secp256r1), + ECPoint.Parse("03409f31f0d66bdc2f70a9730b66fe186658f84a8018204db01c106edc36553cd0", ECCurve.Secp256r1), + ECPoint.Parse("0288342b141c30dc8ffcde0204929bb46aed5756b41ef4a56778d15ada8f0c6654", ECCurve.Secp256r1), + ECPoint.Parse("020f2887f41474cfeb11fd262e982051c1541418137c02a0f4961af911045de639", ECCurve.Secp256r1), + ECPoint.Parse("0222038884bbd1d8ff109ed3bdef3542e768eef76c1247aea8bc8171f532928c30", ECCurve.Secp256r1), + ECPoint.Parse("03d281b42002647f0113f36c7b8efb30db66078dfaaa9ab3ff76d043a98d512fde", ECCurve.Secp256r1), + ECPoint.Parse("02504acbc1f4b3bdad1d86d6e1a08603771db135a73e61c9d565ae06a1938cd2ad", ECCurve.Secp256r1), + ECPoint.Parse("0226933336f1b75baa42d42b71d9091508b638046d19abd67f4e119bf64a7cfb4d", ECCurve.Secp256r1), + ECPoint.Parse("03cdcea66032b82f5c30450e381e5295cae85c5e6943af716cc6b646352a6067dc", ECCurve.Secp256r1), + ECPoint.Parse("02cd5a5547119e24feaa7c2a0f37b8c9366216bab7054de0065c9be42084003c8a", ECCurve.Secp256r1) + ], + ValidatorsCount = 7, + SeedList = + [ + "seed1.neo.org:10333", + "seed2.neo.org:10333", + "seed3.neo.org:10333", + "seed4.neo.org:10333", + "seed5.neo.org:10333" + ], + MillisecondsPerBlock = ProtocolSettings.Default.MillisecondsPerBlock, + MaxTransactionsPerBlock = ProtocolSettings.Default.MaxTransactionsPerBlock, + MemoryPoolMaxTransactions = ProtocolSettings.Default.MemoryPoolMaxTransactions, + MaxTraceableBlocks = ProtocolSettings.Default.MaxTraceableBlocks, + InitialGasDistribution = ProtocolSettings.Default.InitialGasDistribution, + Hardforks = ProtocolSettings.Default.Hardforks + }; + } +} 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..593d2e8cf7 --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/UT_NamedPipeConnectionListener.cs @@ -0,0 +1,58 @@ +// 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 + { + [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 = new byte[1]; + 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.Plugins.NamedPipeService.Tests/UT_NamedPipeServerConnectionThread.cs b/tests/Neo.Plugins.NamedPipeService.Tests/UT_NamedPipeServerConnectionThread.cs new file mode 100644 index 0000000000..054885d4f1 --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/UT_NamedPipeServerConnectionThread.cs @@ -0,0 +1,235 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_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 Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Extensions; +using Neo.Network.P2P.Payloads; +using Neo.Persistence; +using Neo.Plugins; +using Neo.Plugins.Models; +using Neo.Plugins.Models.Payloads; +using Neo.SmartContract.Manifest; +using System; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +namespace Neo.Plugins.NamedPipeService.Tests +{ + [TestClass] + public class UT_NamedPipeServerConnectionThread + { + private static readonly NeoSystem s_neoSystem = new NeoSystem(TestDefaults.DefaultProtocolSettings, new TestDefaults.StoreProvider()); + + private NamedPipeServerListener _connectionListener; + private NamedPipeClientStream _clientConnection; + + [TestInitialize] + public async Task InitTest() + { + _connectionListener = NamedPipeFactory.CreateListener(NamedPipeFactory.GetUniquePipeName()); + _clientConnection = NamedPipeFactory.CreateClientStream(_connectionListener.LocalEndPoint); + + // Server startup + _connectionListener.Start(); + + // Client connecting + await _clientConnection.ConnectAsync().DefaultTimeout(); + + // Server accepting stream + var serverConnectionTask = await _connectionListener.AcceptAsync().DefaultTimeout(); + + var threadPoolItem = new NamedPipeServerConnectionThread(s_neoSystem, serverConnectionTask); + + ThreadPool.UnsafeQueueUserWorkItem(threadPoolItem, preferLocal: false); + } + + [TestMethod] + public async Task Send_Random_Data() + { + var damagedMessage = new byte[PipeMessage.HeaderSize]; + Random.Shared.NextBytes(damagedMessage); + + var writeTask = _clientConnection.WriteAsync(damagedMessage); + + var buffer = new byte[1024]; + // No data should be returned + Assert.ThrowsExceptionAsync(async () => + await _clientConnection.ReadAsync(buffer).TimeoutAfter(TimeSpan.FromSeconds(1))); + } + + [TestMethod] + public async Task SendAndReceive_Messages_GetBlockHeight() + { + var rid = Random.Shared.Next(); + var getBlockHeightPayload = PipeMessage.Create(rid, PipeCommand.GetBlockHeight, PipeMessage.Null); + + var writeTask = _clientConnection.WriteAsync(getBlockHeightPayload.ToByteArray()); + + var buffer = new byte[1024]; + var count = await _clientConnection.ReadAsync(buffer).DefaultTimeout(); + + var message = PipeMessage.Create(buffer); + var payload = message.Payload as PipeUnmanagedPayload; + + Assert.AreNotEqual(0, message.Size); + Assert.AreEqual(PipeCommand.BlockHeight, message.Command); + Assert.AreEqual(rid, message.RequestId); + Assert.IsInstanceOfType>(message.Payload); + Assert.AreEqual(0u, payload.Value); + + } + + [TestMethod] + public async Task SendAndReceive_Messages_GetBlock() + { + var rid = Random.Shared.Next(); + + var getBlockPayload = PipeMessage.Create(rid, PipeCommand.GetBlock, new PipeUnmanagedPayload() + { + Value = 0, + }); + + var writeTask = _clientConnection.WriteAsync(getBlockPayload.ToByteArray()); + + var buffer = new byte[1024]; + var count = await _clientConnection.ReadAsync(buffer).DefaultTimeout(); + + var message = PipeMessage.Create(buffer); + var payload = message.Payload as PipeSerializablePayload; + + Assert.AreNotEqual(0, message.Size); + Assert.AreEqual(PipeCommand.Block, message.Command); + Assert.AreEqual(rid, message.RequestId); + Assert.IsInstanceOfType>(message.Payload); + Assert.AreEqual(0u, payload.Value.Index); + } + + [TestMethod] + public async Task SendAndReceive_Messages_GetTransaction() + { + var rid = Random.Shared.Next(); + + var getBlockPayload = PipeMessage.Create(rid, PipeCommand.GetTransaction, new PipeSerializablePayload() + { + Value = UInt256.Zero, + }); + + var writeTask = _clientConnection.WriteAsync(getBlockPayload.ToByteArray()); + + var buffer = new byte[1024]; + var count = await _clientConnection.ReadAsync(buffer).DefaultTimeout(); + + var message = PipeMessage.Create(buffer); + var payload = message.Payload as PipeSerializablePayload; + + Assert.AreNotEqual(0, message.Size); + Assert.AreEqual(PipeCommand.Transaction, message.Command); + Assert.AreEqual(rid, message.RequestId); + Assert.IsInstanceOfType>(message.Payload); + Assert.IsNull(payload.Value); + } + + [TestMethod] + public async Task SendAndReceive_Messages_GetMemoryPoolUnVerified() + { + var rid = Random.Shared.Next(); + + var getBlockPayload = PipeMessage.Create(rid, PipeCommand.GetMemoryPoolUnVerified, PipeMessage.Null); + + var writeTask = _clientConnection.WriteAsync(getBlockPayload.ToByteArray()); + + var buffer = new byte[1024]; + var count = await _clientConnection.ReadAsync(buffer).DefaultTimeout(); + + var message = PipeMessage.Create(buffer); + var memPool = message.Payload as PipeArrayPayload>; + + Assert.AreNotEqual(0, message.Size); + Assert.AreEqual(PipeCommand.MemoryPoolUnVerified, message.Command); + Assert.AreEqual(rid, message.RequestId); + Assert.IsInstanceOfType>>(message.Payload); + Assert.AreEqual(0, memPool.Value.Length); + } + + [TestMethod] + public async Task SendAndReceive_Messages_GetMemoryPoolVerified() + { + var rid = Random.Shared.Next(); + + var getBlockPayload = PipeMessage.Create(rid, PipeCommand.GetMemoryPoolVerified, PipeMessage.Null); + + var writeTask = _clientConnection.WriteAsync(getBlockPayload.ToByteArray()); + + var buffer = new byte[1024]; + var count = await _clientConnection.ReadAsync(buffer).DefaultTimeout(); + + var message = PipeMessage.Create(buffer); + var memPool = message.Payload as PipeArrayPayload>; + + Assert.AreNotEqual(0, message.Size); + Assert.AreEqual(PipeCommand.MemoryPoolVerified, message.Command); + Assert.AreEqual(rid, message.RequestId); + Assert.IsInstanceOfType>>(message.Payload); + Assert.AreEqual(0, memPool.Value.Length); + } + + [TestMethod] + public async Task SendAndReceive_Messages_GetState() + { + var rid = Random.Shared.Next(); + + var getBlockPayload = PipeMessage.Create(rid, PipeCommand.GetState, PipeMessage.Null); + + var writeTask = _clientConnection.WriteAsync(getBlockPayload.ToByteArray()); + + var buffer = new byte[1024]; + var count = await _clientConnection.ReadAsync(buffer).DefaultTimeout(); + + var message = PipeMessage.Create(buffer); + var remoteNodes = message.Payload as PipeArrayPayload; + + Assert.AreNotEqual(0, message.Size); + Assert.AreEqual(PipeCommand.State, message.Command); + Assert.AreEqual(rid, message.RequestId); + Assert.IsInstanceOfType>(message.Payload); + Assert.AreEqual(0, remoteNodes.Value.Length); + } + + [TestMethod] + public async Task SendAndReceive_Messages_GetContractState() + { + var rid = Random.Shared.Next(); + + var getBlockPayload = PipeMessage.Create(rid, PipeCommand.GetContractState, new PipeSerializablePayload() { Value = UInt160.Parse("0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5") }); + + var writeTask = _clientConnection.WriteAsync(getBlockPayload.ToByteArray()); + + var buffer = new byte[4096]; + var count = await _clientConnection.ReadAsync(buffer).DefaultTimeout(); + + var message = PipeMessage.Create(buffer); + var contractState = message.Payload as PipeContractState; + + Assert.AreNotEqual(0, message.Size); + Assert.AreEqual(PipeCommand.ContractState, message.Command); + Assert.AreEqual(rid, message.RequestId); + Assert.IsInstanceOfType(message.Payload); + Assert.AreEqual(UInt160.Parse("0xef4073a0f2b305a38ec4050e4d3d28bc40ea63f5"), contractState.Hash); + Assert.AreEqual(-5, contractState.Id); + Assert.AreEqual(0, contractState.UpdateCounter); + Assert.AreNotEqual(0, contractState.Nef.Size); + Assert.AreEqual("neo-core-v3.0", contractState.Nef.Compiler); + Assert.AreEqual(1325686241u, contractState.Nef.CheckSum); + Assert.IsInstanceOfType(contractState.Manifest); + } + } +} diff --git a/tests/Neo.Plugins.NamedPipeService.Tests/UT_PipeMessage.cs b/tests/Neo.Plugins.NamedPipeService.Tests/UT_PipeMessage.cs new file mode 100644 index 0000000000..725706192f --- /dev/null +++ b/tests/Neo.Plugins.NamedPipeService.Tests/UT_PipeMessage.cs @@ -0,0 +1,75 @@ +// Copyright (C) 2015-2024 The Neo Project. +// +// UT_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 Microsoft.VisualStudio.TestTools.UnitTesting; +using Neo.Plugins.Models; +using Neo.Plugins.Models.Payloads; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace Neo.Plugins.NamedPipeService.Tests +{ + [TestClass] + public class UT_PipeMessage + { + [TestMethod] + public void PipeMessage_Message_Sizes() + { + var message1 = PipeMessage.Create(1, PipeCommand.NAck, PipeMessage.Null); + var message2 = PipeMessage.Create(1, PipeCommand.Exception, PipeMessage.Null); + var message3 = PipeMessage.Create(1, PipeCommand.Exception, new PipeExceptionPayload() + { + Message = "Hello World", + StackTrace = "Program.cs
() line 99" + }); + + var msg1Bytes = message1.ToByteArray(); + var msg2Bytes = message2.ToByteArray(); + var msg3Bytes = message3.ToByteArray(); + + Assert.AreEqual(msg1Bytes.Length, message1.Size); + Assert.AreEqual(msg2Bytes.Length, message2.Size); + Assert.AreEqual(msg3Bytes.Length, message3.Size); + + Assert.AreEqual(PipeMessage.HeaderSize, msg1Bytes.Length - message1.Payload.Size); + Assert.AreEqual(PipeMessage.HeaderSize, msg2Bytes.Length - message2.Payload.Size); + Assert.AreEqual(PipeMessage.HeaderSize, msg3Bytes.Length - message3.Payload.Size); + } + + [TestMethod] + public void PipeMessage_FromArray_ToArray() + { + var expected = PipeMessage.Create(0, PipeCommand.NAck, PipeMessage.Null); + var expectedBytes = expected.ToByteArray(); + + var actual = PipeMessage.Create(expectedBytes); + var actualBytes = actual.ToByteArray(); + + CollectionAssert.AreEqual(expectedBytes, actualBytes); + Assert.AreEqual(expected.RequestId, actual.RequestId); + Assert.AreEqual(expected.Command, actual.Command); + Assert.IsInstanceOfType(expected.Payload); + } + + [TestMethod] + public void PipeMessage_FromArray_InvalidCommand() + { + var expected = PipeMessage.Create(0, (PipeCommand)0xff, PipeMessage.Null); + var expectedBytes = expected.ToByteArray(); + + Assert.ThrowsException(() => PipeMessage.Create(expectedBytes)); + } + } +} 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); + } + } +}