Code Monkey home page Code Monkey logo

h.pipes's Introduction

Async Named Pipe Wrapper for .NET Standard 2.0

Language License Requirements Build Status

A simple, easy to use, strongly-typed, async wrapper around .NET named pipes.

Features

  • Create named pipe servers that can handle multiple client connections simultaneously.
  • Send strongly-typed messages between clients and servers: any serializable .NET object can be sent over a pipe and will be automatically serialized/deserialized, including cyclical references and complex object graphs.
  • Async
  • Requires .NET Standard 2.0
  • Supports large messages - up to 300 MiB.
  • Server restart automatically
  • Automatically wait for the release of the pipe for the server, if it is already in use
  • Automatically waiting for a server pipe creating when client connecting
  • Automatic reconnect with a given interval and at each client.WriteAsync, if necessary
  • Supports variable formatters, default - BinaryFormatter which uses System.Runtime.Serialization.BinaryFormatter inside
  • Also available ready formatters in separate nuget packages: H.Formatters.Newtonsoft.Json, H.Formatters.System.Text.Json and H.Formatters.Ceras
  • Supports PipeAccessRule's(see H.Pipes.AccessControl nuget package) or more complex code to access using the PipeServer.PipeStreamInitializeAction property

Nuget

NuGet NuGet NuGet NuGet NuGet

// All clients and servers that do not need support AccessControl.
Install-Package H.Pipes

// Servers that need support AccessControl.
Install-Package H.Pipes.AccessControl

// If you want to transfer any data that can be serialized/deserialized in json using Newtonsoft.Json.
Install-Package H.Formatters.Newtonsoft.Json

// If you want to transfer any data that can be serialized/deserialized in json using System.Text.Json.
Install-Package H.Formatters.System.Text.Json

// If you want to transfer any data that can be serialized/deserialized in binary using Ceras.
Install-Package H.Formatters.Ceras

Usage

Server:

await using var server = new PipeServer<MyMessage>(pipeName);
server.ClientConnected += async (o, args) =>
{
    Console.WriteLine($"Client {args.Connection.PipeName} is now connected!");

    await args.Connection.WriteAsync(new MyMessage
    {
        Text = "Welcome!"
    });
};
server.ClientDisconnected += (o, args) =>
{
    Console.WriteLine($"Client {args.Connection.PipeName} disconnected");
};
server.MessageReceived += (sender, args) =>
{
    Console.WriteLine($"Client {args.Connection.PipeName} says: {args.Message}");
};
server.ExceptionOccurred += (o, args) => OnExceptionOccurred(args.Exception);

await server.StartAsync();

await Task.Delay(Timeout.InfiniteTimeSpan);

Client:

await using var client = new PipeClient<MyMessage>(pipeName);
client.MessageReceived += (o, args) => Console.WriteLine("MessageReceived: " + args.Message);
client.Disconnected += (o, args) => Console.WriteLine("Disconnected from server");
client.Connected += (o, args) => Console.WriteLine("Connected to server");
client.ExceptionOccurred += (o, args) => OnExceptionOccurred(args.Exception);

await client.ConnectAsync();

await client.WriteAsync(new MyMessage
{
    Text = "Hello!",
});

await Task.Delay(Timeout.InfiniteTimeSpan);

Notes:

  • To use the server inside the WinForms/WPF/Other UI application, use Task.Run() or any alternative.
  • Be careful and call Dispose before closing the program/after the end of use. Pipes are system resources and you might have problems restarting the server if you don't properly clean up the resources.

Custom Formatters

Since BinaryFormatter is used by default, you should check out this article: https://docs.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide

Install-Package H.Formatters.Newtonsoft.Json
Install-Package H.Formatters.System.Text.Json
Install-Package H.Formatters.Ceras
using H.Formatters;

await using var server = new PipeServer<MyMessage>(pipeName, formatter: new NewtonsoftJsonFormatter());
await using var client = new PipeClient<MyMessage>(pipeName, formatter: new NewtonsoftJsonFormatter());

Access Control

Warning

this is only available for the Windows platform.

Install-Package H.Pipes.AccessControl
using System.IO.Pipes;
using H.Pipes.AccessControl;

await using var server = new PipeServer<string>(pipeName);

// You can set PipeSecurity
var pipeSecurity = new PipeSecurity();
pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), PipeAccessRights.ReadWrite, AccessControlType.Allow));

server.SetPipeSecurity(pipeSecurity);

// or just add AccessRule's (Please be careful, the server will only consider AccessRules from the last call AddAccessRules())
server.AddAccessRules(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), PipeAccessRights.ReadWrite, AccessControlType.Allow));

// or just
server.AllowUsersReadWrite();

Encryption

Warning

this is only available for the Windows platform.

Install-Package H.Formatters.Inferno
using H.Formatters;

await using var server = new PipeServer<MyMessage>(pipeName, formatter: new SystemTextJsonFormatter());
server.EnableEncryption();

await using var client = new PipeClient<MyMessage>(pipeName, formatter: new SystemTextJsonFormatter());
client.EnableEncryption();

await client.ConnectAsync(source.Token).ConfigureAwait(false);
// Waits for key exchange.
await client.Connection!.WaitExchangeAsync();

server.ClientConnected += async (_, args) =>
{
    // Waits for key exchange.
    await args.Connection.WaitExchangeAsync();

    await args.Connection.WriteAsync(new MyMessage
    {
        Text = "Welcome!"
    }, source.Token).ConfigureAwait(false);
};

GetImpersonationUserName

