From 9a8c371f798c4f281a661d9471cb0a2ea03bc32d Mon Sep 17 00:00:00 2001 From: Lars Werner Date: Sun, 20 Sep 2020 21:03:35 +0200 Subject: [PATCH] Change client method for waiting for data Exceptionhandling instead of null when it times out. Gives better controll on how to handle timeouts. Update v1.1.4.4 on nuget also --- Client/Program.cs | 25 ++++++++++++++++ Contract/Contract.cs | 3 +- README.md | 19 ++++++++---- Server/Program.cs | 10 ++++++- .../SimpleCrossFrameworkIPC.core.csproj | 14 ++++----- SimpleCrossFrameworkIPC/Client/Client.cs | 27 +++++++++++++++--- SimpleCrossFrameworkIPC/Common/BasicPipe.cs | 2 +- .../Properties/AssemblyInfo.cs | 4 +-- SimpleCrossFrameworkIPC/Server/Server.cs | 13 +++++++-- icon.jpg | Bin 0 -> 14627 bytes 10 files changed, 93 insertions(+), 24 deletions(-) create mode 100644 icon.jpg diff --git a/Client/Program.cs b/Client/Program.cs index 59d6bec..79c98ea 100644 --- a/Client/Program.cs +++ b/Client/Program.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Client @@ -11,6 +12,9 @@ class Program { static void Main(string[] args) { + Console.WriteLine("Waiting 2 second before creating and connecting client"); + Thread.Sleep(2000); + //Create client with a simple event for disconnect var client = new SimpleCrossFrameworkIPC.Client(); client.ClientDisconnected += (sndr, arguments) => @@ -39,6 +43,27 @@ static void Main(string[] args) //Disconnect the client client.Disconnect(); Console.WriteLine($"Connectionstate: {client.IsConnected()}"); + + //Testing new client with a different timeout + //Create client with a simple event for disconnect + Console.WriteLine("Test timeout for clients, max 3 waittime for 4 second function"); + var client2 = new SimpleCrossFrameworkIPC.Client(3000); + client2.ClientDataReceiveTimeout += (sndr, arguments) => + Console.WriteLine("Client did not receive data in the given time set"); + try + { + //Connect and give state of connection, return values + client2.Connect(Channel.Name, Channel.TimeoutMS); + var proxy = client2.GetProxy(); + Console.WriteLine($"DelayedFunction: {proxy.DelayedFunction()}"); + } + catch (Exception ex) + { + Console.WriteLine("Error: " + ex.ToString()); + } + + client2.Disconnect(); + Console.WriteLine("Press any key to quit"); Console.ReadLine(); } diff --git a/Contract/Contract.cs b/Contract/Contract.cs index 74af559..5ece745 100644 --- a/Contract/Contract.cs +++ b/Contract/Contract.cs @@ -14,12 +14,13 @@ public interface IMySimpleService int Count { get; set; } int Function(); + bool DelayedFunction(); } //Simple common class to share name and timeouts public class Channel { public static string Name = "ChannelName"; - public static int TimeoutMS = 1000; + public static int TimeoutMS = 1000; //Connection timeout } } diff --git a/README.md b/README.md index 05a5d82..f45cb94 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ Simple IPC between .net and .core This is a lightversion of a IPC to be used on either .net or .core and even mixed if you like. The "fix" for the conversion is a hack and might not work for later releases. -As for 10.05.2020 it works as expected between netstandard 2.0 and .net 4.7.2. +As for 20.09.2020 it works as expected between netstandard 2.0 and .net 4.7.2. Class is based on KrakenIPC: https://github.com/darksody/KrakenIPC and Full Duplex Pipes: https://www.codeproject.com/Articles/1179195/Full-Duplex-Asynchronous-Read-Write-with-Named-Pip - ## Pipes are old, why did you mesh this up I had a asp.core application that needed to get data from a .net472 application and the only IPC that actually worked was gRPC ( https://grpc.io/ ). gRPC was overkill for my project so I wanted something simpler and tiny. +#### Updated 20.09.2020 #### +Added a custom delay for the time to wait for server data for the client. +Changed return method to throw an exception instead of null, to make it easier to handle timeouts in the future. +Also an event is added, to ensure that you catch everything if you decide to suppress exceptions. + ## Usage Server and Client need to share a common interface. ```c# @@ -49,7 +53,7 @@ Note that this pipe only works on localhost //Stop server handler.Stop(); } - catch(Exception ex) // + catch(Exception ex) { Console.WriteLine(ex.ToString()); } @@ -58,12 +62,15 @@ Note that this pipe only works on localhost When a client connects it will refer to the same interface. After connection are used to receive data from the server + + ```c# - var client = new SimpleCrossFrameworkIPC.Client(); + int nWaitForServerDataDelay = 2000; //2 sec max waiting for data + var client = new SimpleCrossFrameworkIPC.Client(nWaitForServerDataDelay); try { - //Connect with a 1 second timeout + //Connect with a 1 second connection timeout client.Connect("Channel", 1000); var proxy = client.GetProxy(); @@ -71,7 +78,7 @@ After connection are used to receive data from the server Console.WriteLine("Text: " + proxy.Text); Console.WriteLine("Number: " + proxy.Number.ToString()); } - catch(Exception ex) // + catch(Exception ex) { Console.WriteLine(ex.ToString()); } diff --git a/Server/Program.cs b/Server/Program.cs index d759728..61adcc4 100644 --- a/Server/Program.cs +++ b/Server/Program.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace Server @@ -13,7 +14,14 @@ public class MySimpleService : IMySimpleService public int Number { get => 111; } public string Text { get => "Some string"; } public int Count { get; set; } - + + //Simple function to show how to handle the receiveclientTimeout event + public bool DelayedFunction() + { + Thread.Sleep(4000); + return true; + } + public int Function() { return Count * 12; diff --git a/SimpleCrossFrameworkIPC.core/SimpleCrossFrameworkIPC.core.csproj b/SimpleCrossFrameworkIPC.core/SimpleCrossFrameworkIPC.core.csproj index 72ffff1..ea525da 100644 --- a/SimpleCrossFrameworkIPC.core/SimpleCrossFrameworkIPC.core.csproj +++ b/SimpleCrossFrameworkIPC.core/SimpleCrossFrameworkIPC.core.csproj @@ -3,7 +3,7 @@ netstandard2.0;net472 false - 1.1.3.2 + 1.1.4.4 Lars Werner Simple IPC across .core and .net. Copyright © 2020 @@ -20,12 +20,12 @@ true true - 1.1.3.2 - 1.1.3.2 + 1.1.4.4 + 1.1.4.4 SimpleCrossFrameworkIPC SimpleCrossFrameworkIPC false - SimpleIPC.jpg + icon.jpg MIT SimpleCrossFrameworkIPC SimpleCrossFrameworkIPC @@ -46,15 +46,15 @@ - + - + - + True diff --git a/SimpleCrossFrameworkIPC/Client/Client.cs b/SimpleCrossFrameworkIPC/Client/Client.cs index 7912596..4f0f5f3 100644 --- a/SimpleCrossFrameworkIPC/Client/Client.cs +++ b/SimpleCrossFrameworkIPC/Client/Client.cs @@ -32,17 +32,33 @@ public class Client where T : class //Event if client was disconnected public event EventHandler ClientDisconnected; + //Event if client timed out for waiting for data + public event EventHandler ClientDataReceiveTimeout; + + //Timeout variable (in ms) for waiting for data from server. Default 2000 ms (2 secs) + public int ClientReceiveTimeout { get; set; } + //Variable for connection-station private bool bConnected; /// /// Proxy uses a callbackfunction that builds with the Response-class received from the server + /// Default constructor for 2000 ms wait time for data /// - public Client() + public Client() : this(2000) + { + } + + /// + /// Alternative creator for clients + /// + /// + public Client(int _ClientReceiveTimeout) { clientPipe = null; proxy = ProxyHelper.GetInstance(OnMethodCallback); bConnected = false; + ClientReceiveTimeout = _ClientReceiveTimeout; } /// @@ -150,8 +166,8 @@ private object OnMethodCallback(string methodName, List parameterValues, byte[] requestBytes = Encoding.ASCII.GetBytes(json); clientPipe.WriteBytes(requestBytes, false); - //Wait for data to be received, then build the data (2 seconds max) - responseEvent.Wait(TimeSpan.FromMilliseconds(2000)); + //Wait for data to be received, then build the data (user set timeout) + responseEvent.Wait(TimeSpan.FromMilliseconds(ClientReceiveTimeout)); if (responseEvent.IsSet) { Message message = JsonConvert.DeserializeObject(responseJson); @@ -181,7 +197,10 @@ private object OnMethodCallback(string methodName, List parameterValues, } else //If event is not set, then return nada - return null; + { + ClientDataReceiveTimeout?.Invoke(this, new EventArgs()); + throw new Exception("Timeout waiting for data"); + } } /// diff --git a/SimpleCrossFrameworkIPC/Common/BasicPipe.cs b/SimpleCrossFrameworkIPC/Common/BasicPipe.cs index a14d2fb..abc2c15 100644 --- a/SimpleCrossFrameworkIPC/Common/BasicPipe.cs +++ b/SimpleCrossFrameworkIPC/Common/BasicPipe.cs @@ -132,7 +132,7 @@ public Task WriteBytes(byte[] bytes, bool InsertLengthFirst = true) var blength = BitConverter.GetBytes(bytes.Length); bfull = blength.Concat(bytes).ToArray(); } - + return pipeStream.WriteAsync(bfull, 0, bfull.Length); } diff --git a/SimpleCrossFrameworkIPC/Properties/AssemblyInfo.cs b/SimpleCrossFrameworkIPC/Properties/AssemblyInfo.cs index 630f21c..eb7ec4a 100644 --- a/SimpleCrossFrameworkIPC/Properties/AssemblyInfo.cs +++ b/SimpleCrossFrameworkIPC/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.1.3.2")] -[assembly: AssemblyFileVersion("1.1.3.2")] +[assembly: AssemblyVersion("1.1.4.4")] +[assembly: AssemblyFileVersion("1.1.4.4")] diff --git a/SimpleCrossFrameworkIPC/Server/Server.cs b/SimpleCrossFrameworkIPC/Server/Server.cs index e2898e9..5eb1f23 100644 --- a/SimpleCrossFrameworkIPC/Server/Server.cs +++ b/SimpleCrossFrameworkIPC/Server/Server.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.IO; using System.IO.Pipes; using System.Linq; using System.Reflection; @@ -65,11 +66,12 @@ private void CreateServer(string Pipename) serverPipe.Disconnect += (sndr, args) => { - ClientDisconnected?.Invoke(this, new EventArgs()); ServerPipe sender = sndr as ServerPipe; bool bPipeRemoved = serverPipes.Remove(sender); if (!bPipeRemoved) throw new Exception("Pipe not found, rare case "); + else + ClientDisconnected?.Invoke(this, new EventArgs()); }; } @@ -153,7 +155,14 @@ private void OnMessageReceived(ServerPipe serverPipe, byte[] bytes) var responseMessage = new Message(response); var responseJson = JsonConvert.SerializeObject(responseMessage); byte[] responseBytes = Encoding.ASCII.GetBytes(responseJson); - serverPipe.WriteBytes(responseBytes, false); + + try + { + serverPipe.WriteBytes(responseBytes, false); + } + catch(IOException) //Catch this exception to ensure disconnect + { + } } } } diff --git a/icon.jpg b/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..711150cd38c001071f72d4a92e2bd46d0d86dca7 GIT binary patch literal 14627 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 literal 0 HcmV?d00001