From 5164e0aff7ef406c32e42516ce4790920af2ab83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Wed, 25 Jan 2023 16:21:42 +0100 Subject: [PATCH] feat: add abstractions and core of communication --- NosSmooth.Comms.sln | 66 +++++++ .../NosSmooth.Comms.Abstractions/IClient.cs | 23 +++ .../IConnection.cs | 35 ++++ .../NosSmooth.Comms.Abstractions/IServer.cs | 40 ++++ .../Messages/CommandMessage.cs | 11 ++ .../Messages/HandshakeRequest.cs | 9 + .../Messages/HandshakeResponse.cs | 9 + .../Messages/MessageWrapper.cs | 9 + .../Messages/PacketMessage.cs | 12 ++ .../Messages/RawPacketMessage.cs | 11 ++ .../Messages/ResponseResult.cs | 11 ++ .../NosSmooth.Comms.Abstractions.csproj | 22 +++ .../Responders/IMessageResponder.cs | 24 +++ .../ClientNostaleClient.cs | 81 ++++++++ .../NosSmooth.Comms.Core/ConnectionHandler.cs | 184 ++++++++++++++++++ .../ConnectionInjector.cs | 25 +++ .../Extensions/ServiceCollectionExtensions.cs | 166 ++++++++++++++++ .../Formatters/NameStringFormatter.cs | 46 +++++ .../NosSmooth.Comms.Core/MessageHandler.cs | 112 +++++++++++ .../NosSmooth.Comms.Core.csproj | 33 ++++ .../NosSmoothMessageSerializerOptions.cs | 30 +++ .../NosSmooth.Comms.Core/NosSmoothResolver.cs | 31 +++ .../NostaleClientResolver.cs | 60 ++++++ .../Responders/ResponseResultResponder.cs | 34 ++++ .../NosSmooth.Comms.Core/ServerManager.cs | 159 +++++++++++++++ 25 files changed, 1243 insertions(+) create mode 100644 src/Core/NosSmooth.Comms.Abstractions/IClient.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/IConnection.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/IServer.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/Messages/CommandMessage.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeResponse.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/Messages/MessageWrapper.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/Messages/PacketMessage.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/Messages/RawPacketMessage.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/Messages/ResponseResult.cs create mode 100644 src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj create mode 100644 src/Core/NosSmooth.Comms.Abstractions/Responders/IMessageResponder.cs create mode 100644 src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs create mode 100644 src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs create mode 100644 src/Core/NosSmooth.Comms.Core/ConnectionInjector.cs create mode 100644 src/Core/NosSmooth.Comms.Core/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Core/NosSmooth.Comms.Core/Formatters/NameStringFormatter.cs create mode 100644 src/Core/NosSmooth.Comms.Core/MessageHandler.cs create mode 100644 src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj create mode 100644 src/Core/NosSmooth.Comms.Core/NosSmoothMessageSerializerOptions.cs create mode 100644 src/Core/NosSmooth.Comms.Core/NosSmoothResolver.cs create mode 100644 src/Core/NosSmooth.Comms.Core/NostaleClientResolver.cs create mode 100644 src/Core/NosSmooth.Comms.Core/Responders/ResponseResultResponder.cs create mode 100644 src/Core/NosSmooth.Comms.Core/ServerManager.cs diff --git a/NosSmooth.Comms.sln b/NosSmooth.Comms.sln index 85a1ac2..4083b34 100644 --- a/NosSmooth.Comms.sln +++ b/NosSmooth.Comms.sln @@ -1,8 +1,74 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Abstractions", "src\Core\NosSmooth.Comms.Abstractions\NosSmooth.Comms.Abstractions.csproj", "{D3B6C5ED-9291-4215-8F8A-F3530849B896}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Local", "src\Local\NosSmooth.Comms.Local\NosSmooth.Comms.Local.csproj", "{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.NamedPipes", "src\Core\NosSmooth.Comms.NamedPipes\NosSmooth.Comms.NamedPipes.csproj", "{9621D790-97FC-4E01-BB0C-CA1F33B4C934}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Injector", "src\Local\NosSmooth.Comms.Injector\NosSmooth.Comms.Injector.csproj", "{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{EFE65101-4414-4966-813D-90FE3736F6B1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Local", "Local", "{1AD3F38F-67A8-472D-8AF3-875C6F93EC16}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{711B1BE4-9AE3-4A9D-A5F2-3434D057870E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsolePacketLogger", "src\Samples\ConsolePacketLogger\ConsolePacketLogger.csproj", "{A154A209-B86F-4180-B329-9B51F5FCD99F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.LocalData", "src\Local\NosSmooth.Comms.LocalData\NosSmooth.Comms.LocalData.csproj", "{8749D2C0-6253-4A88-BA38-55B7C10D074B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Core", "src\Core\NosSmooth.Comms.Core\NosSmooth.Comms.Core.csproj", "{70275C91-1114-4673-8F9B-B0C311BFE337}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Tcp", "src\Core\NosSmooth.Comms.Tcp\NosSmooth.Comms.Tcp.csproj", "{04F43EA9-BC90-446F-8272-90705CAE9C27}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D3B6C5ED-9291-4215-8F8A-F3530849B896}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D3B6C5ED-9291-4215-8F8A-F3530849B896}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D3B6C5ED-9291-4215-8F8A-F3530849B896}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D3B6C5ED-9291-4215-8F8A-F3530849B896}.Release|Any CPU.Build.0 = Release|Any CPU + {7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}.Release|Any CPU.Build.0 = Release|Any CPU + {9621D790-97FC-4E01-BB0C-CA1F33B4C934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9621D790-97FC-4E01-BB0C-CA1F33B4C934}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9621D790-97FC-4E01-BB0C-CA1F33B4C934}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9621D790-97FC-4E01-BB0C-CA1F33B4C934}.Release|Any CPU.Build.0 = Release|Any CPU + {A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}.Release|Any CPU.Build.0 = Release|Any CPU + {A154A209-B86F-4180-B329-9B51F5FCD99F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A154A209-B86F-4180-B329-9B51F5FCD99F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A154A209-B86F-4180-B329-9B51F5FCD99F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A154A209-B86F-4180-B329-9B51F5FCD99F}.Release|Any CPU.Build.0 = Release|Any CPU + {8749D2C0-6253-4A88-BA38-55B7C10D074B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8749D2C0-6253-4A88-BA38-55B7C10D074B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8749D2C0-6253-4A88-BA38-55B7C10D074B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8749D2C0-6253-4A88-BA38-55B7C10D074B}.Release|Any CPU.Build.0 = Release|Any CPU + {70275C91-1114-4673-8F9B-B0C311BFE337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70275C91-1114-4673-8F9B-B0C311BFE337}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70275C91-1114-4673-8F9B-B0C311BFE337}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70275C91-1114-4673-8F9B-B0C311BFE337}.Release|Any CPU.Build.0 = Release|Any CPU + {04F43EA9-BC90-446F-8272-90705CAE9C27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04F43EA9-BC90-446F-8272-90705CAE9C27}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04F43EA9-BC90-446F-8272-90705CAE9C27}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04F43EA9-BC90-446F-8272-90705CAE9C27}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D3B6C5ED-9291-4215-8F8A-F3530849B896} = {EFE65101-4414-4966-813D-90FE3736F6B1} + {9621D790-97FC-4E01-BB0C-CA1F33B4C934} = {EFE65101-4414-4966-813D-90FE3736F6B1} + {A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4} = {1AD3F38F-67A8-472D-8AF3-875C6F93EC16} + {7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC} = {1AD3F38F-67A8-472D-8AF3-875C6F93EC16} + {A154A209-B86F-4180-B329-9B51F5FCD99F} = {711B1BE4-9AE3-4A9D-A5F2-3434D057870E} + {8749D2C0-6253-4A88-BA38-55B7C10D074B} = {1AD3F38F-67A8-472D-8AF3-875C6F93EC16} + {70275C91-1114-4673-8F9B-B0C311BFE337} = {EFE65101-4414-4966-813D-90FE3736F6B1} + {04F43EA9-BC90-446F-8272-90705CAE9C27} = {EFE65101-4414-4966-813D-90FE3736F6B1} + EndGlobalSection EndGlobal diff --git a/src/Core/NosSmooth.Comms.Abstractions/IClient.cs b/src/Core/NosSmooth.Comms.Abstractions/IClient.cs new file mode 100644 index 0000000..e5fe1b2 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/IClient.cs @@ -0,0 +1,23 @@ +// +// IClient.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Data; +using Remora.Results; + +namespace NosSmooth.Comms.Data; + +/// +/// An abstraction for a client connection. +/// +public interface IClient : IConnection +{ + /// + /// Connect to the server. + /// + /// The cancellation token used for cancelling the operation. + /// A result that may or may not have succeeded. + public Task ConnectAsync(CancellationToken ct = default); +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/IConnection.cs b/src/Core/NosSmooth.Comms.Abstractions/IConnection.cs new file mode 100644 index 0000000..3cb33e7 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/IConnection.cs @@ -0,0 +1,35 @@ +// +// IConnection.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Data; + +namespace NosSmooth.Comms.Data; + +/// +/// A connection, either a client or a server connection. +/// +public interface IConnection +{ + /// + /// Gets the state of the connection. + /// + public ConnectionState State { get; } + + /// + /// Gets the stream used for reading the data received. + /// + public Stream ReadStream { get; } + + /// + /// Gets the stream used for writing data. + /// + public Stream WriteStream { get; } + + /// + /// Disconnect, close the connection. + /// + public void Disconnect(); +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/IServer.cs b/src/Core/NosSmooth.Comms.Abstractions/IServer.cs new file mode 100644 index 0000000..85f4326 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/IServer.cs @@ -0,0 +1,40 @@ +// +// IServer.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.ComponentModel; +using Remora.Results; + +namespace NosSmooth.Comms.Data; + +/// +/// An abstraction for a server. +/// +public interface IServer +{ + /// + /// Gets the clients connected to the server. + /// + public IReadOnlyList Clients { get; } + + /// + /// Listen for a new connection and wait for it. + /// + /// The cancellation token for cancelling the operation. + /// A client connection, returned after the client has connected. + public Task> WaitForConnectionAsync(CancellationToken ct = default); + + /// + /// Start the server. + /// + /// The token used for stopping the server. may also be used. + /// A result that may or may not have succeeded. + public Task ListenAsync(CancellationToken stopToken = default); + + /// + /// Close all connections, stop listening. + /// + public void Close(); +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/Messages/CommandMessage.cs b/src/Core/NosSmooth.Comms.Abstractions/Messages/CommandMessage.cs new file mode 100644 index 0000000..6e30c02 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/Messages/CommandMessage.cs @@ -0,0 +1,11 @@ +// +// CommandMessage.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Core.Commands; + +namespace NosSmooth.Comms.Data.Messages; + +public record CommandMessage(ICommand Command); \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs b/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs new file mode 100644 index 0000000..6c896a2 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs @@ -0,0 +1,9 @@ +// +// HandshakeRequest.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Comms.Data.Messages; + +public record HandshakeRequest(bool SendRawPackets, bool SendDeserializedPackets); \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeResponse.cs b/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeResponse.cs new file mode 100644 index 0000000..663967a --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeResponse.cs @@ -0,0 +1,9 @@ +// +// HandshakeResponse.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Comms.Data.Messages; + +public record HandshakeResponse(long? CharacterId, string? CharacterName); \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/Messages/MessageWrapper.cs b/src/Core/NosSmooth.Comms.Abstractions/Messages/MessageWrapper.cs new file mode 100644 index 0000000..e0a9150 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/Messages/MessageWrapper.cs @@ -0,0 +1,9 @@ +// +// MessageWrapper.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace NosSmooth.Comms.Data.Messages; + +public record MessageWrapper(long ProtocolVersion, long MessageId, TMessage Data); \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/Messages/PacketMessage.cs b/src/Core/NosSmooth.Comms.Abstractions/Messages/PacketMessage.cs new file mode 100644 index 0000000..66c2150 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/Messages/PacketMessage.cs @@ -0,0 +1,12 @@ +// +// PacketMessage.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Packets; +using NosSmooth.PacketSerializer.Abstractions.Attributes; + +namespace NosSmooth.Comms.Data.Messages; + +public record PacketMessage(PacketSource Source, IPacket Packet); \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/Messages/RawPacketMessage.cs b/src/Core/NosSmooth.Comms.Abstractions/Messages/RawPacketMessage.cs new file mode 100644 index 0000000..bd3d12b --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/Messages/RawPacketMessage.cs @@ -0,0 +1,11 @@ +// +// RawPacketMessage.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.PacketSerializer.Abstractions.Attributes; + +namespace NosSmooth.Comms.Data.Messages; + +public record RawPacketMessage(PacketSource Source, string Packet); \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/Messages/ResponseResult.cs b/src/Core/NosSmooth.Comms.Abstractions/Messages/ResponseResult.cs new file mode 100644 index 0000000..fc960ef --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/Messages/ResponseResult.cs @@ -0,0 +1,11 @@ +// +// ResponseResult.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Remora.Results; + +namespace NosSmooth.Comms.Data.Messages; + +public record ResponseResult(long MessageId, Result Result); \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj b/src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj new file mode 100644 index 0000000..ce5dd66 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj @@ -0,0 +1,22 @@ + + + + net7.0 + enable + enable + NosSmooth.Comms.Data + + + + + + + + + + + ..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\nossmooth.core\3.3.1\lib\net7.0\NosSmooth.Core.dll + + + + diff --git a/src/Core/NosSmooth.Comms.Abstractions/Responders/IMessageResponder.cs b/src/Core/NosSmooth.Comms.Abstractions/Responders/IMessageResponder.cs new file mode 100644 index 0000000..3033fdc --- /dev/null +++ b/src/Core/NosSmooth.Comms.Abstractions/Responders/IMessageResponder.cs @@ -0,0 +1,24 @@ +// +// IMessageResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Remora.Results; + +namespace NosSmooth.Comms.Data.Responders; + +/// +/// A responder to a received message from client or server. +/// +/// The type of the message to respond to. +public interface IMessageResponder +{ + /// + /// Respond to the given message. + /// + /// The message received. + /// The cancellation token used for cancelling the operation. + /// A result that may or may not have succeeded. + public Task Respond(TMessage message, CancellationToken ct = default); +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs b/src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs new file mode 100644 index 0000000..69ab2bf --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs @@ -0,0 +1,81 @@ +// +// ClientNostaleClient.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Comms.Data; +using NosSmooth.Comms.Data.Messages; +using NosSmooth.Core.Client; +using NosSmooth.Core.Commands; +using NosSmooth.Packets; +using NosSmooth.PacketSerializer; +using NosSmooth.PacketSerializer.Abstractions.Attributes; +using Remora.Results; + +namespace NosSmooth.Comms.Core; + +/// +/// A NosTale client using . +/// +public class ClientNostaleClient : INostaleClient +{ + private readonly ConnectionHandler _connection; + + /// + /// Initializes a new instance of the class. + /// + /// The connection handler. + public ClientNostaleClient + (ConnectionHandler connection) + { + _connection = connection; + } + + /// + public Task RunAsync(CancellationToken stopRequested = default) + { + _connection.StartHandler(stopRequested); + return Task.FromResult(Result.FromSuccess()); + } + + /// + public async Task SendPacketAsync(IPacket packet, CancellationToken ct = default) + { + var messageResponse = await _connection.SendMessageAsync + (new PacketMessage(PacketSource.Client, packet), ct); + return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse); + } + + /// + public async Task SendPacketAsync(string packetString, CancellationToken ct = default) + { + var messageResponse = await _connection.SendMessageAsync + (new RawPacketMessage(PacketSource.Client, packetString), ct); + return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse); + } + + /// + public async Task ReceivePacketAsync(string packetString, CancellationToken ct = default) + { + var messageResponse = await _connection.SendMessageAsync + (new RawPacketMessage(PacketSource.Server, packetString), ct); + return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse); + } + + /// + public async Task ReceivePacketAsync(IPacket packet, CancellationToken ct = default) + { + var messageResponse = await _connection.SendMessageAsync + (new PacketMessage(PacketSource.Server, packet), ct); + return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse); + } + + /// + public async Task SendCommandAsync(ICommand command, CancellationToken ct = default) + { + var messageResponse = await _connection.SendMessageAsync + (new CommandMessage(command), ct); + return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse); + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs b/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs new file mode 100644 index 0000000..c8678ef --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs @@ -0,0 +1,184 @@ +// +// ConnectionHandler.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MessagePack; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NosSmooth.Comms.Data; +using NosSmooth.Comms.Data.Messages; +using NosSmooth.Core.Contracts; +using NosSmooth.Core.Extensions; +using Remora.Results; + +namespace NosSmooth.Comms.Core; + +/// +/// Manages a connection, calls message handler when message is received. +/// Serializes and deserializes the messages from the stream. +/// +public class ConnectionHandler +{ + private readonly Contractor? _contractor; + private readonly IConnection _connection; + private readonly MessageHandler _messageHandler; + private readonly MessagePackSerializerOptions _options; + private readonly ILogger _logger; + private long _messageId = 1; + private Task? _task; + + /// + /// Initializes a new instance of the class. + /// + /// The contractor. + /// The connection. + /// The message handler. + /// The options. + /// The logger. + public ConnectionHandler + ( + Contractor? contractor, + IConnection connection, + MessageHandler messageHandler, + IOptions options, + ILogger logger + ) + { + _contractor = contractor; + _connection = connection; + _messageHandler = messageHandler; + _options = options.Value; + _logger = logger; + } + + /// + /// Gets the connection. + /// + public IConnection Connection => _connection; + + /// + /// Run the handler and await the task. + /// + /// The token used for stopping the handler and disconnecting the connection. + /// A result that may or may not have succeeded. + public Task RunHandlerAsync(CancellationToken stopToken) + { + StartHandler(stopToken); + return _task!; + } + + /// + /// Start the connection handler task, do not wait for it. + /// + /// The token used for stopping/disconnecting the connection and handling. + public void StartHandler(CancellationToken stopToken) + { + if (_task is not null) + { + return; + } + + _task = HandlerTask(stopToken); + } + + private async Task HandlerTask(CancellationToken ct) + { + using var reader = new MessagePackStreamReader(_connection.ReadStream, true); + while (!ct.IsCancellationRequested) + { + try + { + var read = await reader.ReadAsync(ct); + if (!read.HasValue) + { + _logger.LogWarning("Message not read? ..."); + continue; + } + + var message = MessagePackSerializer.Typeless.Deserialize + (read.Value, _options, ct); + var result = await _messageHandler.HandleMessageAsync(this, message, ct); + + if (!result.IsSuccess) + { + _logger.LogResultError(result); + } + } + catch (Exception e) + { + _logger.LogError(e, "An exception was thrown during deserialization of a message."); + } + } + + _connection.Disconnect(); + return Result.FromSuccess(); + } + + /// + /// Create a contract for sending a message, + /// will be returned back. + /// + /// The message. + /// The type of the message. + /// A contract representing send message operation. + /// Thrown in case contract is created on the server. Clients do not send responses. + public IContract ContractSendMessage(TMessage message) + { + if (_contractor is null) + { + throw new InvalidOperationException + ( + "Contracting is not supported, the other side does not send responses. Only server sends responses back." + ); + } + + long messageId = 0; + return new ContractBuilder(_contractor, DefaultStates.None) + .SetMoveAction + ( + DefaultStates.None, + async (a, ct) => + { + var result = await SendMessageAsync(message, ct); + if (!result.IsDefined(out messageId)) + { + return Result.FromError(result); + } + + return true; + }, + DefaultStates.Requested + ) + .SetMoveFilter + (DefaultStates.Requested, (r) => r.MessageId == messageId, DefaultStates.ResponseObtained) + .SetFillData(DefaultStates.ResponseObtained, r => r.Result) + .Build(); + } + + /// + /// Send message to the other end. + /// + /// The message to send. It will be wrapped before sending. + /// The cancellation token used for cancelling the operation. + /// Type of the message to send. + /// The id of the message sent or an error. + public async Task> SendMessageAsync(TMessage message, CancellationToken ct = default) + { + var messageId = _messageId++; + var messageWrapper = new MessageWrapper(1, messageId, message); + + try + { + await MessagePackSerializer.Typeless.SerializeAsync(_connection.WriteStream, messageWrapper, _options, ct); + await _connection.WriteStream.FlushAsync(ct); + } + catch (Exception e) + { + return e; + } + + return messageId; + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/ConnectionInjector.cs b/src/Core/NosSmooth.Comms.Core/ConnectionInjector.cs new file mode 100644 index 0000000..b97168a --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/ConnectionInjector.cs @@ -0,0 +1,25 @@ +// +// ConnectionInjector.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Comms.Data; + +namespace NosSmooth.Comms.Core; + +/// +/// Injects connection and connection handler into dependency injection. +/// +public class ConnectionInjector +{ + /// + /// Gets or sets the connection. + /// + public IConnection? Connection { get; set; } + + /// + /// Gets or sets the connection handler. + /// + public ConnectionHandler? ConnectionHandler { get; set; } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/Extensions/ServiceCollectionExtensions.cs b/src/Core/NosSmooth.Comms.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..32137d8 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,166 @@ +// +// ServiceCollectionExtensions.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.DependencyInjection; +using NosSmooth.Comms.Core.Responders; +using NosSmooth.Comms.Data; +using NosSmooth.Comms.Data.Responders; +using NosSmooth.Core.Client; + +namespace NosSmooth.Comms.Core.Extensions; + +/// +/// Extension methods for . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds server handling ( and ). + /// + /// + /// The specific server has to be added separately as . + /// + /// The service collection. + /// The same service collection. + public static IServiceCollection AddServerHandling(this IServiceCollection serviceCollection) + => serviceCollection + .AddNosSmoothResolverOptions() + .AddSingleton(p => new MessageHandler(p, true)) + .AddSingleton() + .AddInjecting(); + + /// + /// Adds handling for a single client. + /// + /// + /// The specific client has to be added separately as . + /// + /// The service collection. + /// The same service collection. + public static IServiceCollection AddSingleClientHandling(this IServiceCollection serviceCollection) + => serviceCollection + .AddInjecting() + .AddNosSmoothResolverOptions() + .AddMessageResponder() + .AddSingleton() + .AddSingleton(p => p.GetRequiredService()) + .AddSingleton(p => new MessageHandler(p, false)) + .AddScoped() + .AddScoped(); + + /// + /// Add handling for multiple clients. + /// + /// + /// The clients should not be inside of the provider. + /// Initialize clients outside of the provider and use the + /// provider for injecting connection handler and nostale client. + /// Nostale client will be created automatically if connection is injected successfully. + /// Connection will be injected when calling message handler with the specific connection. + /// + /// Connection may be injected by setting properties in a scope. + /// + /// The service collection. + /// The same service collection. + public static IServiceCollection AddMultiClientHandling(this IServiceCollection serviceCollection) + => serviceCollection + .AddNosSmoothResolverOptions() + .AddSingleton() + .AddSingleton(p => new MessageHandler(p, false)) + .AddInjecting() + .AddScoped + (p => p.GetRequiredService().Resolve(p.GetRequiredService())); + + /// + /// Add with default NosSmooth options. + /// + /// The service collection. + /// The same service collection. + public static IServiceCollection AddNosSmoothResolverOptions(this IServiceCollection serviceCollection) + => serviceCollection + .Configure + (o => o.Options = o.Options.WithResolver(NosSmoothResolver.Instance)); + + /// + /// Adds a message responder. + /// + /// The service collection. + /// The type of the responder. + /// The same service collection. + public static IServiceCollection AddMessageResponder(this IServiceCollection serviceCollection) + { + return serviceCollection.AddMessageResponder(typeof(TResponder)); + } + + /// + /// Adds a message responder. + /// + /// The service collection. + /// The type of the responder. + /// The same service collection. + public static IServiceCollection AddMessageResponder(this IServiceCollection serviceCollection, Type responderType) + { + if (serviceCollection.Any(x => x.ImplementationType == responderType)) + { // already added... assuming every packet responder was added even though that may not be the case. + return serviceCollection; + } + + if (!responderType.GetInterfaces().Any + ( + i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMessageResponder<>) + )) + { + throw new ArgumentException + ( + $"{nameof(responderType)} should implement IMessageResponder.", + nameof(responderType) + ); + } + + var responderTypeInterfaces = responderType.GetInterfaces(); + var responderInterfaces = responderTypeInterfaces.Where + ( + r => r.IsGenericType && r.GetGenericTypeDefinition() == typeof(IMessageResponder<>) + ); + + foreach (var responderInterface in responderInterfaces) + { + serviceCollection.AddScoped(responderInterface, responderType); + } + + return serviceCollection; + } + + private static IServiceCollection AddInjecting(this IServiceCollection serviceCollection) + => serviceCollection + .AddScoped() + .AddScoped + ( + p => + { + var handler = p.GetRequiredService().ConnectionHandler; + if (handler is null) + { + throw new InvalidOperationException("Connection handler was requested, but is not injected."); + } + + return handler; + } + ) + .AddScoped + ( + p => + { + var connection = p.GetRequiredService().Connection; + if (connection is null) + { + throw new InvalidOperationException("Connection was requested, but is not injected."); + } + + return connection; + } + ); +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/Formatters/NameStringFormatter.cs b/src/Core/NosSmooth.Comms.Core/Formatters/NameStringFormatter.cs new file mode 100644 index 0000000..11ed208 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/Formatters/NameStringFormatter.cs @@ -0,0 +1,46 @@ +// +// NameStringFormatter.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Text; +using MessagePack; +using MessagePack.Formatters; +using NosSmooth.PacketSerializer.Abstractions.Common; + +namespace NosSmooth.Comms.Core.Formatters; + +/// +/// A formatter for . +/// +public class NameStringFormatter : IMessagePackFormatter +{ + /// + public void Serialize(ref MessagePackWriter writer, NameString? value, MessagePackSerializerOptions options) + { + if (value is null) + { + writer.WriteNil(); + return; + } + + var bytes = Encoding.UTF8.GetBytes(value.Name); + writer.WriteString(bytes); + } + + /// + public NameString? Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + if (reader.TryReadNil()) + { + return null; + } + + options.Security.DepthStep(ref reader); + var name = reader.ReadString(); + + reader.Depth--; + return NameString.FromString(name); + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/MessageHandler.cs b/src/Core/NosSmooth.Comms.Core/MessageHandler.cs new file mode 100644 index 0000000..e12007f --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/MessageHandler.cs @@ -0,0 +1,112 @@ +// +// MessageHandler.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Reflection; +using System.Runtime.InteropServices.JavaScript; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NosSmooth.Comms.Data; +using NosSmooth.Comms.Data.Messages; +using NosSmooth.Comms.Data.Responders; +using Remora.Results; + +namespace NosSmooth.Comms.Core; + +/// +/// An executor of message responders. +/// +public class MessageHandler +{ + private readonly IServiceProvider _services; + private readonly bool _respond; + + /// + /// Initializes a new instance of the class. + /// + /// The services. + /// Whether to respond to the messages. + public MessageHandler(IServiceProvider services, bool respond) + { + _services = services; + _respond = respond; + } + + /// + /// Handle the given message, call responders. + /// + /// The connection the message comes from. + /// The message to handle. + /// The cancellation token used for cancelling the operation. + /// A result that may or may not have succeeded. + public async Task HandleMessageAsync + (ConnectionHandler connection, object wrappedMessage, CancellationToken ct) + { + var wrappedType = wrappedMessage.GetType(); + if (!wrappedType.IsGenericType) + { + return new GenericError($"Message type is not MessageWrapper<>, but {wrappedType.FullName}"); + } + + if (wrappedType.GetGenericTypeDefinition() != typeof(MessageWrapper<>)) + { + return new GenericError($"Message type is not MessageWrapper<>, but {wrappedType.FullName}"); + } + + var messageType = wrappedType.GetGenericArguments().First(); + + var handleMessageMethod = GetType().GetMethod + (nameof(GenericHandleMessageAsync), BindingFlags.NonPublic | BindingFlags.Instance)!.MakeGenericMethod + (new[] { messageType }); + + var task = (Task)handleMessageMethod.Invoke(this, new[] { connection, wrappedMessage, ct })!; + return await task; + } + + private async Task GenericHandleMessageAsync + (ConnectionHandler connection, MessageWrapper wrappedMessage, CancellationToken ct) + { + var data = wrappedMessage.Data; + + await using var scope = _services.CreateAsyncScope(); + var injector = scope.ServiceProvider.GetRequiredService(); + injector.ConnectionHandler = connection; + injector.Connection = connection.Connection; + + var responders = scope.ServiceProvider + .GetServices>() + .Select(x => x.Respond(data, ct)); + + var results = (await Task.WhenAll(responders)) + .Where(x => !x.IsSuccess) + .Cast() + .ToList(); + + var result = results.Count switch + { + 0 => Result.FromSuccess(), + 1 => (Result)results[0], + _ => new AggregateError(results) + }; + + if (_respond && wrappedMessage.Data is not ResponseResult) + { + var response = new ResponseResult(wrappedMessage.MessageId, result); + var sentMessageResult = await connection.SendMessageAsync(response, ct); + if (!sentMessageResult.IsSuccess) + { + results.Add(sentMessageResult); + result = results.Count switch + { + 0 => Result.FromSuccess(), + 1 => (Result)results[0], + _ => new AggregateError(results) + }; + } + } + + return result; + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj b/src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj new file mode 100644 index 0000000..4efb34d --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj @@ -0,0 +1,33 @@ + + + + net7.0 + enable + enable + + + + + ..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\messagepack\2.4.59\lib\net6.0\MessagePack.dll + + + ..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\7.0.0\lib\net7.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll + + + ..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\microsoft.extensions.logging.abstractions\7.0.0\lib\net7.0\Microsoft.Extensions.Logging.Abstractions.dll + + + ..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\nossmooth.packetserializer.abstractions\1.3.0\lib\net7.0\NosSmooth.PacketSerializer.Abstractions.dll + + + + + + + + + + + + + diff --git a/src/Core/NosSmooth.Comms.Core/NosSmoothMessageSerializerOptions.cs b/src/Core/NosSmooth.Comms.Core/NosSmoothMessageSerializerOptions.cs new file mode 100644 index 0000000..7680d11 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/NosSmoothMessageSerializerOptions.cs @@ -0,0 +1,30 @@ +// +// NosSmoothMessageSerializerOptions.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MessagePack; + +namespace NosSmooth.Comms.Core; + +/// +/// Contains options for MessagePack. +/// +public class NosSmoothMessageSerializerOptions +{ + /// + /// Gets or sets the message pack options. + /// + public MessagePackSerializerOptions Options { get; set; } = MessagePackSerializer.Typeless.DefaultOptions; + + /// + /// Obtain the options. + /// + /// The options wrapper. + /// The options. + public static implicit operator MessagePackSerializerOptions(NosSmoothMessageSerializerOptions options) + { + return options.Options; + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/NosSmoothResolver.cs b/src/Core/NosSmooth.Comms.Core/NosSmoothResolver.cs new file mode 100644 index 0000000..f98175b --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/NosSmoothResolver.cs @@ -0,0 +1,31 @@ +// +// NosSmoothResolver.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using MessagePack; +using NosSmooth.Comms.Core.Formatters; + +namespace NosSmooth.Comms.Core; + +/// +/// A class for obtaining MessagePack formatter resolver. +/// +public class NosSmoothResolver +{ + /// + /// Gets a formatter resolver for NosSmooth messages. + /// + public static IFormatterResolver Instance => MessagePack.Resolvers.CompositeResolver.Create + ( + new[] + { + new NameStringFormatter() + }, + new[] + { + MessagePackSerializer.Typeless.DefaultOptions.Resolver, + } + ); +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/NostaleClientResolver.cs b/src/Core/NosSmooth.Comms.Core/NostaleClientResolver.cs new file mode 100644 index 0000000..28a214c --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/NostaleClientResolver.cs @@ -0,0 +1,60 @@ +// +// NostaleClientResolver.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; +using NosSmooth.Comms.Data; +using NosSmooth.Core.Client; + +namespace NosSmooth.Comms.Core; + +/// +/// Resolves s into s. +/// +/// +/// Clients will be connected in case the client is not registered yet. +/// If you wish to register the client yourself, use . +/// +public class NostaleClientResolver +{ + private readonly IServiceProvider _services; + private readonly ConcurrentDictionary _clients; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider. + public NostaleClientResolver(IServiceProvider services) + { + _services = services; + _clients = new ConcurrentDictionary(); + } + + /// + /// Resolve the connection handler into nostale client. + /// + /// The connection. + /// The resolved client. + public INostaleClient Resolve(ConnectionHandler connection) + { + if (!_clients.ContainsKey(connection.Connection)) + { + RegisterClient(connection, ActivatorUtilities.CreateInstance(_services, connection)); + } + + return _clients[connection.Connection]; + } + + /// + /// Register the given client for the given connection. + /// + /// The connection handler. + /// The client to register for the given handler. + public void RegisterClient(ConnectionHandler connection, INostaleClient client) + { + _clients[connection.Connection] = client; + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/Responders/ResponseResultResponder.cs b/src/Core/NosSmooth.Comms.Core/Responders/ResponseResultResponder.cs new file mode 100644 index 0000000..712cf2b --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/Responders/ResponseResultResponder.cs @@ -0,0 +1,34 @@ +// +// ResponseResultResponder.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using NosSmooth.Comms.Data.Messages; +using NosSmooth.Comms.Data.Responders; +using NosSmooth.Core.Contracts; +using Remora.Results; + +namespace NosSmooth.Comms.Core.Responders; + +/// +/// Responds to by updating contractor with the response. +/// +public class ResponseResultResponder : IMessageResponder +{ + private readonly Contractor _contractor; + + /// + /// Initializes a new instance of the class. + /// + /// The contractor. + public ResponseResultResponder(Contractor contractor) + { + _contractor = contractor; + + } + + /// + public Task Respond(ResponseResult message, CancellationToken ct = default) + => _contractor.Update(message, ct); +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/ServerManager.cs b/src/Core/NosSmooth.Comms.Core/ServerManager.cs new file mode 100644 index 0000000..ae125b1 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/ServerManager.cs @@ -0,0 +1,159 @@ +// +// ServerManager.cs +// +// Copyright (c) František Boháček. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NosSmooth.Comms.Data; +using NosSmooth.Comms.Data.Messages; +using NosSmooth.Core.Extensions; +using Remora.Results; + +namespace NosSmooth.Comms.Core; + +/// +/// Manages a server, awaits connections, handles messages. +/// +public class ServerManager +{ + private readonly IServer _server; + private readonly MessageHandler _messageHandler; + private readonly IOptions _options; + private readonly ILogger _logger; + private readonly ILogger _handlerLogger; + private readonly List _connectionHandlers; + private Task? _task; + private CancellationTokenSource? _ctSource; + + /// + /// Initializes a new instance of the class. + /// + /// The server to manage. + /// The message handler. + /// The options. + /// The logger. + /// The logger for message handler. + public ServerManager + ( + IServer server, + MessageHandler messageHandler, + IOptions options, + ILogger logger, + ILogger handlerLogger + ) + { + _server = server; + _connectionHandlers = new List(); + _messageHandler = messageHandler; + _options = options; + _logger = logger; + _handlerLogger = handlerLogger; + } + + /// + /// Run the manager and await the task. + /// + /// The token used for stopping the handler and disconnecting the connection. + /// A result that may or may not have succeeded. + public Task RunManagerAsync(CancellationToken stopToken) + { + StartManager(stopToken); + return _task!; + } + + /// + /// Broadcast the given message to all clients. + /// + /// The message to broadcast. + /// The cancellation token used for cancelling the operation. + /// The type of the message. + /// A result that may or may not have succeeded. + public async Task BroadcastAsync(TMessage message, CancellationToken ct = default) + { + var errors = new List(); + foreach (var handler in _connectionHandlers) + { + var result = await handler.SendMessageAsync(message, ct); + if (!result.IsSuccess) + { + errors.Add(Result.FromError(result)); + } + } + + return errors.Count switch + { + 0 => Result.FromSuccess(), + 1 => (Result)errors[0], + _ => new AggregateError(errors) + }; + } + + /// + /// Run the handler without awaiting the task. + /// + /// The token used for stopping the handler and disconnecting the connection. + public void StartManager(CancellationToken stopToken = default) + { + if (_task is not null) + { + return; + } + + _ctSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken); + _task = ManagerTask(); + } + + /// + /// Request stop the server. + /// + public void RequestStop() + { + _ctSource?.Cancel(); + } + + private async Task ManagerTask() + { + if (_ctSource is null) + { + throw new InvalidOperationException("The ct source is not initialized."); + } + + await _server.ListenAsync(_ctSource!.Token); + + while (!_ctSource.IsCancellationRequested) + { + var connectionResult = await _server.WaitForConnectionAsync(_ctSource.Token); + if (!connectionResult.IsDefined(out var connection)) + { + _logger.LogResultError(connectionResult); + continue; + } + + var handler = new ConnectionHandler(null, connection, _messageHandler, _options, _handlerLogger); + _connectionHandlers.Add(handler); + + handler.StartHandler(_ctSource.Token); + } + + List errors = new List(); + foreach (var handler in _connectionHandlers) + { + var handlerResult = await handler.RunHandlerAsync(_ctSource.Token); + + if (!handlerResult.IsSuccess) + { + errors.Add(handlerResult); + } + } + + _server.Close(); + return errors.Count switch + { + 0 => Result.FromSuccess(), + 1 => (Result)errors[0], + _ => new AggregateError(errors) + }; + } +} \ No newline at end of file -- 2.48.1