server.ClientConnected += async (o, args) =>
{
    var name = args.Connection.GetImpersonationUserName();

    Console.WriteLine($"Client {name} is now connected!");
};

Inter-process communication

I recommend that you take a look at my other library if you plan on doing IPC. This is a SourceGenerator that will generate client and server code based on the presented interface.

Contacts

h.pipes's People

Contributors

dependabot[bot] avatar havendv avatar zbalkan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

h.pipes's Issues

Generic message type <T>

I have noticed that all the read/write APIs can only read and write the generic type defined by the user when instantiating the client and server, eg. PipeServer<MyMessage>, PipeClient<MyMessage>.

Would you agree to a PR with the following changes:

  • PipeClient, PipeServer, SingleConnectionPipeServer, SingleConnectionPipeClient would have a generic and non-generic variant
  • The non-generic variant would be the base class for the generic variant to avoid code duplication
  • The non-generic variant would expose the following methods for writing:
    • void WriteAsync<T>(T value, [...])
    • void WriteAsync(byte[] value, [...])
  • The non-generic variant would expose the following event for receiving data event EventHandler<ConnectionMessageEventArgs<byte[]>>? MessageReceived;
  • The generic variant would be the same as the current PipeXXX
  • One of the following:
    • SingleConnectionPipeXXX and PipeXXX appear to share a lot of code. They could inherit an abstract class for all the code they have in common
    • Merge SingleConnectionPipeXXX and PipeXXX by adding a bool isSingleConnection argument to the constructor
  • Optionally make the classes non-sealed (more on that below)

In essence we would have something like that:

Avoiding code duplication between SingleConnectionPipeXXX and PipeXXX

Option 1: Base class

public abstract BasePipeServer : IPipeServer
{
  // Put all code shared by PipeServer and SingleConnectionPipeServer here
}
public abstract BasePipeClient : IPipeClient
{
  // Put all code shared by PipeClient and SingleConnectionPipeClient here
}

Option 2: Merge them

We delete the SingleConnectionPipeXXX classes and add an argument in the constructor:

public PipeServer(string pipeName, bool isSingleConnection = false, IFormatter? formatter = default)
public PipeClient(string pipeName, string serverName = ".", bool isSingleConnection = false, TimeSpan? reconnectionInterval = default, IFormatter? formatter = default)

Non-generic PipeXXX classes

public class PipeServer: BasePipeServer
{
  public event EventHandler<ConnectionMessageEventArgs<byte[]>>? MessageReceived;

  public async Task WriteAsync<T>(T value, CancellationToken cancellationToken = default)
  { /* Code here */ }

  public async Task WriteAsync(byte[] value, CancellationToken cancellationToken = default)
  { /* Code here */ }
}
public class PipeClient: BasePipeClient
{
  public event EventHandler<ConnectionMessageEventArgs<byte[]>>? MessageReceived;

  public async Task WriteAsync<T>(T value, CancellationToken cancellationToken = default)
  { /* Code here */ }

  public async Task WriteAsync(byte[] value, CancellationToken cancellationToken = default)
  { /* Code here */ }
}

Repeat for SingleConnectionPipeXXX.

Generic PipeXXX classes

public class PipeServer<T>: PipeServer, IPipeServer<T>
{
  public new event EventHandler<ConnectionMessageEventArgs<T?>>? MessageReceived;

  public new async Task WriteAsync(T value, CancellationToken cancellationToken = default)
  { /* Code here */ }
}
public class PipeClient<T>: PipeClient, IPipeClient<T>
{
  public new event EventHandler<ConnectionMessageEventArgs<T?>>? MessageReceived;

  public new async Task WriteAsync(T value, CancellationToken cancellationToken = default)
  { /* Code here */ }
}

Repeat for SingleConnectionPipeXXX.

Optional: Sealed classes

Optionally, it might be nice to make the PipeServer<T> and PipeClient<T> non-sealed for people who want to extend their functionalities without requiring a fork or a PR.

Optional: Sharing code between PipeServer and PipeClient

PipeServer and PipeClient also appear to share a lot of code. I could make a base class for them to avoid code duplication.

Final words

I'll be writing this for my own needs, but I would enjoy contributing to the project if possible with a PR. Let me know what you think! :)

Different Send and Receive Types Info Request

I was wondering if there is a configuration I'm missing somewhere. Most examples seem to show using the same generic type for both sending and receiving. Typically though you rarely would receive an object of the same type as the one you are sending. Is there a way to configure H.Pipes to use different types. My current workaround is just including a string in the message that I then deserialize later, but this takes a way a lot of the value that this library adds as its not super hard just to do that to the standard pipe stream without adding another dependency to a project.

Identify which user is connected

Hi,

In System.IO.Pipes there is a function "GetImpersonationUserName()" for getting the username of the user connecting.
Have you looked into adding similar capabilities?

This would need to be handled server side to avoid the possibility of a user impersonating another user.

Couldn't send data in specflow testing application via pipes

Describe the bug

While I am H Pipes in specflow, c# and nunit Framework for testing the pipe connection and passing data using pipes.

image

The WriteAsync function is called but it's not sending the data to the client application.

Steps to reproduce the bug

  1. create specflow feature file
  2. use the below coding for testing

private PipeServer? _server;

[When(@"Get Sending Data")]
public async Task WhenSendingData()
{

 do
 {
     
    Task.Run(() => StartServer());

 } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

}

