From 59f89c10d3b19d2dff02b651d893181519a7f3e0 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:50 +0100 Subject: [PATCH] feat: add named pipes implementation --- .../Extensions/ServiceCollectionExtensions.cs | 41 ++++++ .../NamedPipeClient.cs | 64 +++++++++ .../NamedPipeServer.cs | 125 ++++++++++++++++++ .../NosSmooth.Comms.NamedPipes.csproj | 13 ++ 4 files changed, 243 insertions(+) create mode 100644 src/Core/NosSmooth.Comms.NamedPipes/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Core/NosSmooth.Comms.NamedPipes/NamedPipeClient.cs create mode 100644 src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs create mode 100644 src/Core/NosSmooth.Comms.NamedPipes/NosSmooth.Comms.NamedPipes.csproj diff --git a/src/Core/NosSmooth.Comms.NamedPipes/Extensions/ServiceCollectionExtensions.cs b/src/Core/NosSmooth.Comms.NamedPipes/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..f6a6b32 --- /dev/null +++ b/src/Core/NosSmooth.Comms.NamedPipes/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,41 @@ +// +// 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 System.IO.Pipes; +using Microsoft.Extensions.DependencyInjection; +using NosSmooth.Comms.Data; + +namespace NosSmooth.Comms.NamedPipes.Extensions; + +/// +/// Extension methods for . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds a named pipe client as a . + /// + /// The service collection. + /// A function for resolving the name of the pipe using service provider. + /// The same service collection. + public static IServiceCollection AddNamedPipeClient + (this IServiceCollection serviceCollection, Func pipeNameResolver) + => serviceCollection + .AddSingleton(p => new NamedPipeClient(pipeNameResolver(p))) + .AddSingleton(p => p.GetRequiredService()); + + /// + /// Adds a named pipe server as a . + /// + /// The service collection. + /// A function for resolving the name of the pipe using service provider. + /// The same service collection. + public static IServiceCollection AddNamedPipeServer + (this IServiceCollection serviceCollection, Func pipeNameResolver) + => serviceCollection + .AddSingleton(p => new NamedPipeServer(pipeNameResolver(p))) + .AddSingleton(p => p.GetRequiredService()); +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeClient.cs b/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeClient.cs new file mode 100644 index 0000000..d806ddf --- /dev/null +++ b/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeClient.cs @@ -0,0 +1,64 @@ +// +// NamedPipeClient.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 System.IO.Pipes; +using NosSmooth.Comms.Data; +using Remora.Results; + +namespace NosSmooth.Comms.NamedPipes; + +/// +/// A client using named pipes. +/// +public class NamedPipeClient : IClient +{ + private readonly NamedPipeClientStream _stream; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the pipe. + public NamedPipeClient(string pipeName) + { + _stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + } + + /// + public ConnectionState State => _stream.IsConnected ? ConnectionState.Open : ConnectionState.Closed; + + /// + public Stream ReadStream => _stream; + + /// + public Stream WriteStream => _stream; + + /// + public void Disconnect() + { + _stream.Close(); + _stream.Dispose(); + } + + /// + public async Task ConnectAsync(CancellationToken ct) + { + try + { + await _stream.ConnectAsync(ct); + return Result.FromSuccess(); + } + catch (OperationCanceledException) + { + // ignored + return Result.FromSuccess(); + } + catch (Exception e) + { + return e; + } + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs b/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs new file mode 100644 index 0000000..ec07069 --- /dev/null +++ b/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs @@ -0,0 +1,125 @@ +// +// NamedPipeServer.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 System.IO.Pipes; +using NosSmooth.Comms.Data; +using Remora.Results; + +namespace NosSmooth.Comms.NamedPipes; + +/// +/// A server using named pipes. +/// +public class NamedPipeServer : IServer +{ + private readonly List _connections; + private readonly ReaderWriterLockSlim _readerWriterLock; + private readonly string _pipeName; + private bool _listening; + + /// + /// Initializes a new instance of the class. + /// + /// The name of the pipe. + public NamedPipeServer(string pipeName) + { + _readerWriterLock = new ReaderWriterLockSlim(); + _pipeName = pipeName; + _connections = new List(); + } + + /// + public IReadOnlyList Clients + { + get + { + _readerWriterLock.EnterReadLock(); + var connections = new List(_connections); + _readerWriterLock.ExitReadLock(); + return connections.AsReadOnly(); + } + } + + /// + public async Task> WaitForConnectionAsync(CancellationToken ct = default) + { + if (!_listening) + { + throw new InvalidOperationException("The server is not listening."); + } + + var serverStream = new NamedPipeServerStream + ( + _pipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous + ); + + await serverStream.WaitForConnectionAsync(ct); + + var connection = new NamedPipeConnection(this, serverStream); + _readerWriterLock.EnterWriteLock(); + _connections.Add(connection); + _readerWriterLock.ExitWriteLock(); + + return connection; + } + + /// + public Task ListenAsync(CancellationToken stopToken = default) + { + _listening = true; + stopToken.Register(Close); + return Task.FromResult(Result.FromSuccess()); + } + + /// + public void Close() + { + _readerWriterLock.EnterReadLock(); + var connections = new List(_connections); + _readerWriterLock.ExitReadLock(); + + foreach (var connection in connections) + { + connection.Disconnect(); + } + + _listening = false; + } + + private class NamedPipeConnection : IConnection + { + private readonly NamedPipeServer _server; + private readonly NamedPipeServerStream _serverStream; + + public NamedPipeConnection(NamedPipeServer server, NamedPipeServerStream serverStream) + { + _server = server; + _serverStream = serverStream; + } + + public ConnectionState State { get; private set; } = ConnectionState.Open; + + public Stream ReadStream => _serverStream; + + public Stream WriteStream => _serverStream; + + public void Disconnect() + { + _serverStream.Disconnect(); + _serverStream.Close(); + State = ConnectionState.Closed; + + _server._readerWriterLock.EnterWriteLock(); + _server._connections.Remove(this); + _server._readerWriterLock.ExitWriteLock(); + } + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.NamedPipes/NosSmooth.Comms.NamedPipes.csproj b/src/Core/NosSmooth.Comms.NamedPipes/NosSmooth.Comms.NamedPipes.csproj new file mode 100644 index 0000000..62eb048 --- /dev/null +++ b/src/Core/NosSmooth.Comms.NamedPipes/NosSmooth.Comms.NamedPipes.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + -- 2.48.1