HavenDV / H.Pipes

A simple, easy to use, strongly-typed, async wrapper around .NET named pipes.
MIT License
219 stars 26 forks source link

Interesting Bug...version 2.0.34 #16

Closed GeoDirk closed 2 years ago

GeoDirk commented 2 years ago

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,

HavenDV commented 2 years ago

Hello.

There are differences here in what happens during serialization and deserialization. C# is designed to load libraries the first time you explicitly use any type from that library. As I understand it, when deserializing, BinaryFormatter requires that the required library has already been loaded. Therefore, to fix it, you just need to explicitly call any type from your library at the time of server initialization like:

_ = new PipeMessage()
HavenDV commented 2 years ago

I'm actively developing a SourceGenerator that generates client/server code based on a C# interface. I would be glad if you take a look at it (if you are still interested) and provide any feedback - https://github.com/HavenDV/H.Ipc

GeoDirk commented 2 years ago

Hmm...that didn't seem to fix the issue. Here is what I put on the server (see second line):

            _PipeServer = new PipeServer<PipeMessage>(PipeName);
            _ = new PipeMessage();
            _PipeServer.ClientConnected += async (o, args) =>
            {
                Clients.Add(args.Connection.PipeName);
                UpdateClientList();

                AddLine($"{args.Connection.PipeName} connected!");

                try
                {
                    await args.Connection.WriteAsync(new PipeMessage
                    {
                        Action = ActionType.SendText,
                        Id = new Guid(),
                        Text = "Welcome! You are now connected to the server."
                    }).ConfigureAwait(false);
                }
                catch (Exception exception)
                {
                    OnExceptionOccurred(exception);
                }
            };
            _PipeServer.ClientDisconnected += (o, args) =>
            {
                Clients.Remove(args.Connection.PipeName);
                UpdateClientList();

                AddLine($"{args.Connection.PipeName} disconnected!");
            };
            _PipeServer.MessageReceived += (sender, args) =>
            {
                if (args.Message != null)
                {
                    OnMessageReceivedAsync(args.Message);
                }
            };

            _PipeServer.ExceptionOccurred += (o, args) =>
            {

                OnExceptionOccurred(args.Exception);
            };

On the client I did the same. Unfortunately, the same error message pops up.

HavenDV commented 2 years ago

Please try this: https://stackoverflow.com/questions/14990980/system-runtime-serialization-serializationexception-unable-to-find-assembly-mya I have added BinaryFormatter.InternalFormatter property. You need to use an explicit BinaryFormatter:

var formatter = new BinaryFormatter();
formatter.InternalFormatter.Binder = new CustomizedBinder();
_PipeServer = new PipeServer<PipeMessage>(PipeName, formatter: formatter);
GeoDirk commented 2 years ago

Hi @HavenDV

Thanks to your pointers and your latest code changes, I was able to get it to work with the following:

            var formatter = new BinaryFormatter();
            formatter.InternalFormatter.Binder = new CustomizedBinder();
            _PipeServer = new PipeServer<PipeMessage>(PipeName, formatter: formatter);

and then the CustomizedBinder:

        sealed class CustomizedBinder : SerializationBinder
        {
            public override Type BindToType(string assemblyName, string typeName)
            {
                Type returntype = Type.GetType(String.Format("{0}, {1}", typeName, assemblyName));
                return returntype;
            }

            public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
            {
                base.BindToName(serializedType, out assemblyName, out typeName);
                assemblyName = "Pipes_Shared, Version=1.0.1.0, Culture=neutral, PublicKeyToken=null";
            }
        }

Fantastic support! How can I buy you a beer?

HavenDV commented 2 years ago

Fantastic support! How can I buy you a beer?

Thanks. Not at the moment, I'm from Russia. Your star will be enough for me 😉