public async Task StartServer()
{

 while (!_token.IsCancellationRequested && _server is not { IsStarted: true })
 {
     try
     {
       
         _server = new PipeServer<string>("TestPipe");

         _server.ClientConnected += OnClientConnected;

         await _server.StartAsync(_token.Token);
     }
     catch (Exception ex)
     {
         Console.WriteLine("Failed to start server");
     }
 }

}

public void OnClientConnected(object? sender, ConnectionEventArgs e)
{
Console.WriteLine($"Client has connected to {e.Connection.PipeName}");
Console.WriteLine($"Client has connected to {e.Connection}");

     e.Connection.WriteAsync("Welcome!");

}

  1. Run the test

There is no bug but it's not communicating. If i close the c++ execution, it's giving me pipe closed error. The id for Pipe is also created

Test.SignalPipe_ba006207-cdaf-4365-99b8-a06258d3c65e

Please not the same is working well in console application where I could send message but not working in specflow testing application.

Expected behavior

No response

Screenshots

No response

NuGet package version

<PackageReference Include="H.Formatters.BinaryFormatter" Version="2.0.59" />
<PackageReference Include="H.Pipes" Version="2.0.59" />
<PackageReference Include="H.Pipes.AccessControl" Version="2.0.59" />

Platform

Console

IDE

Visual Studio 2022

Additional context

No response

Missing resiliency options

Hi,

I was trying to implement the H.Pipes thanks to the blog of Erik Engberg's blog post. This library saved me from a huge blocker.

Currently, I am trying to implement a timeout, retry and circuit breaker policy. My option was to track the time and cancel the ConnectAsync() method. I used the Microsoft documentation on CancellationTokenSource.

However, I believe this needs to be handled as if HTTP requests are handled via Polly. In that case, it can be done either by allowing Polly to handle the resiliency policies or by adding those policies into the H.Pipes. directly. In either case, it requires a TimeOut exception, so that either Polly or the pipe itself can handle the retries and possible circuit breaking.

Initially, I planned to do it myself and send a PR but I just followed the docs and moved on due to project deadlines. I hope we can see these policies in H.Pipes.

Edit: The piece of code below is based on the example, in the class NamedPipeClient.cs. It includes only timeout. I did not try to add retry yet.

        private static readonly CancellationTokenSource Cts = new();
        private readonly int _cancellationTimeout;

        public NamedPipeClient(int timeout = 500)
        {
            _cancellationTimeout = timeout;
            //...
        }

        public async Task InitializeAsync()
        {
           // This try/catch/finally might be included in the library. 
           // So ctor parameter would suffice
            try
            {
                Cts.CancelAfter(_cancellationTimeout); 
                await _client.ConnectAsync(Cts.Token);

                await _client.WriteAsync(new PipeMessage
                {
                    Action = ActionType.SendText,
                    Text = "Hello from client",
                }, Cts.Token);
            }
            catch (TaskCanceledException)
            {
                Debug.WriteLine($"Task cancelled after timeout {_cancellationTimeout}.");
                Debug.WriteLine(e.Message);
               ///...
            }
            finally
            {
               // this is tricky. If the Cts will live throughout the lifetime of the class, 
               // this should be in the Dispose method of the class, not here
                Cts.Dispose();
            }
        }

Does this library support read-only or write-only?

CreatePipeStreamFunc = (pipeName) => new NamedPipeServerStream(pipeName, PipeDirection.Out, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough);

When customizing a NamedPipeServerStream, the client connects and prompts that reading the stream is not supported.

Cannot access a disposed object.

Describe the bug

When my client was disconnected it throws exception
My usage:

    private readonly IPipeClient<PipeMessage> _client;

    private async void ClientOnDisconnected(object? _, ConnectionEventArgs<PipeMessage> e)
    {
        _logger.LogInformation("IPC client name {name} was disconnected", e.Connection.PipeName);
        while (!_client.IsConnected)
        {
            using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(3));
            try
            {
                await Start(cancellationTokenSource.Token).ConfigureAwait(false);
            }
            catch (Exception exception)
            {
                _logger.LogWarning(exception,"Failed to reconnect");
            }
        }
    }

    public async Task Start(CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("Connecting to IPC {pipeName}", _client.PipeName);
        await _client.ConnectAsync(cancellationToken);
    }

Steps to reproduce the bug

Just stop IPC server

System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Timers.Timer'.
at void System.Timers.Timer.set_Enabled(bool value)
at async Task H.Pipes.PipeClient.ConnectAsync(CancellationToken cancellationToken) in //src/libs/H.Pipes/PipeClient.cs:line 173
at async Task Ipc.Client.BaseIpcClient.Start(CancellationToken cancellationToken) in /
/src/Ipc/Client/BaseIpcClient.cs:line 141
at async void Ipc.Client.BaseIpcClient.ClientOnDisconnected(object , ConnectionEventArgs e) in //src/Ipc/Client/BaseIpcClient.cs:line 26

Expected behavior

No response

Screenshots

No response

NuGet package version

2.0.59

Platform

Console

IDE

Visual Studio 2022

Additional context

No response

ConsoleApp sample server generates an exception with .NET 8.0

Describe the bug

I have cloned the repo and tried to run the ConsoleApp sample.
By default, .NET 8.0 is selected.

When the client connects to the server it generates the exception:

ConsoleApp.MyMessage is not registered in resolver: MessagePack.Resolvers.StandardResolver

The complete message is

