M NosSmooth.Comms.sln => NosSmooth.Comms.sln +66 -0
@@ 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
A src/Core/NosSmooth.Comms.Abstractions/IClient.cs => src/Core/NosSmooth.Comms.Abstractions/IClient.cs +23 -0
@@ 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;
+
+/// <summary>
+/// An abstraction for a client connection.
+/// </summary>
+public interface IClient : IConnection
+{
+ /// <summary>
+ /// Connect to the server.
+ /// </summary>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> ConnectAsync(CancellationToken ct = default);
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Abstractions/IConnection.cs => src/Core/NosSmooth.Comms.Abstractions/IConnection.cs +35 -0
@@ 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;
+
+/// <summary>
+/// A connection, either a client or a server connection.
+/// </summary>
+public interface IConnection
+{
+ /// <summary>
+ /// Gets the state of the connection.
+ /// </summary>
+ public ConnectionState State { get; }
+
+ /// <summary>
+ /// Gets the stream used for reading the data received.
+ /// </summary>
+ public Stream ReadStream { get; }
+
+ /// <summary>
+ /// Gets the stream used for writing data.
+ /// </summary>
+ public Stream WriteStream { get; }
+
+ /// <summary>
+ /// Disconnect, close the connection.
+ /// </summary>
+ public void Disconnect();
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Abstractions/IServer.cs => src/Core/NosSmooth.Comms.Abstractions/IServer.cs +40 -0
@@ 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;
+
+/// <summary>
+/// An abstraction for a server.
+/// </summary>
+public interface IServer
+{
+ /// <summary>
+ /// Gets the clients connected to the server.
+ /// </summary>
+ public IReadOnlyList<IConnection> Clients { get; }
+
+ /// <summary>
+ /// Listen for a new connection and wait for it.
+ /// </summary>
+ /// <param name="ct">The cancellation token for cancelling the operation.</param>
+ /// <returns>A client connection, returned after the client has connected.</returns>
+ public Task<Result<IConnection>> WaitForConnectionAsync(CancellationToken ct = default);
+
+ /// <summary>
+ /// Start the server.
+ /// </summary>
+ /// <param name="stopToken">The token used for stopping the server. <see cref="CloseAsync"/> may also be used.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> ListenAsync(CancellationToken stopToken = default);
+
+ /// <summary>
+ /// Close all connections, stop listening.
+ /// </summary>
+ public void Close();
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Abstractions/Messages/CommandMessage.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/CommandMessage.cs +11 -0
@@ 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
A src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs +9 -0
@@ 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
A src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeResponse.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeResponse.cs +9 -0
@@ 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
A src/Core/NosSmooth.Comms.Abstractions/Messages/MessageWrapper.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/MessageWrapper.cs +9 -0
@@ 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<TMessage>(long ProtocolVersion, long MessageId, TMessage Data);<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Abstractions/Messages/PacketMessage.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/PacketMessage.cs +12 -0
@@ 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
A src/Core/NosSmooth.Comms.Abstractions/Messages/RawPacketMessage.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/RawPacketMessage.cs +11 -0
@@ 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
A src/Core/NosSmooth.Comms.Abstractions/Messages/ResponseResult.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/ResponseResult.cs +11 -0
@@ 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
A src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj => src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj +22 -0
@@ 0,0 1,22 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <RootNamespace>NosSmooth.Comms.Data</RootNamespace>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
+ <PackageReference Include="NosSmooth.Packets" Version="3.5.0" />
+ <PackageReference Include="NosSmooth.PacketSerializer.Abstractions" Version="1.3.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Reference Include="NosSmooth.Core">
+ <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\nossmooth.core\3.3.1\lib\net7.0\NosSmooth.Core.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+</Project>
A src/Core/NosSmooth.Comms.Abstractions/Responders/IMessageResponder.cs => src/Core/NosSmooth.Comms.Abstractions/Responders/IMessageResponder.cs +24 -0
@@ 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;
+
+/// <summary>
+/// A responder to a received message from client or server.
+/// </summary>
+/// <typeparam name="TMessage">The type of the message to respond to.</typeparam>
+public interface IMessageResponder<TMessage>
+{
+ /// <summary>
+ /// Respond to the given message.
+ /// </summary>
+ /// <param name="message">The message received.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> Respond(TMessage message, CancellationToken ct = default);
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs => src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs +81 -0
@@ 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;
+
+/// <summary>
+/// A NosTale client using <see cref="IClient"/>.
+/// </summary>
+public class ClientNostaleClient : INostaleClient
+{
+ private readonly ConnectionHandler _connection;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ClientNostaleClient"/> class.
+ /// </summary>
+ /// <param name="connection">The connection handler.</param>
+ public ClientNostaleClient
+ (ConnectionHandler connection)
+ {
+ _connection = connection;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> RunAsync(CancellationToken stopRequested = default)
+ {
+ _connection.StartHandler(stopRequested);
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> 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);
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> 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);
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> 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);
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> 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);
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> 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
A src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs => src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs +184 -0
@@ 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;
+
+/// <summary>
+/// Manages a connection, calls message handler when message is received.
+/// Serializes and deserializes the messages from the stream.
+/// </summary>
+public class ConnectionHandler
+{
+ private readonly Contractor? _contractor;
+ private readonly IConnection _connection;
+ private readonly MessageHandler _messageHandler;
+ private readonly MessagePackSerializerOptions _options;
+ private readonly ILogger<ConnectionHandler> _logger;
+ private long _messageId = 1;
+ private Task<Result>? _task;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ConnectionHandler"/> class.
+ /// </summary>
+ /// <param name="contractor">The contractor.</param>
+ /// <param name="connection">The connection.</param>
+ /// <param name="messageHandler">The message handler.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="logger">The logger.</param>
+ public ConnectionHandler
+ (
+ Contractor? contractor,
+ IConnection connection,
+ MessageHandler messageHandler,
+ IOptions<NosSmoothMessageSerializerOptions> options,
+ ILogger<ConnectionHandler> logger
+ )
+ {
+ _contractor = contractor;
+ _connection = connection;
+ _messageHandler = messageHandler;
+ _options = options.Value;
+ _logger = logger;
+ }
+
+ /// <summary>
+ /// Gets the connection.
+ /// </summary>
+ public IConnection Connection => _connection;
+
+ /// <summary>
+ /// Run the handler and await the task.
+ /// </summary>
+ /// <param name="stopToken">The token used for stopping the handler and disconnecting the connection.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> RunHandlerAsync(CancellationToken stopToken)
+ {
+ StartHandler(stopToken);
+ return _task!;
+ }
+
+ /// <summary>
+ /// Start the connection handler task, do not wait for it.
+ /// </summary>
+ /// <param name="stopToken">The token used for stopping/disconnecting the connection and handling.</param>
+ public void StartHandler(CancellationToken stopToken)
+ {
+ if (_task is not null)
+ {
+ return;
+ }
+
+ _task = HandlerTask(stopToken);
+ }
+
+ private async Task<Result> 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();
+ }
+
+ /// <summary>
+ /// Create a contract for sending a message,
+ /// <see cref="ResponseResult"/> will be returned back.
+ /// </summary>
+ /// <param name="message">The message.</param>
+ /// <typeparam name="TMessage">The type of the message.</typeparam>
+ /// <returns>A contract representing send message operation.</returns>
+ /// <exception cref="InvalidOperationException">Thrown in case contract is created on the server. Clients do not send responses.</exception>
+ public IContract<Result, DefaultStates> ContractSendMessage<TMessage>(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<Result, DefaultStates, NoErrors>(_contractor, DefaultStates.None)
+ .SetMoveAction
+ (
+ DefaultStates.None,
+ async (a, ct) =>
+ {
+ var result = await SendMessageAsync(message, ct);
+ if (!result.IsDefined(out messageId))
+ {
+ return Result<bool>.FromError(result);
+ }
+
+ return true;
+ },
+ DefaultStates.Requested
+ )
+ .SetMoveFilter<ResponseResult>
+ (DefaultStates.Requested, (r) => r.MessageId == messageId, DefaultStates.ResponseObtained)
+ .SetFillData<ResponseResult>(DefaultStates.ResponseObtained, r => r.Result)
+ .Build();
+ }
+
+ /// <summary>
+ /// Send message to the other end.
+ /// </summary>
+ /// <param name="message">The message to send. It will be wrapped before sending.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <typeparam name="TMessage">Type of the message to send.</typeparam>
+ /// <returns>The id of the message sent or an error.</returns>
+ public async Task<Result<long>> SendMessageAsync<TMessage>(TMessage message, CancellationToken ct = default)
+ {
+ var messageId = _messageId++;
+ var messageWrapper = new MessageWrapper<TMessage>(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
A src/Core/NosSmooth.Comms.Core/ConnectionInjector.cs => src/Core/NosSmooth.Comms.Core/ConnectionInjector.cs +25 -0
@@ 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;
+
+/// <summary>
+/// Injects connection and connection handler into dependency injection.
+/// </summary>
+public class ConnectionInjector
+{
+ /// <summary>
+ /// Gets or sets the connection.
+ /// </summary>
+ public IConnection? Connection { get; set; }
+
+ /// <summary>
+ /// Gets or sets the connection handler.
+ /// </summary>
+ public ConnectionHandler? ConnectionHandler { get; set; }
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Core/Extensions/ServiceCollectionExtensions.cs => src/Core/NosSmooth.Comms.Core/Extensions/ServiceCollectionExtensions.cs +166 -0
@@ 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;
+
+/// <summary>
+/// Extension methods for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Adds server handling (<see cref="MessageHandler"/> and <see cref="ServerManager"/>).
+ /// </summary>
+ /// <remarks>
+ /// The specific server has to be added separately as <see cref="IServer"/>.
+ /// </remarks>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The same service collection.</returns>
+ public static IServiceCollection AddServerHandling(this IServiceCollection serviceCollection)
+ => serviceCollection
+ .AddNosSmoothResolverOptions()
+ .AddSingleton<MessageHandler>(p => new MessageHandler(p, true))
+ .AddSingleton<ServerManager>()
+ .AddInjecting();
+
+ /// <summary>
+ /// Adds handling for a single client.
+ /// </summary>
+ /// <remarks>
+ /// The specific client has to be added separately as <see cref="IConnection"/>.
+ /// </remarks>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The same service collection.</returns>
+ public static IServiceCollection AddSingleClientHandling(this IServiceCollection serviceCollection)
+ => serviceCollection
+ .AddInjecting()
+ .AddNosSmoothResolverOptions()
+ .AddMessageResponder<ResponseResultResponder>()
+ .AddSingleton<NostaleClientResolver>()
+ .AddSingleton<IConnection>(p => p.GetRequiredService<IClient>())
+ .AddSingleton<MessageHandler>(p => new MessageHandler(p, false))
+ .AddScoped<ConnectionHandler>()
+ .AddScoped<INostaleClient, ClientNostaleClient>();
+
+ /// <summary>
+ /// Add handling for multiple clients.
+ /// </summary>
+ /// <remarks>
+ /// 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 <see cref="ConnectionInjector"/> properties in a scope.
+ /// </remarks>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The same service collection.</returns>
+ public static IServiceCollection AddMultiClientHandling(this IServiceCollection serviceCollection)
+ => serviceCollection
+ .AddNosSmoothResolverOptions()
+ .AddSingleton<NostaleClientResolver>()
+ .AddSingleton<MessageHandler>(p => new MessageHandler(p, false))
+ .AddInjecting()
+ .AddScoped<INostaleClient>
+ (p => p.GetRequiredService<NostaleClientResolver>().Resolve(p.GetRequiredService<ConnectionHandler>()));
+
+ /// <summary>
+ /// Add <see cref="NosSmoothMessageSerializerOptions"/> with default NosSmooth options.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <returns>The same service collection.</returns>
+ public static IServiceCollection AddNosSmoothResolverOptions(this IServiceCollection serviceCollection)
+ => serviceCollection
+ .Configure<NosSmoothMessageSerializerOptions>
+ (o => o.Options = o.Options.WithResolver(NosSmoothResolver.Instance));
+
+ /// <summary>
+ /// Adds a message responder.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <typeparam name="TResponder">The type of the responder.</typeparam>
+ /// <returns>The same service collection.</returns>
+ public static IServiceCollection AddMessageResponder<TResponder>(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection.AddMessageResponder(typeof(TResponder));
+ }
+
+ /// <summary>
+ /// Adds a message responder.
+ /// </summary>
+ /// <param name="serviceCollection">The service collection.</param>
+ /// <param name="responderType">The type of the responder.</param>
+ /// <returns>The same service collection.</returns>
+ 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<ConnectionInjector>()
+ .AddScoped<ConnectionHandler>
+ (
+ p =>
+ {
+ var handler = p.GetRequiredService<ConnectionInjector>().ConnectionHandler;
+ if (handler is null)
+ {
+ throw new InvalidOperationException("Connection handler was requested, but is not injected.");
+ }
+
+ return handler;
+ }
+ )
+ .AddScoped<IConnection>
+ (
+ p =>
+ {
+ var connection = p.GetRequiredService<ConnectionInjector>().Connection;
+ if (connection is null)
+ {
+ throw new InvalidOperationException("Connection was requested, but is not injected.");
+ }
+
+ return connection;
+ }
+ );
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Core/Formatters/NameStringFormatter.cs => src/Core/NosSmooth.Comms.Core/Formatters/NameStringFormatter.cs +46 -0
@@ 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;
+
+/// <summary>
+/// A formatter for <see cref="NameString"/>.
+/// </summary>
+public class NameStringFormatter : IMessagePackFormatter<NameString?>
+{
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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
A src/Core/NosSmooth.Comms.Core/MessageHandler.cs => src/Core/NosSmooth.Comms.Core/MessageHandler.cs +112 -0
@@ 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;
+
+/// <summary>
+/// An executor of message responders.
+/// </summary>
+public class MessageHandler
+{
+ private readonly IServiceProvider _services;
+ private readonly bool _respond;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="MessageHandler"/> class.
+ /// </summary>
+ /// <param name="services">The services.</param>
+ /// <param name="respond">Whether to respond to the messages.</param>
+ public MessageHandler(IServiceProvider services, bool respond)
+ {
+ _services = services;
+ _respond = respond;
+ }
+
+ /// <summary>
+ /// Handle the given message, call responders.
+ /// </summary>
+ /// <param name="connection">The connection the message comes from.</param>
+ /// <param name="wrappedMessage">The message to handle.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public async Task<Result> 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<Result>)handleMessageMethod.Invoke(this, new[] { connection, wrappedMessage, ct })!;
+ return await task;
+ }
+
+ private async Task<Result> GenericHandleMessageAsync<TMessage>
+ (ConnectionHandler connection, MessageWrapper<TMessage> wrappedMessage, CancellationToken ct)
+ {
+ var data = wrappedMessage.Data;
+
+ await using var scope = _services.CreateAsyncScope();
+ var injector = scope.ServiceProvider.GetRequiredService<ConnectionInjector>();
+ injector.ConnectionHandler = connection;
+ injector.Connection = connection.Connection;
+
+ var responders = scope.ServiceProvider
+ .GetServices<IMessageResponder<TMessage>>()
+ .Select(x => x.Respond(data, ct));
+
+ var results = (await Task.WhenAll(responders))
+ .Where(x => !x.IsSuccess)
+ .Cast<IResult>()
+ .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
A src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj => src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj +33 -0
@@ 0,0 1,33 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Reference Include="MessagePack">
+ <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\messagepack\2.4.59\lib\net6.0\MessagePack.dll</HintPath>
+ </Reference>
+ <Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
+ <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\microsoft.extensions.dependencyinjection.abstractions\7.0.0\lib\net7.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
+ </Reference>
+ <Reference Include="Microsoft.Extensions.Logging.Abstractions">
+ <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\microsoft.extensions.logging.abstractions\7.0.0\lib\net7.0\Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
+ </Reference>
+ <Reference Include="NosSmooth.PacketSerializer.Abstractions">
+ <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\nossmooth.packetserializer.abstractions\1.3.0\lib\net7.0\NosSmooth.PacketSerializer.Abstractions.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
+ <PackageReference Include="NosSmooth.Core" Version="3.3.1" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\NosSmooth.Comms.Abstractions\NosSmooth.Comms.Abstractions.csproj" />
+ </ItemGroup>
+
+</Project>
A src/Core/NosSmooth.Comms.Core/NosSmoothMessageSerializerOptions.cs => src/Core/NosSmooth.Comms.Core/NosSmoothMessageSerializerOptions.cs +30 -0
@@ 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;
+
+/// <summary>
+/// Contains options for MessagePack.
+/// </summary>
+public class NosSmoothMessageSerializerOptions
+{
+ /// <summary>
+ /// Gets or sets the message pack options.
+ /// </summary>
+ public MessagePackSerializerOptions Options { get; set; } = MessagePackSerializer.Typeless.DefaultOptions;
+
+ /// <summary>
+ /// Obtain the options.
+ /// </summary>
+ /// <param name="options">The options wrapper.</param>
+ /// <returns>The options.</returns>
+ public static implicit operator MessagePackSerializerOptions(NosSmoothMessageSerializerOptions options)
+ {
+ return options.Options;
+ }
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Core/NosSmoothResolver.cs => src/Core/NosSmooth.Comms.Core/NosSmoothResolver.cs +31 -0
@@ 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;
+
+/// <summary>
+/// A class for obtaining MessagePack formatter resolver.
+/// </summary>
+public class NosSmoothResolver
+{
+ /// <summary>
+ /// Gets a formatter resolver for NosSmooth messages.
+ /// </summary>
+ public static IFormatterResolver Instance => MessagePack.Resolvers.CompositeResolver.Create
+ (
+ new[]
+ {
+ new NameStringFormatter()
+ },
+ new[]
+ {
+ MessagePackSerializer.Typeless.DefaultOptions.Resolver,
+ }
+ );
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Core/NostaleClientResolver.cs => src/Core/NosSmooth.Comms.Core/NostaleClientResolver.cs +60 -0
@@ 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;
+
+/// <summary>
+/// Resolves <see cref="IConnection"/>s into <see cref="INostaleClient"/>s.
+/// </summary>
+/// <remarks>
+/// Clients will be connected in case the client is not registered yet.
+/// If you wish to register the client yourself, use <see cref="RegisterClient"/>.
+/// </remarks>
+public class NostaleClientResolver
+{
+ private readonly IServiceProvider _services;
+ private readonly ConcurrentDictionary<IConnection, INostaleClient> _clients;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NostaleClientResolver"/> class.
+ /// </summary>
+ /// <param name="services">The service provider.</param>
+ public NostaleClientResolver(IServiceProvider services)
+ {
+ _services = services;
+ _clients = new ConcurrentDictionary<IConnection, INostaleClient>();
+ }
+
+ /// <summary>
+ /// Resolve the connection handler into nostale client.
+ /// </summary>
+ /// <param name="connection">The connection.</param>
+ /// <returns>The resolved client.</returns>
+ public INostaleClient Resolve(ConnectionHandler connection)
+ {
+ if (!_clients.ContainsKey(connection.Connection))
+ {
+ RegisterClient(connection, ActivatorUtilities.CreateInstance<ClientNostaleClient>(_services, connection));
+ }
+
+ return _clients[connection.Connection];
+ }
+
+ /// <summary>
+ /// Register the given client for the given connection.
+ /// </summary>
+ /// <param name="connection">The connection handler.</param>
+ /// <param name="client">The client to register for the given handler.</param>
+ public void RegisterClient(ConnectionHandler connection, INostaleClient client)
+ {
+ _clients[connection.Connection] = client;
+ }
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Core/Responders/ResponseResultResponder.cs => src/Core/NosSmooth.Comms.Core/Responders/ResponseResultResponder.cs +34 -0
@@ 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;
+
+/// <summary>
+/// Responds to <see cref="ResponseResult"/> by updating contractor with the response.
+/// </summary>
+public class ResponseResultResponder : IMessageResponder<ResponseResult>
+{
+ private readonly Contractor _contractor;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ResponseResultResponder"/> class.
+ /// </summary>
+ /// <param name="contractor">The contractor.</param>
+ public ResponseResultResponder(Contractor contractor)
+ {
+ _contractor = contractor;
+
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(ResponseResult message, CancellationToken ct = default)
+ => _contractor.Update(message, ct);
+}<
\ No newline at end of file
A src/Core/NosSmooth.Comms.Core/ServerManager.cs => src/Core/NosSmooth.Comms.Core/ServerManager.cs +159 -0
@@ 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;
+
+/// <summary>
+/// Manages a server, awaits connections, handles messages.
+/// </summary>
+public class ServerManager
+{
+ private readonly IServer _server;
+ private readonly MessageHandler _messageHandler;
+ private readonly IOptions<NosSmoothMessageSerializerOptions> _options;
+ private readonly ILogger<ServerManager> _logger;
+ private readonly ILogger<ConnectionHandler> _handlerLogger;
+ private readonly List<ConnectionHandler> _connectionHandlers;
+ private Task<Result>? _task;
+ private CancellationTokenSource? _ctSource;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ServerManager"/> class.
+ /// </summary>
+ /// <param name="server">The server to manage.</param>
+ /// <param name="messageHandler">The message handler.</param>
+ /// <param name="options">The options.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="handlerLogger">The logger for message handler.</param>
+ public ServerManager
+ (
+ IServer server,
+ MessageHandler messageHandler,
+ IOptions<NosSmoothMessageSerializerOptions> options,
+ ILogger<ServerManager> logger,
+ ILogger<ConnectionHandler> handlerLogger
+ )
+ {
+ _server = server;
+ _connectionHandlers = new List<ConnectionHandler>();
+ _messageHandler = messageHandler;
+ _options = options;
+ _logger = logger;
+ _handlerLogger = handlerLogger;
+ }
+
+ /// <summary>
+ /// Run the manager and await the task.
+ /// </summary>
+ /// <param name="stopToken">The token used for stopping the handler and disconnecting the connection.</param>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public Task<Result> RunManagerAsync(CancellationToken stopToken)
+ {
+ StartManager(stopToken);
+ return _task!;
+ }
+
+ /// <summary>
+ /// Broadcast the given message to all clients.
+ /// </summary>
+ /// <param name="message">The message to broadcast.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <typeparam name="TMessage">The type of the message.</typeparam>
+ /// <returns>A result that may or may not have succeeded.</returns>
+ public async Task<Result> BroadcastAsync<TMessage>(TMessage message, CancellationToken ct = default)
+ {
+ var errors = new List<IResult>();
+ foreach (var handler in _connectionHandlers)
+ {
+ var result = await handler.SendMessageAsync<TMessage>(message, ct);
+ if (!result.IsSuccess)
+ {
+ errors.Add(Result.FromError(result));
+ }
+ }
+
+ return errors.Count switch
+ {
+ 0 => Result.FromSuccess(),
+ 1 => (Result)errors[0],
+ _ => new AggregateError(errors)
+ };
+ }
+
+ /// <summary>
+ /// Run the handler without awaiting the task.
+ /// </summary>
+ /// <param name="stopToken">The token used for stopping the handler and disconnecting the connection.</param>
+ public void StartManager(CancellationToken stopToken = default)
+ {
+ if (_task is not null)
+ {
+ return;
+ }
+
+ _ctSource = CancellationTokenSource.CreateLinkedTokenSource(stopToken);
+ _task = ManagerTask();
+ }
+
+ /// <summary>
+ /// Request stop the server.
+ /// </summary>
+ public void RequestStop()
+ {
+ _ctSource?.Cancel();
+ }
+
+ private async Task<Result> 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<IResult> errors = new List<IResult>();
+ 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