Client named_pipe_test_server_021d8e5e-1e5d-4d21-9abd-4cba84e491db is now connected!
Exception: MessagePack.MessagePackSerializationException: Failed to serialize System.Object value.
 ---> MessagePack.FormatterNotRegisteredException: ConsoleApp.MyMessage is not registered in resolver: MessagePack.Resolvers.StandardResolver
   at MessagePack.FormatterResolverExtensions.Throw(Type t, IFormatterResolver resolver)
   at MessagePack.FormatterResolverExtensions.GetFormatterDynamicWithVerify(IFormatterResolver resolver, Type type)
   at MessagePack.Formatters.DynamicObjectTypeFallbackFormatter.Serialize(MessagePackWriter& writer, Object value, MessagePackSerializerOptions options)
   at MessagePack.MessagePackSerializer.Serialize[T](MessagePackWriter& writer, T value, MessagePackSerializerOptions options)
   --- End of inner exception stack trace ---
   at MessagePack.MessagePackSerializer.Serialize[T](MessagePackWriter& writer, T value, MessagePackSerializerOptions options)
   at MessagePack.MessagePackSerializer.Serialize[T](T value, MessagePackSerializerOptions options, CancellationToken cancellationToken)
   at H.Formatters.MessagePackFormatter.SerializeInternal(Object obj) in C:\GitHub\H.Pipes\src\libs\H.Formatters.MessagePack\MessagePackFormatter.cs:line 12
   at H.Formatters.FormatterBase.Serialize(Object obj) in C:\GitHub\H.Pipes\src\libs\H.Formatters\FormatterBase.cs:line 28
   at H.Pipes.PipeConnection`1.WriteAsync(T value, CancellationToken cancellationToken) in C:\GitHub\H.Pipes\src\libs\H.Pipes\PipeConnection.cs:line 162
   at ConsoleApp.MyServer.<>c__DisplayClass1_0.<<RunAsync>b__0>d.MoveNext() in C:\GitHub\H.Pipes\src\samples\ConsoleApp\MyServer.cs:line 30

The error does not occur with .NET 6.0 or .NET 7.0

Steps to reproduce the bug

  1. In the project ConsoleApp, select .NET 8.0. This is the default.
  2. Compile the solution
  3. Start the ConsoleApp and enter SERVER
  4. Start the ConsoleApp and enter CLIENT
  5. At this point the client connects to the server and the server generates an exception

Expected behavior

No response

Screenshots

No response

NuGet package version

No response

Platform

Console

IDE

Visual Studio 2022

Additional context

No response

PipeServer crashing with many connections, IOException: 'Pipe is broken'

Describe the bug

I have a program with a client that sends a message and waits for a response, and a server that waits for clients, and when one sends a message, the server sends a response.

Sometimes the server crashes when it tries to send a response with args.Connection.WriteAsync
image

   at System.IO.Pipes.PipeStream.CheckWriteOperations()
   at System.IO.Pipes.PipeStream.Flush()
   at System.IO.Pipes.PipeStream.FlushAsync(CancellationToken cancellationToken)
--- End of stack trace from previous location ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at H.Pipes.IO.PipeStreamWriter.<WriteAsync>d__8.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at H.Pipes.IO.PipeStreamWrapper.<WriteAsync>d__17.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at H.Pipes.PipeConnection`1.<WriteAsync>d__38.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at Program.<>c.<<<Main>$>b__0_2>d.MoveNext() in C:\Users\Erez\source\repos\HPipeMultiClient\Server\Server.cs:line 21

Happens when running the server in debug from Visual Studio. Doesn't happen when running the server from console.

Steps to reproduce the bug

  1. In Visual Studio 2022, Open this minimal solution that reproduces the problem: https://github.com/erezwanderman/HPipeMultiClient
  2. Build the solution and start debugging (the Server project)
  3. Start Client.exe outside of Visual Studio
  4. Watch the server crash on line 21

server.MessageReceived += async (sender, args) =>
{
Console.WriteLine($"Client {args.Connection.PipeName} says: {args.Message}");
var response = new Message { Response = args.Message!.Request + args.Message!.Request };
Console.WriteLine($"Sending to {args.Connection.PipeName}: {response}");
await args.Connection.WriteAsync(response);
};

Expected behavior

No crash, send message successfully

Screenshots

image

NuGet package version

2.0.37

Platform

Console

IDE

Visual Studio 2022

Additional context

No response

CryptoFormatter

A wrapper for other formatter that encrypts/decrypts data

At the moment, my knowledge is enough only to implement the simplest XOR encryption, which makes little sense

I can't transmit a simple message [question]

I'm having trouble moving a simple message (my first step with this library).
This is a minimal new .NET Framework 4.7.2 project (Desktop Application) with nothing only Load event for form.
I only added libraries: H.Pipes and H.Formatters.System.Text.Json
This is the entire form code.

using H.Formatters;
using H.Pipes;
using System;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Test1
{
    public partial class Frm_Test : Form
    {
        private PipeServer<MyMessage> server;
        private PipeClient<MyMessage> client;
        private string pipeName = "a";
        private string test = "THIS IS TEST MESSAGE";

        public Frm_Test() { InitializeComponent(); }

        private void Frm_Test_Load(object sender, EventArgs e)
        {
            server = new PipeServer<MyMessage>(pipeName, formatter: new SystemTextJsonFormatter());
            server.MessageReceived += (c, args) =>
            {
                Console.WriteLine("Received: " + args.Message.Text);
                if (args.Message.Text != test) { Console.WriteLine("WRONG MESSAGE"); }
            };
            Task.Run(async () => { await server.StartAsync(); });

            client = new PipeClient<MyMessage>(pipeName, formatter: new SystemTextJsonFormatter());
            client.Connected += (o, args) =>
            {
                Console.WriteLine("Sending: " + test);
                client.WriteAsync(new MyMessage { Text = test });
            };
            client.ConnectAsync();
        }
    }

    public class MyMessage { public string Text; }
}

Console output is like this (message.Text is always empty):

Sending: THIS IS TEST MESSAGE
Received: 
WRONG MESSAGE

I get the same problem even with two separated client-server applications.
Please advise me where I am making a mistake?

Handles are not disposed

Describe the bug

Not destroyed handles after a new connection is once used (writeasynch) and disposed. It leads to OutOfMemoryException after many iterations in the same process.

Steps to reproduce the bug

  1. Create a new pipe client:
    _client = new PipeClient(uri, ".", null);
    _client.ConnectAsync().Wait(30);
    _client.ExceptionOccurred += Client_ExceptionOccurred;
    _client.MessageReceived += Client_MessageReceived;

  2. Write sometihng to server process:
    _client.WriteAsync(messageObject).Wait();

  3. Dispose client after all done:
    _client.ExceptionOccurred -= Client_ExceptionOccurred;
    _client.MessageReceived -= Client_MessageReceived;
    _client.DisconnectAsync().Wait();
    _client.DisposeAsync();
    _client = null;

  4. Iterate 1000 times -> there are about 1000 more handles in the process. Note: if you omit step 2 - everything works clean.

Expected behavior

No additional handles after 1000 iterations within the same client process

Screenshots

No response

NuGet package version

1.14.8

Platform

Console

IDE

Visual Studio 2022, Visual Studio 2019

Additional context

No response

MessagePack Unions not serialized correctly.

Describe the bug

I have several classes defined as a Union, using the MessagePack.Union attribute.
See https://github.com/MessagePack-CSharp/MessagePack-CSharp?tab=readme-ov-file#union

I had no problem with .NET 6, but with .NET 8 I find that the Union classes are not serialized correctly,

So far as I can tell, it is necessary to provide a type parameter to MessagePackSerializer.Serialize(), specifying the base class used for the union.

I have defined my own formatter class

  public class MessagePackFormatterT<T1> : FormatterBase
    where T1 : class
  {
    protected override byte[] SerializeInternal( object obj )
    {
      return MessagePackSerializer.Serialize<T1> ( obj as T1 );
    }

    protected override T DeserializeInternal<T>( byte[] bytes )
    {
      return MessagePackSerializer.Deserialize<T> ( bytes );
    }
  }

which I specify with both PipeClient and the PipeSever

_client = new PipeClient<MyBase> ( PipeName, formatter: new MessagePackFormatterT<MyBase>() ) ;
_server = new PipeServer<MyBase> ( PipeName, new MessagePackFormatterT<MyBase>() );

In my application, this seems to have fixed the problem, but I'm not sure that it is a general solution.

Steps to reproduce the bug

Define a set of classes using MessageBase.Union, following the example in
https://github.com/MessagePack-CSharp/MessagePack-CSharp?tab=readme-ov-file#union

I defined the base class as an abstract class, not an interface.

Create a PipeServer an a PipeClient based on the union class.

Create an instance of one of the derived classes.

Send it via the PipeServer.

On receiving the message, the PipeClient will generate an error.

Expected behavior

No response

Screenshots

No response

NuGet package version

No response

Platform

Console

IDE

Visual Studio 2022

Additional context

.NET 8

Application getting terminated

Description: The process was terminated due to an unhandled exception.
Exception Info: System.IO.FileLoadException
at System.Threading.Tasks.ValueTask.get_IsCompleted()
at H.Pipes.PipeClient1+<DisconnectInternalAsync>d__48[[System.__Canon, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]].MoveNext() at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) at H.Pipes.PipeClient1+<b__46_0>d[[System.__Canon, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]].MoveNext()
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
at System.Threading.ThreadPoolWorkQueue.Dispatch()

The server stops working

Describe the bug

I am looking for a reliable named pipes server for communication between C ++ and C #.
Unfortunately, the H.Pipes server can easily crash due to an error in the client.
Here's an example code that shows how to make the server stop responding to new connections:

Steps to reproduce the bug

  1. Run ConsoleApp from samples as server
  2. Connect to server by the following code:
using System;
using System.IO.Pipes;
using System.Net;
using System.Threading;

namespace HPipeError
{
  internal class Program
  {
    private static void Main(string[] args)
    {
      var pipeClient = new NamedPipeClientStream("named_pipe_test_server");
      while (!pipeClient.IsConnected)
      {
        pipeClient.Connect();
        Thread.Sleep(100);
      }
      // read string length
      var buffer = new byte[sizeof(int)];
      pipeClient.Read(buffer, 0, sizeof(int));
      var len = IPAddress.NetworkToHostOrder(BitConverter.ToInt32(buffer, 0));
      // read string content
      buffer = new byte[len];
      pipeClient.Read(buffer, 0, len);
    }
  }
}
  1. After executing this code, try to connect to the server (e.g. using SampleApp as a client). It will be impossible.

Expected behavior

No response

Screenshots

No response

NuGet package version

No response

Platform

No response

IDE

No response

Additional context

Why is this happening?

After establishing the connection by the client, the server sends the name of the pipe created for the client through the main pipe.
The server then waits for the client to establish a connection to the newly created pipe, but unfortunately the main pipe is closed.
If the client fails to connect, the main pipe will not be recreated and you will not be able to connect to the server anymore.
So one malfunctioning client can therefore block the server.

Feature request: Have a connection identifier on the server based on a client connection

When establishing a connection it would be nice if one could add client metadata to the connection like a Connection Identifier (CID).
At the moment it's hard to identify a client connection on the server side. At the moment we make this part of the message but if the server gets a client message and needs to "send updates to a specific other" connected client then having a CID would be good.

await using var client = new PipeClient(pipeName) {
ClientMeta = new ClientMeta() { MachineName=Enviroment.MachineName, Role="MyRole", ...}
};

One could add the CID in the server connection handshake, the service already maintains a state for connected clients

When implementing something like this, you could do:
await server.ConnectedClients.Where(w=>s.MetaData.Role=="SomeRole").SendMsgAsync(..)

Stream access from Connection object

Sometimes need to transfer large files. Right now PipeConnection does not provide the ability to work with Stream. It means a response side should upload part of files into memory.

Pipe name handling on client side

The line below adds a number for the given pipe name in specific cases.

var connectionPipeName = $"{PipeName}_{++NextPipeId}";

In my case, a long-running windows service acts as the named pipe server. After a while, the numbering became an issue and I missed that part. I thought it is my code. I realized the issue is by design thanks to pipelist tool of Sysinternals. Then, I checked if it is an OS thin or the library and found the line above.

On the client side, I solved the issue by enumerating the named pipes and picking the first one that starts with the specified name.

Helper class with enumeration method.

public static class PipeHelper
    {
        /// <summary>
        ///     Enumerates the named pipes excluding the ones with illegal names.
        /// </summary>
        /// <remarks>
        ///     Pipes might have illegal characters in path. Seen one from IAR containing < and >.
        ///     The FileSystemEnumerable.MoveNext source code indicates that another call to MoveNext will return
        ///     the next entry.
        ///     Pose a limit in case the underlying implementation changes somehow. This also means that no more
        ///     than 10 (default) pipes with bad names may occur in sequence.
        /// </remarks>
        /// <param name="retryCount"> If there are more illegal named pipes consequently then this param, it stops enumerating. </param>
        /// <see cref="https://stackoverflow.com/a/53432640/5910839"/>
        /// <returns>named pipe names</returns>
        public static IEnumerable<string> EnumeratePipes(int retryCount = 10)
        {
            using var enumerator = Directory.EnumerateFiles(@"\\.\pipe\").GetEnumerator();
            while (MoveNextSafe(enumerator, retryCount))
            {
                yield return enumerator.Current.Replace(@"\\.\pipe\", "");
            }
        }

        private static bool MoveNextSafe(IEnumerator<string> enumerator, int retryCount)
        {
            for (var i = 0; i < retryCount; i++)
            {
                try
                {
                    return enumerator.MoveNext();
                }
                catch (ArgumentException)
                {
                }
            }
            return false;
        }
    }

And on the named pipe client:

...
public class NamedPipeClient : IAsyncDisposable
    {
        private const string PipeName = "MyPipe";

        private readonly PipeClient<PipeMessage> _client;
        ...

        public NamedPipeClient(...)
        {
            var pipe = PipeHelper.EnumeratePipes().First(p => p.StartsWith(PipeName, StringComparison.InvariantCulture));
            if (string.IsNullOrEmpty(pipe))
            {
                throw new InvalidOperationException("No pipe found.");
            }
           ...

You might consider either using it as an example in a sample project or documentation. Or you can add the method in the H.Pipes. It is just an edge case users might face.

.NET 8 support

Hi, has anyone checked the work on .NET 8? This doesn't work for me

Implementation questions

Hi, thanks for the great job. I have noticed several problems

  1. After approximately 7,000 clients contacted, 4GB of memory disappears in the system.
    My solution:
_server.ClientDisconnected += async (o, args) =>
{
    await args.Connection.DisposeAsync();
    Debug.WriteLine($"Client {args.Connection.PipeName} disconnected");
};

After that the memory ceased to disappear. This helped free up memory.

  1. After some time, the server stops responding. However, it does not log an error anywhere. To reproduce this quickly, you need to terminate the client in the middle of the connection. Thus the exception is returned from the handshakeWrapper

https://github.com/HavenDV/H.Pipes/blob/0db095207ddfabfabb4a2c74f34ece8c26962e0b/src/libs/H.Pipes/PipeServer.cs#LL182C33-L182C33

Then the break stops the while cycle. New clients can no longer connect to the server.

https://github.com/HavenDV/H.Pipes/blob/0db095207ddfabfabb4a2c74f34ece8c26962e0b/src/libs/H.Pipes/PipeServer.cs#LL195C25-L195C25

My solution:
replace break; to throw;
Question: How reasonable was the break in this place? Is my fix correct or should this cycle be created per client?

  1. I looked at this commit 0db0952 here I have doubts about the correct usage System.Memory. I'd rather not fill head with newfangled classes and conditions, instead add a warning to Suppress CA1835 message. And then use a single codebase, like this
    {
        var buffer = new byte[length];
        var read = await BaseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);

        if (read != length)
        {
            return throwIfReadLessThanLength
                ? throw new IOException($"Expected {length} bytes but read {read}")
                : Array.Empty<byte>();
        }

        return buffer;
    }

this code looks cleaner and easier to maintain. Same with WriteAsync.

Returned by server pipeName is null

Describe the bug

We are sporadically seeing the following exception when connecting > 1 pipeclient with pipeClient.ConnectAsync()

System.InvalidOperationException: Connection failed: Returned by server pipeName is null at H.Pipes.PipeClient1.GetConnectionPipeName(CancellationToken cancellationToken)
at H.Pipes.PipeClient1.GetConnectionPipeName(CancellationToken cancellationToken) at H.Pipes.PipeClient1.ConnectAsync(CancellationToken cancellationToken)`

Clients are connected in a relatively short time, the problem is already visible with 3 clients, but does not happen all the time.

Steps to reproduce the bug

Create pipe server
Create multiple pipe clients

Expected behavior

All pipe clients connect normally and without ab exception

Screenshots

No response

NuGet package version

Platform

Console

IDE

Other

Additional context

No response

Compiler Errors: Tests, 4.8

Describe the bug

I seem to have some compiler problems such as

Severity Code Description Project File Line Suppression State
Error CS8417 'NamedPipeClientStream': type used in an asynchronous using statement must be implicitly convertible to 'System.IAsyncDisposable' or implement a suitable 'DisposeAsync' method. Did you mean 'using' rather than 'await using'? H.Pipes.Tests (net4.8) C:\Work\Temp\H.Pipes-master\H.Pipes-master\src\tests\H.Pipes.Tests\Tests.cs 89 Active

Steps to reproduce the bug

Compile solution

Expected behavior

No response

Screenshots

No response

NuGet package version

No response

Platform

No response

IDE

No response

Additional context

No response

does H.Pipes support RPC?

I am working on building a vsix solution that requires that an object must cross from a .net framework process to net core process. When the .net framework process signals the net core process then acts and returns the results. named pipes are involved for certain and I believe RPC is involved when the object shared back and forth has method functions to set some serialized data in the remote object.

will H.Pipes accomplish this?

Exception in IPC call

Description

We have recently reproduced bug with H.Pipes.
Nuget version: 2.0.59
Our server receives many IPC messages (~1000/min) in many cases there was just high cpu usage but one of our collegues crashed IPC server

System.Text.Json.JsonException: '0x00' is an invalid start of a value. Path: $ | LineNumber: 0 | BytePositionInLine: 0.
 ---> System.Text.Json.JsonReaderException: '0x00' is an invalid start of a value. LineNumber: 0 | BytePositionInLine: 0.
   at void System.Text.Json.ThrowHelper.ThrowJsonReaderException(ref Utf8JsonReader json, ExceptionResource resource, byte nextByte, ReadOnlySpan<byte> bytes)
   at bool System.Text.Json.Utf8JsonReader.ConsumeValue(byte marker)
   at bool System.Text.Json.Utf8JsonReader.ReadFirstToken(byte first)
   at bool System.Text.Json.Utf8JsonReader.ReadSingleSegment()
   at T System.Text.Json.Serialization.JsonConverter<T>.ReadCore(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state)
   --- End of inner exception stack trace ---
   at void System.Text.Json.ThrowHelper.ReThrowWithPath(ref ReadStack state, JsonReaderException ex)
   at T System.Text.Json.Serialization.JsonConverter<T>.ReadCore(ref Utf8JsonReader reader, JsonSerializerOptions options, ref ReadStack state)
   at TValue System.Text.Json.JsonSerializer.ReadFromSpan<TValue>(ReadOnlySpan<char> json, JsonTypeInfo<TValue> jsonTypeInfo)
   at TValue System.Text.Json.JsonSerializer.Deserialize<TValue>(string json, JsonSerializerOptions options)
   at T Ipc.Client.PipeMessageJsonFormatter.DeserializeInternal<T>(byte[] bytes) in /_/src/Ipc/Client/PipeMessageJsonFormatter.cs:line 23
   at T H.Formatters.FormatterBase.Deserialize<T>(byte[] bytes) in /_/src/libs/H.Formatters/FormatterBase.cs:line 34
   at async void H.Pipes.PipeConnection<T>.Start()+(?) => { }

This is my custom json formatter

public sealed class PipeMessageJsonFormatter : SystemTextJsonFormatter
{
    protected override byte[] SerializeInternal(object? obj)
    {
        if (obj is PipeMessage message)
        {
            var serialize = JsonSerializer.Serialize<PipeMessage>(message, new JsonSerializerOptions());
            var bytes = System.Text.Encoding.UTF8.GetBytes(serialize);
            return bytes;
        }

        throw new InvalidOperationException($"Type of message {obj?.GetType()} is not supported");
    }

    protected override T DeserializeInternal<T>(byte[] bytes)
    {
        var json = System.Text.Encoding.UTF8.GetString(bytes);
        var deserialized = JsonSerializer.Deserialize<T>(json);
        return deserialized ?? throw new InvalidOperationException($"Unable to deserialize message: {json}");
    }
}

We have created process dump before crash, but I am not sure how this could happend.
I have investigated dump and there is buffer which is pretty high 1734681213 bytes. (I am able to provide dump)
In my opinion there are still incomming IPC messages, but nobody can read.

Steps to reproduce the bug

I am not sure if there is 100% way to reproduce this issue.

Expected behavior

No response

Screenshots

image

NuGet package version

2.0.59

Platform

Console

IDE

Visual Studio 2022

Additional context

No response

Large messages on .NET 6 fail to send

Describe the bug

When transferring messages larger than 65535 bytes on .NET 6 the server throws an exception stating

Expected <number of sent> bytes but read 65536

Steps to reproduce the bug

Send the contents of a 1MB file over the channel and catch the exception

Same issue whether I send a serializable object or a standard string

Expected behavior

No response

Screenshots

No response

NuGet package version

No response

Platform

Console

IDE

Visual Studio 2022

Additional context

Seems to be related to this:

var read = await BaseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);

ReadAsync length

Hello,

First of all, this is a really nice project. I'm enjoying reading your code a lot, and this will definitely save me time so thanks. :)

While familiarizing myself with H.Pipes, I came across this bit in PipeStreamReader.cs

    private async Task<byte[]> ReadAsync(int length, CancellationToken cancellationToken = default)
    {
        var buffer = new byte[length];
#if NETSTANDARD2_1 || NETCOREAPP3_1_OR_GREATER
        await BaseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
#elif NET461_OR_GREATER || NETSTANDARD2_0
        await BaseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
#else
#error Target Framework is not supported
#endif

        return buffer;
    }

Shouldn't the return value (= number of bytes read) of ReadAsync be assigned and compared with the intended message length?

var readLength = await BaseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);

if (readLength != length)
  throw new IOException($"Expected {length} bytes but read {readLength}");

some windows 10 and windows 11 clients can't connect to the PipeServer if hosted in a service

I have a service that would need to accept client data that runs without elevated permissions
I use this code

_server = new PipeServer<T>(pipeName, new NewtonsoftJsonFormatter());
_server.AllowUsersReadWrite();

My developer machine can connect just fine, some other machines however can't. If I take the NuGet package where I wrapped the PipeServer and PipeClient in and create a chat app using Winforms that same machine accepts the client without any issues, both are .net 6.0

If I take the Production client and have it connect to the Winforms test service all seems to work well.

The only difference is that one is hosted in service, the other is in a normal WinForms, with no changes made to the machines that are being problematic.

What am I doing wrong, how can I solve the issue?

Security question(s) regarding BinaryFormatter

Hi, first of all let me say thanks for you great project!

I've seen in #42 that you switched away from BinaryFormatter to SystemTextJsonFormatter.

Nevertheless, in my .NET 8 app, I'm still getting errors (as in #42) with this sample code, so I have to specify SystemTextJsonFormatter explicitly to get rid of errors:

// in server app
server = new PipeServer<PipeMessage>(pipeName, formatter: new SystemTextJsonFormatter());
// in client app
client = new PipeClient<PipeMessage>(pipeName, formatter: new SystemTextJsonFormatter());
  1. Is it expected behavior (for now) that BinaryFormatter is still a default formatter, and one should specify SystemTextJsonFormatter explicitly? Or am I doing something wrong?
  2. Can you please confirm that with the code above BinaryFormatter is not used anywhere in the compiled app? (so this does not apply any more...)
  3. Should I use .NET Standard 2.0 for pipeMessage class (in a common project), or can I use .NET 8 there as well?

Interesting Bug...version 2.0.34

Using H.Pipes, H.Formatter & H.Formatters.BinaryFormatteer version 2.0.34 NuGets.

I'm writing a plugin for a WinForms 4.7.2 program (server) which is trying to talk to a .NET 6.0 WPF application (client). It uses a separate Pipes_Shared .NET Standard 2.0 class for the messages between the two applications:

    [Serializable]
    public class PipeMessage
    {
        public PipeMessage()
        {
            Id = Guid.NewGuid();
        }

        public Guid Id { get; set; }
        public ActionType Action { get; set; }
        public string Text { get; set; } = string.Empty;
    }

    public enum ActionType
    {
        Unknown,
        SendText,
    }

The two ends of the pipe connect just fine. I get a message back from the server that goes through the PipeMessage to send a "You have connected!" message to the WPF client. No problems there. But I send some text back from the client to the server, the

            _PipeServer.ExceptionOccurred += (o, args) =>
            {
                OnExceptionOccurred(args.Exception);
            };

trips with the very odd message:

Exception: System.Runtime.Serialization.SerializationException: Unable to find assembly 'pipes_shared, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null'.
   at System.Runtime.Serialization.Formatters.Binary.BinaryAssemblyInfo.GetAssembly()
   at System.Runtime.Serialization.Formatters.Binary.ObjectReader.GetType(BinaryAssemblyInfo assemblyInfo, String name)
   at System.Runtime.Serialization.Formatters.Binary.ObjectMap..ctor(String objectName, String[] memberNames, BinaryTypeEnum[] binaryTypeEnumA, Object[] typeInformationA, Int32[] memberAssemIds, ObjectReader objectReader, Int32 objectId, BinaryAssemblyInfo assemblyInfo, SizedArray assemIdToAssemblyTable)
   at System.Runtime.Serialization.Formatters.Binary.__BinaryParser.ReadObjectWithMapTyped(BinaryObjectWithMapTyped record)
   at System.Runtime.Serialization.Formatters.Binary.__BinaryParser.Run()
   at System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
   at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage)
   at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream)
   at H.Formatters.BinaryFormatter.DeserializeInternal[T](Byte[] bytes) in D:\DotNET_Proj\H.Pipes\src\libs\H.Formatters.BinaryFormatter\BinaryFormatter.cs:line 22
   at H.Formatters.FormatterBase.Deserialize[T](Byte[] bytes) in D:\DotNET_Proj\H.Pipes\src\libs\H.Formatters\FormatterBase.cs:line 39
   at H.Pipes.PipeConnection`1.<<Start>b__37_0>d.MoveNext() in D:\DotNET_Proj\H.Pipes\src\libs\H.Pipes\PipeConnection.cs:line 124

How is that possible that the server found the assembly pipes_shared the first time around to generate the message to the client, but now it can't find it for deserialization?

Thanks,

  • Dirk

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.