├── .editorconfig ├── Icon_64x64.png ├── Icon_Working.pdn ├── TestCore ├── ICrasher.cs ├── TestCore.csproj ├── PipeSide.cs ├── IConcatenator.cs ├── PipeSerializerType.cs ├── Crasher.cs ├── Scenario.cs ├── Concatenator.cs ├── WrappedInt.cs ├── IAdder.cs └── Adder.cs ├── PipeMethodCalls ├── Models │ ├── IMessage.cs │ ├── MessageType.cs │ ├── PendingCall.cs │ ├── PipeState.cs │ ├── SerializedPipeRequest.cs │ ├── TypedPipeRequest.cs │ ├── SerializedPipeResponse.cs │ └── TypedPipeResponse.cs ├── RequestHandler │ ├── IRequestHandler.cs │ └── RequestHandler.cs ├── Invoker │ ├── IResponseHandler.cs │ ├── IPipeInvokerHost.cs │ ├── ArgumentResolver.cs │ ├── IPipeInvoker.cs │ └── MethodInvoker.cs ├── Endpoints │ ├── IPipeServerWithCallback.cs │ ├── IPipeServer.cs │ ├── IPipeClient.cs │ ├── PipeServer.cs │ ├── PipeServerWithCallback.cs │ ├── PipeClient.cs │ └── PipeClientWithCallback.cs ├── Extensions │ ├── ActionExtensionMethods.cs │ ├── StreamExtensions.cs │ └── PipeInvokerHostExtensions.cs ├── Serialization │ └── IPipeSerializer.cs ├── Exceptions │ └── PipeInvokeFailedException.cs ├── PipeMethodCalls.csproj ├── PipeMessageProcessor.cs ├── Utilities.cs └── PipeStreamWrapper.cs ├── PerformanceTestRunner ├── PerformanceTestRunner.csproj └── Program.cs ├── TestScenarioRunner ├── TestScenarioRunner.csproj ├── ServerCrashScenario.cs ├── NoCallbackScenario.cs ├── PerformanceScenario.cs ├── WithCallbackScenario.cs └── Program.cs ├── PipeMethodCalls.NetJson ├── NetJsonPipeSerializer.cs └── PipeMethodCalls.NetJson.csproj ├── PipeMethodCalls.Tests ├── PipeMethodCalls.Tests.csproj └── EndToEndTests.cs ├── PipeMethodCalls.MessagePack ├── MessagePackPipeSerializer.cs └── PipeMethodCalls.MessagePack.csproj ├── LICENSE ├── README.md ├── PipeMethodCalls.sln └── .gitignore /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{cs,xaml}] 4 | indent_style = tab 5 | tab_width = 4 -------------------------------------------------------------------------------- /Icon_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RandomEngy/PipeMethodCalls/HEAD/Icon_64x64.png -------------------------------------------------------------------------------- /Icon_Working.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RandomEngy/PipeMethodCalls/HEAD/Icon_Working.pdn -------------------------------------------------------------------------------- /TestCore/ICrasher.cs: -------------------------------------------------------------------------------- 1 | namespace TestCore 2 | { 3 | public interface ICrasher 4 | { 5 | void Crash(); 6 | } 7 | } -------------------------------------------------------------------------------- /TestCore/TestCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TestCore/PipeSide.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace TestCore 6 | { 7 | public enum PipeSide 8 | { 9 | Client, 10 | Server 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /TestCore/IConcatenator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace TestCore 6 | { 7 | public interface IConcatenator 8 | { 9 | string Concatenate(string a, string b); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /TestCore/PipeSerializerType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace TestCore 6 | { 7 | public enum PipeSerializerType 8 | { 9 | NetJson, 10 | MessagePack 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /TestCore/Crasher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace TestCore 6 | { 7 | public class Crasher : ICrasher 8 | { 9 | public void Crash() 10 | { 11 | Environment.Exit(1); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TestCore/Scenario.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace TestCore 6 | { 7 | public enum Scenario 8 | { 9 | WithCallback, 10 | NoCallback, 11 | Performance, 12 | ServerCrash, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /TestCore/Concatenator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace TestCore 6 | { 7 | public class Concatenator : IConcatenator 8 | { 9 | public string Concatenate(string a, string b) 10 | { 11 | return a + b; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PipeMethodCalls/Models/IMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace PipeMethodCalls 6 | { 7 | internal interface IMessage 8 | { 9 | /// 10 | /// The call ID. 11 | /// 12 | int CallId { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PipeMethodCalls/Models/MessageType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace PipeMethodCalls 6 | { 7 | /// 8 | /// The pipe message type. 9 | /// 10 | internal enum MessageType 11 | { 12 | Request = 0, 13 | Response = 1, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /TestCore/WrappedInt.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Runtime.Serialization; 4 | using System.Text; 5 | 6 | namespace TestCore 7 | { 8 | [DataContract] 9 | public class WrappedInt 10 | { 11 | [DataMember(Order = 0)] 12 | public int Num { get; set; } 13 | 14 | public override string ToString() 15 | { 16 | return "Wrapped " + this.Num; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PerformanceTestRunner/PerformanceTestRunner.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /PipeMethodCalls/RequestHandler/IRequestHandler.cs: -------------------------------------------------------------------------------- 1 | namespace PipeMethodCalls 2 | { 3 | /// 4 | /// Handles a request message received from a remote endpoint. 5 | /// 6 | internal interface IRequestHandler 7 | { 8 | /// 9 | /// Handles a request message received from a remote endpoint. 10 | /// 11 | /// The request message. 12 | void HandleRequest(SerializedPipeRequest request); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PipeMethodCalls/Invoker/IResponseHandler.cs: -------------------------------------------------------------------------------- 1 | namespace PipeMethodCalls 2 | { 3 | /// 4 | /// Handles a response message received from a remote endpoint. 5 | /// 6 | internal interface IResponseHandler 7 | { 8 | /// 9 | /// Handles a response message received from a remote endpoint. 10 | /// 11 | /// The response message to handle. 12 | void HandleResponse(SerializedPipeResponse response); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PipeMethodCalls/Invoker/IPipeInvokerHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace PipeMethodCalls 6 | { 7 | /// 8 | /// Holds an Invoker property. 9 | /// 10 | /// The type to make method calls for. 11 | public interface IPipeInvokerHost 12 | where TRequesting : class 13 | { 14 | /// 15 | /// Get the invoker. 16 | /// 17 | IPipeInvoker Invoker { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /PipeMethodCalls/Endpoints/IPipeServerWithCallback.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace PipeMethodCalls 7 | { 8 | /// 9 | /// A named pipe server with a callback channel. 10 | /// 11 | /// The callback channel interface that the client will be handling. 12 | public interface IPipeServerWithCallback : IPipeServer, IPipeInvokerHost 13 | where TRequesting : class 14 | { 15 | } 16 | } -------------------------------------------------------------------------------- /PipeMethodCalls/Models/PendingCall.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace PipeMethodCalls 7 | { 8 | /// 9 | /// Represents a pending call executing on the remote endpoint. 10 | /// 11 | internal class PendingCall 12 | { 13 | /// 14 | /// The task completion source for the call. 15 | /// 16 | public TaskCompletionSource TaskCompletionSource { get; } = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /TestCore/IAdder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace TestCore 7 | { 8 | public interface IAdder 9 | { 10 | int AddNumbers(int a, int b); 11 | double Sum(double[] numbers); 12 | 13 | WrappedInt AddWrappedNumbers(WrappedInt a, WrappedInt b); 14 | 15 | Task AddAsync(int a, int b); 16 | 17 | IList Listify(T item); 18 | 19 | int Unwrap(WrappedInt a); 20 | 21 | void DoesNothing(); 22 | 23 | Task DoesNothingAsync(); 24 | 25 | void AlwaysFails(); 26 | 27 | void HasRefParam(ref int refParam); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /PipeMethodCalls/Extensions/ActionExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace PipeMethodCalls 6 | { 7 | /// 8 | /// Extension methods for actions. 9 | /// 10 | internal static class ActionExtensionMethods 11 | { 12 | /// 13 | /// Invokes the given action as a logger. 14 | /// 15 | /// The action to invoke as a logger. 16 | /// The function to produce the string to log. 17 | public static void Log(this Action logger, Func messageFunc) 18 | { 19 | logger?.Invoke(messageFunc()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /PipeMethodCalls/Models/PipeState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace PipeMethodCalls 6 | { 7 | /// 8 | /// Represents the state of the pipe connection. 9 | /// 10 | public enum PipeState 11 | { 12 | /// 13 | /// The pipe has not yet connected. 14 | /// 15 | NotOpened, 16 | 17 | /// 18 | /// The pipe is connected, methods can be invoked and we are listening for message from the other side of the pipe. 19 | /// 20 | Connected, 21 | 22 | /// 23 | /// The pipe has closed gracefully. 24 | /// 25 | Closed, 26 | 27 | /// 28 | /// An unexpected error caused the pipe to close. 29 | /// 30 | Faulted 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TestScenarioRunner/TestScenarioRunner.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /PipeMethodCalls.NetJson/NetJsonPipeSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Text.Json; 6 | 7 | namespace PipeMethodCalls.NetJson 8 | { 9 | /// 10 | /// Serializes pipe method call information with System.Text.Json. 11 | /// 12 | public class NetJsonPipeSerializer : IPipeSerializer 13 | { 14 | public object Deserialize(byte[] data, Type type) 15 | { 16 | return JsonSerializer.Deserialize(data, type); 17 | } 18 | 19 | public byte[] Serialize(object o) 20 | { 21 | using (var memoryStream = new MemoryStream()) 22 | using (var utf8JsonWriter = new Utf8JsonWriter(memoryStream)) 23 | { 24 | JsonSerializer.Serialize(utf8JsonWriter, o); 25 | return memoryStream.ToArray(); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /PipeMethodCalls.Tests/PipeMethodCalls.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /PipeMethodCalls.MessagePack/MessagePackPipeSerializer.cs: -------------------------------------------------------------------------------- 1 | using MessagePack; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace PipeMethodCalls.MessagePack 7 | { 8 | /// 9 | /// Serializes pipe method call information with MessagePack. 10 | /// 11 | public class MessagePackPipeSerializer : IPipeSerializer 12 | { 13 | public object Deserialize(byte[] data, Type type) 14 | { 15 | if (data.Length == 0) 16 | { 17 | return null; 18 | } 19 | 20 | 21 | return MessagePackSerializer.Deserialize(type, data); 22 | } 23 | 24 | public byte[] Serialize(object o) 25 | { 26 | if (o == null || o.GetType().FullName == "System.Threading.Tasks.VoidTaskResult") 27 | { 28 | return Array.Empty(); 29 | } 30 | 31 | return MessagePackSerializer.Serialize(o.GetType(), o); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /TestScenarioRunner/ServerCrashScenario.cs: -------------------------------------------------------------------------------- 1 | using PipeMethodCalls; 2 | using Shouldly; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using TestCore; 9 | 10 | namespace TestScenarioRunner 11 | { 12 | public static class ServerCrashScenario 13 | { 14 | public static async Task RunClientAsync(PipeClient pipeClient) 15 | { 16 | await pipeClient.ConnectAsync().ConfigureAwait(false); 17 | await Should.ThrowAsync(async () => 18 | { 19 | await pipeClient.InvokeAsync(crasher => crasher.Crash()).ConfigureAwait(false); 20 | }); 21 | 22 | pipeClient.Dispose(); 23 | } 24 | 25 | public static async Task RunServerAsync(PipeServer pipeServer) 26 | { 27 | await pipeServer.WaitForConnectionAsync(); 28 | await pipeServer.WaitForRemotePipeCloseAsync(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PipeMethodCalls/Serialization/IPipeSerializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace PipeMethodCalls 6 | { 7 | /// 8 | /// Defines how to serialize and deserialize objects to pass through the pipe. 9 | /// 10 | public interface IPipeSerializer 11 | { 12 | /// 13 | /// Serializes the given object into bytes. 14 | /// 15 | /// The object to serialize. 16 | /// The bytes to represent the serialized object. 17 | byte[] Serialize(object o); 18 | 19 | /// 20 | /// Deserializes the given bytes into an object. 21 | /// 22 | /// The data to deserialize. 23 | /// The type to deserialize to. 24 | /// The deserialized object. 25 | object Deserialize(byte[] data, Type type); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /TestScenarioRunner/NoCallbackScenario.cs: -------------------------------------------------------------------------------- 1 | using PipeMethodCalls; 2 | using Shouldly; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using TestCore; 9 | 10 | namespace TestScenarioRunner 11 | { 12 | public static class NoCallbackScenario 13 | { 14 | public static async Task RunClientAsync(PipeClient pipeClient) 15 | { 16 | await pipeClient.ConnectAsync().ConfigureAwait(false); 17 | WrappedInt result = await pipeClient.InvokeAsync(adder => adder.AddWrappedNumbers(new WrappedInt { Num = 1 }, new WrappedInt { Num = 3 })).ConfigureAwait(false); 18 | result.Num.ShouldBe(4); 19 | 20 | pipeClient.Dispose(); 21 | } 22 | 23 | public static async Task RunServerAsync(PipeServer pipeServer) 24 | { 25 | await pipeServer.WaitForConnectionAsync(); 26 | await pipeServer.WaitForRemotePipeCloseAsync(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /PerformanceTestRunner/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using TestCore; 3 | 4 | ProcessStartInfo serverInfo = new ProcessStartInfo("TestScenarioRunner", $"--scenario {Scenario.Performance} --side {PipeSide.Server} --type {PipeSerializerType.MessagePack}"); 5 | Process serverProcess = Process.Start(serverInfo); 6 | 7 | var stopwatch = new Stopwatch(); 8 | stopwatch.Start(); 9 | 10 | ProcessStartInfo clientInfo = new ProcessStartInfo("TestScenarioRunner", $"--scenario {Scenario.Performance} --side {PipeSide.Client} --type {PipeSerializerType.MessagePack}"); 11 | Process clientProcess = Process.Start(clientInfo); 12 | 13 | clientProcess.WaitForExit(); 14 | stopwatch.Stop(); 15 | 16 | var standardError = new StreamWriter(Console.OpenStandardError()); 17 | standardError.AutoFlush = true; 18 | 19 | serverProcess.WaitForExit(); 20 | 21 | //Console.SetError(standardError); 22 | 23 | Console.WriteLine(); 24 | Console.WriteLine($@"Time elapsed: {stopwatch.Elapsed.TotalMilliseconds} ms"); 25 | Console.ReadLine(); 26 | -------------------------------------------------------------------------------- /PipeMethodCalls/Endpoints/IPipeServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace PipeMethodCalls 6 | { 7 | /// 8 | /// A named pipe server. 9 | /// 10 | public interface IPipeServer : IDisposable 11 | { 12 | /// 13 | /// Sets up the given action as a logger for the module. 14 | /// 15 | /// The logger action. 16 | void SetLogger(Action logger); 17 | 18 | /// 19 | /// Waits for a client to connect to the pipe. 20 | /// 21 | /// A token to cancel the request. 22 | Task WaitForConnectionAsync(CancellationToken cancellationToken = default); 23 | 24 | /// 25 | /// Waits for the client to close the pipe. 26 | /// 27 | /// A token to cancel the request. 28 | Task WaitForRemotePipeCloseAsync(CancellationToken cancellationToken = default); 29 | } 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 David Rickard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PipeMethodCalls/Exceptions/PipeInvokeFailedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Runtime.Serialization; 5 | using System.Text; 6 | 7 | namespace PipeMethodCalls 8 | { 9 | /// 10 | /// Represents when an invoke was successfully executed on the remote endpoint, but the method threw an exception. 11 | /// 12 | public class PipeInvokeFailedException : IOException 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The exception message. 18 | public PipeInvokeFailedException(string message) 19 | : base(message) 20 | { 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The exception message. 27 | /// The inner exception. 28 | public PipeInvokeFailedException(string message, Exception innerException) 29 | : base(message, innerException) 30 | { 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /PipeMethodCalls/Endpoints/IPipeClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace PipeMethodCalls 7 | { 8 | /// 9 | /// A named pipe client. 10 | /// 11 | /// The interface that the client will be invoking on the server. 12 | public interface IPipeClient : IPipeInvokerHost 13 | where TRequesting : class 14 | { 15 | /// 16 | /// Connects to the server. 17 | /// 18 | /// A token to cancel the request. 19 | Task ConnectAsync(CancellationToken cancellationToken = default); 20 | 21 | /// 22 | /// Sets up the given action as a logger for the module. 23 | /// 24 | /// The logger action. 25 | void SetLogger(Action logger); 26 | 27 | /// 28 | /// Waits for the server to close the pipe. 29 | /// 30 | /// A token to cancel the request. 31 | Task WaitForRemotePipeCloseAsync(CancellationToken cancellationToken = default); 32 | } 33 | } -------------------------------------------------------------------------------- /PipeMethodCalls/PipeMethodCalls.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | true 5 | David Rickard 6 | LICENSE 7 | Lightweight .NET Standard 2.0 library for method calls over named pipes for IPC. Supports two-way communication with callbacks. 8 | https://github.com/RandomEngy/PipeMethodCalls 9 | https://github.com/RandomEngy/PipeMethodCalls 10 | 4.0.2 11 | Icon_64x64.png 12 | README.md 13 | Fixed an issue with hung calls when pipe server disconnects. Pending calls now throw an IOException when the pipe closes in the middle of the call. 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /PipeMethodCalls.MessagePack/PipeMethodCalls.MessagePack.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | David Rickard 7 | LICENSE 8 | MessagePack serializer for PipeMethodCalls. Uses the MessagePack-CSharp library. 9 | https://github.com/RandomEngy/PipeMethodCalls.NetJson 10 | https://github.com/RandomEngy/PipeMethodCalls.NetJson 11 | 3.0.2 12 | Icon_64x64.png 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | True 26 | 27 | 28 | 29 | True 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /PipeMethodCalls.NetJson/PipeMethodCalls.NetJson.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | true 6 | David Rickard 7 | LICENSE 8 | System.Text.Json serializer for PipeMethodCalls. 9 | https://github.com/RandomEngy/PipeMethodCalls.NetJson 10 | https://github.com/RandomEngy/PipeMethodCalls.NetJson 11 | 3.0.0 12 | Icon_64x64.png 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | True 26 | 27 | 28 | 29 | True 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /TestCore/Adder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace TestCore 8 | { 9 | public class Adder : IAdder 10 | { 11 | public int AddNumbers(int a, int b) 12 | { 13 | return a + b; 14 | } 15 | 16 | public double Sum(double[] numbers) 17 | { 18 | return numbers.Sum(); 19 | } 20 | 21 | public WrappedInt AddWrappedNumbers(WrappedInt a, WrappedInt b) 22 | { 23 | return new WrappedInt { Num = a.Num + b.Num }; 24 | } 25 | 26 | public Task AddAsync(int a, int b) 27 | { 28 | return Task.FromResult(a + b); 29 | } 30 | 31 | public IList Listify(T item) 32 | { 33 | return new List { item }; 34 | } 35 | 36 | public int Unwrap(WrappedInt a) 37 | { 38 | if (a == null) 39 | { 40 | return 0; 41 | } 42 | 43 | return a.Num; 44 | } 45 | 46 | public void DoesNothing() 47 | { 48 | } 49 | 50 | public Task DoesNothingAsync() 51 | { 52 | return Task.CompletedTask; 53 | } 54 | 55 | public void AlwaysFails() 56 | { 57 | throw new InvalidOperationException("This method always fails."); 58 | } 59 | 60 | public void HasRefParam(ref int refParam) 61 | { 62 | refParam = 5; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /TestScenarioRunner/PerformanceScenario.cs: -------------------------------------------------------------------------------- 1 | using PipeMethodCalls; 2 | using Shouldly; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using TestCore; 9 | 10 | namespace TestScenarioRunner 11 | { 12 | public static class PerformanceScenario 13 | { 14 | public static async Task RunClientAsync(PipeClient pipeClient) 15 | { 16 | await pipeClient.ConnectAsync().ConfigureAwait(false); 17 | 18 | double[] numbers = new[] { 19 | 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 20 | 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 21 | 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 22 | 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 }; 23 | 24 | var tasks = new List>(); 25 | 26 | for (int i = 0; i < 100_000; i++) 27 | { 28 | var task = pipeClient.Invoker.InvokeAsync(i => i.Sum(numbers)); 29 | tasks.Add(task); 30 | } 31 | 32 | foreach (var task in tasks) 33 | { 34 | var result = await task; 35 | result.ShouldBe(45.0 * 4); 36 | } 37 | 38 | pipeClient.Dispose(); 39 | } 40 | 41 | public static async Task RunServerAsync(PipeServer pipeServer) 42 | { 43 | await pipeServer.WaitForConnectionAsync(); 44 | 45 | await pipeServer.WaitForRemotePipeCloseAsync(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /PipeMethodCalls/Models/SerializedPipeRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace PipeMethodCalls 7 | { 8 | /// 9 | /// A request sent over the pipe that has been user serialized by an . 10 | /// 11 | /// 12 | /// There are two stages to serialization: 13 | /// 1: User serialization, where typed values are serialized to a byte array. 14 | /// 2: Message serialization, where the message is sent with a custom binary serialization over the pipe. 15 | /// 16 | internal class SerializedPipeRequest : IMessage 17 | { 18 | /// 19 | /// The call ID. 20 | /// 21 | public int CallId { get; set; } 22 | 23 | /// 24 | /// The name of the method to invoke. 25 | /// 26 | public string MethodName { get; set; } 27 | 28 | /// 29 | /// The list of parameters to pass to the method. 30 | /// 31 | /// 32 | /// The method invoker will know what type each parameter needs to be converted to, and with that type the serializer will be able to convert it to and from a byte array. 33 | /// 34 | public byte[][] Parameters { get; set; } 35 | 36 | /// 37 | /// The types for the generic arguments. 38 | /// 39 | public Type[] GenericArguments { get; set; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /PipeMethodCalls/Invoker/ArgumentResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq.Expressions; 3 | using System.Reflection; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace PipeMethodCalls 8 | { 9 | public static class ArgumentResolver 10 | { 11 | public static object GetValue(Expression expression) 12 | { 13 | switch (expression) 14 | { 15 | case null: 16 | return null; 17 | case ConstantExpression constantExpression: 18 | return constantExpression.Value; 19 | case MemberExpression memberExpression: 20 | return GetValue(memberExpression); 21 | case MethodCallExpression methodCallExpression: 22 | return GetValue(methodCallExpression); 23 | } 24 | 25 | var lambdaExpression = Expression.Lambda(expression); 26 | var @delegate = lambdaExpression.Compile(); 27 | return @delegate.DynamicInvoke(); 28 | } 29 | 30 | private static object GetValue(MemberExpression memberExpression) 31 | { 32 | var value = GetValue(memberExpression.Expression); 33 | 34 | var member = memberExpression.Member; 35 | switch (member) 36 | { 37 | case FieldInfo fieldInfo: 38 | return fieldInfo.GetValue(value); 39 | case PropertyInfo propertyInfo: 40 | try 41 | { 42 | return propertyInfo.GetValue(value); 43 | } 44 | catch (TargetInvocationException e) 45 | { 46 | throw e.InnerException; 47 | } 48 | default: 49 | throw new Exception("Unknown member type: " + member.GetType()); 50 | } 51 | } 52 | 53 | private static object GetValue(MethodCallExpression methodCallExpression) 54 | { 55 | var paras = GetArray(methodCallExpression.Arguments); 56 | var obj = GetValue(methodCallExpression.Object); 57 | 58 | try 59 | { 60 | return methodCallExpression.Method.Invoke(obj, paras); 61 | } 62 | catch (TargetInvocationException e) 63 | { 64 | throw e.InnerException; 65 | } 66 | } 67 | 68 | private static object[] GetArray(IEnumerable expressions) 69 | { 70 | return expressions.Select(GetValue).ToArray(); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /PipeMethodCalls.Tests/EndToEndTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using Shouldly; 3 | using System.Diagnostics; 4 | using TestCore; 5 | 6 | namespace PipeMethodCalls.Tests 7 | { 8 | [TestClass] 9 | public class EndToEndTests 10 | { 11 | [TestMethod] 12 | public void WithCallbacks_NetJson() 13 | { 14 | TestScenario(Scenario.WithCallback, PipeSerializerType.NetJson); 15 | } 16 | 17 | [TestMethod] 18 | public void WithCallbacks_MessagePack() 19 | { 20 | TestScenario(Scenario.WithCallback, PipeSerializerType.MessagePack); 21 | } 22 | 23 | [TestMethod] 24 | public void NoCallbacks_NetJson() 25 | { 26 | TestScenario(Scenario.NoCallback, PipeSerializerType.NetJson); 27 | } 28 | 29 | [TestMethod] 30 | public void NoCallbacks_MessagePack() 31 | { 32 | TestScenario(Scenario.NoCallback, PipeSerializerType.MessagePack); 33 | } 34 | 35 | [TestMethod] 36 | public void ServerCrash() 37 | { 38 | Process serverProcess = StartRunnerProcess(Scenario.ServerCrash, PipeSerializerType.NetJson, PipeSide.Server); 39 | Process clientProcess = StartRunnerProcess(Scenario.ServerCrash, PipeSerializerType.NetJson, PipeSide.Client); 40 | 41 | clientProcess.WaitForExit(); 42 | serverProcess.WaitForExit(); 43 | 44 | clientProcess.ExitCode.ShouldBe(0); 45 | serverProcess.ExitCode.ShouldBe(1); 46 | } 47 | 48 | private static void TestScenario(Scenario scenario, PipeSerializerType pipeSerializerType) 49 | { 50 | Process serverProcess = StartRunnerProcess(scenario, pipeSerializerType, PipeSide.Server); 51 | Process clientProcess = StartRunnerProcess(scenario, pipeSerializerType, PipeSide.Client); 52 | 53 | clientProcess.WaitForExit(); 54 | serverProcess.WaitForExit(); 55 | 56 | clientProcess.ExitCode.ShouldBe(0); 57 | serverProcess.ExitCode.ShouldBe(0); 58 | } 59 | 60 | private static Process StartRunnerProcess(Scenario scenario, PipeSerializerType pipeSerializerType, PipeSide side) 61 | { 62 | ProcessStartInfo processInfo = new ProcessStartInfo("TestScenarioRunner", $"--scenario {scenario} --side {side} --type {pipeSerializerType}"); 63 | return Process.Start(processInfo); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /PipeMethodCalls/Models/TypedPipeRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace PipeMethodCalls 7 | { 8 | /// 9 | /// A method request to be sent over the pipe. Contains typed parameters. 10 | /// 11 | internal class TypedPipeRequest 12 | { 13 | /// 14 | /// The call ID. 15 | /// 16 | public int CallId { get; set; } 17 | 18 | /// 19 | /// The name of the method to invoke. 20 | /// 21 | public string MethodName { get; set; } 22 | 23 | /// 24 | /// The list of parameters to pass to the method. 25 | /// 26 | public object[] Parameters { get; set; } 27 | 28 | /// 29 | /// The types for the generic arguments. 30 | /// 31 | public Type[] GenericArguments { get; set; } 32 | 33 | /// 34 | /// Gets a string representation of this typed request. 35 | /// 36 | /// The string representation of this typed request. 37 | public override string ToString() 38 | { 39 | var builder = new StringBuilder("Request"); 40 | builder.AppendLine(); 41 | builder.Append(" CallId: "); 42 | builder.Append(this.CallId); 43 | builder.AppendLine(); 44 | builder.Append(" MethodName: "); 45 | builder.Append(this.MethodName); 46 | if (this.Parameters.Length > 0) 47 | { 48 | builder.AppendLine(); 49 | builder.Append(" Parameters:"); 50 | foreach (object parameter in this.Parameters) 51 | { 52 | builder.AppendLine(); 53 | builder.Append(" "); 54 | builder.Append(parameter?.ToString() ?? ""); 55 | } 56 | } 57 | 58 | if (this.GenericArguments.Length > 0) 59 | { 60 | builder.AppendLine(); 61 | builder.Append(" Generic arguments:"); 62 | foreach (Type genericArgument in this.GenericArguments) 63 | { 64 | builder.AppendLine(); 65 | builder.Append(" "); 66 | builder.Append(genericArgument.ToString()); 67 | } 68 | } 69 | 70 | return builder.ToString(); 71 | } 72 | 73 | /// 74 | /// Serializes the request. 75 | /// 76 | /// The serializer to use. 77 | /// The serialized request. 78 | public SerializedPipeRequest Serialize(IPipeSerializer serializer) 79 | { 80 | return new SerializedPipeRequest 81 | { 82 | CallId = this.CallId, 83 | MethodName = this.MethodName, 84 | Parameters = this.Parameters.Select(r => serializer.Serialize(r)).ToArray(), 85 | GenericArguments = this.GenericArguments 86 | }; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PipeMethodCalls 2 | 3 | Lightweight .NET Standard 2.0 library to use method calls over named and anonymous pipes for IPC. Supports two-way communication with callbacks. 4 | 5 | ### Calls from client to server 6 | 7 | ```csharp 8 | var pipeServer = new PipeServer( 9 | new NetJsonPipeSerializer(), 10 | "mypipe", 11 | () => new Adder()); 12 | await pipeServer.WaitForConnectionAsync(); 13 | ``` 14 | 15 | ```csharp 16 | var pipeClient = new PipeClient(new NetJsonPipeSerializer(), "mypipe"); 17 | await pipeClient.ConnectAsync(); 18 | int result = await pipeClient.InvokeAsync(adder => adder.AddNumbers(1, 3)); 19 | ``` 20 | 21 | ### Calls both way 22 | 23 | ```csharp 24 | var pipeServer = new PipeServerWithCallback( 25 | new NetJsonPipeSerializer(), 26 | "mypipe", 27 | () => new Adder()); 28 | await pipeServer.WaitForConnectionAsync(); 29 | string concatResult = await pipeServer.InvokeAsync(c => c.Concatenate("a", "b")); 30 | ``` 31 | 32 | ```csharp 33 | var pipeClient = new PipeClientWithCallback( 34 | new NetJsonPipeSerializer(), 35 | "mypipe", 36 | () => new Concatenator()); 37 | await pipeClient.ConnectAsync(); 38 | int result = await pipeClient.InvokeAsync(a => a.AddNumbers(4, 7)); 39 | ``` 40 | 41 | ### About the library 42 | This library uses named pipes to invoke method calls on a remote endpoint. The method arguments are serialized to binary and sent over the pipe. 43 | 44 | ### Serialization 45 | PipeMethodCalls supports customizable serialization logic through `IPipeSerializer`. You've got two options: 46 | 47 | * Use a pre-built serializer 48 | * `new NetJsonPipeSerializer()` from the PipeMethodCalls.NetJson package. That uses the System.Text.Json serializer that's built into .NET. 49 | * `new MessagePackPipeSerializer()` from the PipeMethodCalls.MessagePack package. That uses the [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp) serializer, which has excellent performance. 50 | * Plug in your own implementation of `IPipeSerializer`. Refer to [the NetJsonPipeSerializer code](https://github.com/RandomEngy/PipeMethodCalls/blob/master/PipeMethodCalls.NetJson/NetJsonPipeSerializer.cs) for an example of how to do this. 51 | 52 | Open an issue or pull request if you'd like to see more built-in serializers. 53 | 54 | ### Features 55 | * 100% asynchronous communication with .ConfigureAwait(false) to minimize context switches and reduce thread use 56 | * 45KB with no built-in dependencies 57 | * Invoking async methods 58 | * Passing and returning complex types with pluggable JSON or binary serialization 59 | * Interleaved or multiple simultaneous calls 60 | * Throwing exceptions 61 | * CancellationToken support 62 | * Works on Windows, Linux and MacOS 63 | 64 | ### Not supported 65 | * Methods with out and ref parameters 66 | * Properties 67 | * Method overloads 68 | 69 | -------------------------------------------------------------------------------- /TestScenarioRunner/WithCallbackScenario.cs: -------------------------------------------------------------------------------- 1 | using PipeMethodCalls; 2 | using Shouldly; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using TestCore; 9 | 10 | namespace TestScenarioRunner 11 | { 12 | public static class WithCallbackScenario 13 | { 14 | public static async Task RunClientAsync(PipeClientWithCallback pipeClientWithCallback) 15 | { 16 | await pipeClientWithCallback.ConnectAsync().ConfigureAwait(false); 17 | WrappedInt result = await pipeClientWithCallback.InvokeAsync(adder => adder.AddWrappedNumbers(new WrappedInt { Num = 1 }, new WrappedInt { Num = 3 })).ConfigureAwait(false); 18 | result.Num.ShouldBe(4); 19 | 20 | int asyncResult = await pipeClientWithCallback.InvokeAsync(adder => adder.AddAsync(4, 7)).ConfigureAwait(false); 21 | asyncResult.ShouldBe(11); 22 | 23 | IList listifyResult = await pipeClientWithCallback.InvokeAsync(adder => adder.Listify("item")).ConfigureAwait(false); 24 | listifyResult.Count.ShouldBe(1); 25 | listifyResult[0].ShouldBe("item"); 26 | 27 | int unwrapResult = await pipeClientWithCallback.InvokeAsync(adder => adder.Unwrap(null)).ConfigureAwait(false); 28 | unwrapResult.ShouldBe(0); 29 | 30 | await pipeClientWithCallback.InvokeAsync(adder => adder.DoesNothing()).ConfigureAwait(false); 31 | await pipeClientWithCallback.InvokeAsync(adder => adder.DoesNothingAsync()).ConfigureAwait(false); 32 | 33 | var expectedException = await Should.ThrowAsync(async () => 34 | { 35 | await pipeClientWithCallback.InvokeAsync(adder => adder.AlwaysFails()).ConfigureAwait(false); 36 | }); 37 | 38 | expectedException.Message.ShouldContain("This method always fails"); 39 | 40 | var refException = await Should.ThrowAsync(async () => 41 | { 42 | int refValue = 4; 43 | await pipeClientWithCallback.InvokeAsync(adder => adder.HasRefParam(ref refValue)).ConfigureAwait(false); 44 | }); 45 | 46 | refException.Message.ShouldBe("ref parameters are not supported. Method: 'HasRefParam'"); 47 | pipeClientWithCallback.Dispose(); 48 | } 49 | 50 | public static async Task RunServerAsync(PipeServerWithCallback pipeServerWithCallback) 51 | { 52 | await pipeServerWithCallback.WaitForConnectionAsync().ConfigureAwait(false); 53 | 54 | string concatResult = await pipeServerWithCallback.InvokeAsync(c => c.Concatenate("a", "b")).ConfigureAwait(false); 55 | concatResult.ShouldBe("ab"); 56 | 57 | // 100 character string. Concat to make sure that the continuation bit works for varint. 58 | string longString = "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"; 59 | 60 | string longConcatResult = await pipeServerWithCallback.InvokeAsync(c => c.Concatenate(longString, longString)).ConfigureAwait(false); 61 | longConcatResult.ShouldBe(longString + longString); 62 | 63 | await pipeServerWithCallback.WaitForRemotePipeCloseAsync().ConfigureAwait(false); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /PipeMethodCalls/Models/SerializedPipeResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace PipeMethodCalls 6 | { 7 | /// 8 | /// A response sent over the pipe that has been user serialized with an . 9 | /// 10 | /// 11 | /// There are two stages to serialization: 12 | /// 1: User serialization, where typed values are serialized to a byte array. 13 | /// 2: Message serialization, where the message is sent with a custom binary serialization over the pipe. 14 | /// 15 | internal class SerializedPipeResponse : IMessage 16 | { 17 | /// 18 | /// The call ID. 19 | /// 20 | public int CallId { get; private set; } 21 | 22 | /// 23 | /// True if the call succeeded. 24 | /// 25 | public bool Succeeded { get; private set; } 26 | 27 | /// 28 | /// The response data. Valid if Succeeded is true. 29 | /// 30 | /// 31 | /// The method invoker will know what type this is, and with that type the serializer will be able to convert it to and from a byte array. 32 | /// 33 | public byte[] Data { get; private set; } 34 | 35 | /// 36 | /// The error details. Valid if Succeeded is false. 37 | /// 38 | public string Error { get; private set; } 39 | 40 | public override string ToString() 41 | { 42 | var builder = new StringBuilder("SerializedResponse"); 43 | builder.AppendLine(); 44 | builder.Append(" CallId: "); 45 | builder.Append(this.CallId); 46 | builder.AppendLine(); 47 | builder.Append(" Succeeded: "); 48 | builder.Append(this.Succeeded); 49 | builder.AppendLine(); 50 | if (this.Succeeded) 51 | { 52 | builder.Append(" Data: "); 53 | if (this.Data == null) 54 | { 55 | builder.Append(""); 56 | } 57 | else 58 | { 59 | builder.Append("0x"); 60 | builder.Append(Utilities.BytesToHexString(this.Data)); 61 | } 62 | } 63 | else 64 | { 65 | builder.Append(" Error: "); 66 | builder.Append(this.Error); 67 | } 68 | 69 | return builder.ToString(); 70 | } 71 | 72 | /// 73 | /// Creates a new success pipe response. 74 | /// 75 | /// The ID of the call. 76 | /// The returned data. 77 | /// The success pipe response. 78 | public static SerializedPipeResponse Success(int callId, byte[] data) 79 | { 80 | return new SerializedPipeResponse { Succeeded = true, CallId = callId, Data = data }; 81 | } 82 | 83 | /// 84 | /// Creates a new failure pipe response. 85 | /// 86 | /// The ID of the call. 87 | /// The failure message. 88 | /// The failure pipe response. 89 | public static SerializedPipeResponse Failure(int callId, string message) 90 | { 91 | return new SerializedPipeResponse { Succeeded = false, CallId = callId, Error = message }; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /PipeMethodCalls/Models/TypedPipeResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace PipeMethodCalls 6 | { 7 | /// 8 | /// A method response to be sent over the pipe. Contains a typed response value. 9 | /// 10 | internal class TypedPipeResponse 11 | { 12 | /// 13 | /// The call ID. 14 | /// 15 | public int CallId { get; private set; } 16 | 17 | /// 18 | /// True if the call succeeded. 19 | /// 20 | public bool Succeeded { get; private set; } 21 | 22 | /// 23 | /// The response data. Valid if Succeeded is true. 24 | /// 25 | public object Data { get; private set; } 26 | 27 | /// 28 | /// The error details. Valid if Succeeded is false. 29 | /// 30 | public string Error { get; private set; } 31 | 32 | /// 33 | /// Creates a new success pipe response. 34 | /// 35 | /// The ID of the call. 36 | /// The returned data. 37 | /// The success pipe response. 38 | public static TypedPipeResponse Success(int callId, object data) 39 | { 40 | return new TypedPipeResponse { Succeeded = true, CallId = callId, Data = data }; 41 | } 42 | 43 | /// 44 | /// Creates a new failure pipe response. 45 | /// 46 | /// The ID of the call. 47 | /// The failure message. 48 | /// The failure pipe response. 49 | public static TypedPipeResponse Failure(int callId, string message) 50 | { 51 | return new TypedPipeResponse { Succeeded = false, CallId = callId, Error = message }; 52 | } 53 | 54 | /// 55 | /// Gets a string representation of this typed response. 56 | /// 57 | /// A string representation of this typed response. 58 | public override string ToString() 59 | { 60 | var builder = new StringBuilder("Response"); 61 | builder.AppendLine(); 62 | builder.Append(" CallId: "); 63 | builder.Append(this.CallId); 64 | builder.AppendLine(); 65 | builder.Append(" Succeeded: "); 66 | builder.Append(this.Succeeded); 67 | builder.AppendLine(); 68 | if (this.Succeeded) 69 | { 70 | builder.Append(" Data: "); 71 | builder.Append(this.Data?.ToString() ?? ""); 72 | } 73 | else 74 | { 75 | builder.Append(" Error: "); 76 | builder.Append(this.Error); 77 | } 78 | 79 | return builder.ToString(); 80 | } 81 | 82 | /// 83 | /// Serializes the response. 84 | /// 85 | /// The serializer to use. 86 | /// The serialized response. 87 | public SerializedPipeResponse Serialize(IPipeSerializer serializer) 88 | { 89 | if (this.Succeeded) 90 | { 91 | return SerializedPipeResponse.Success(this.CallId, serializer.Serialize(this.Data)); 92 | } 93 | else 94 | { 95 | return SerializedPipeResponse.Failure(this.CallId, this.Error); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /PipeMethodCalls/Invoker/IPipeInvoker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq.Expressions; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace PipeMethodCalls 10 | { 11 | /// 12 | /// Invokes methods on a named pipe. 13 | /// 14 | /// The interface that we will be invoking. 15 | public interface IPipeInvoker 16 | where TRequesting : class 17 | { 18 | /// 19 | /// Invokes a method on the remote endpoint. 20 | /// 21 | /// The method to invoke. 22 | /// A token to cancel the request. 23 | /// Thrown when the invoked method throws an exception. 24 | /// Thrown when there is an issue with the pipe communication. 25 | /// Thrown when the cancellation token is invoked. 26 | Task InvokeAsync(Expression> expression, CancellationToken cancellationToken = default); 27 | 28 | /// 29 | /// Invokes a method on the remote endpoint. 30 | /// 31 | /// The method to invoke. 32 | /// A token to cancel the request. 33 | /// Thrown when the invoked method throws an exception. 34 | /// Thrown when there is an issue with the pipe communication. 35 | /// Thrown when the cancellation token is invoked. 36 | Task InvokeAsync(Expression> expression, CancellationToken cancellationToken = default); 37 | 38 | /// 39 | /// Invokes a method on the remote endpoint. 40 | /// 41 | /// The type of result from the method. 42 | /// The method to invoke. 43 | /// A token to cancel the request. 44 | /// The method result. 45 | /// Thrown when the invoked method throws an exception. 46 | /// Thrown when there is an issue with the pipe communication. 47 | /// Thrown when the cancellation token is invoked. 48 | Task InvokeAsync(Expression>> expression, CancellationToken cancellationToken = default); 49 | 50 | /// 51 | /// Invokes a method on the remote endpoint. 52 | /// 53 | /// The type of result from the method. 54 | /// The method to invoke. 55 | /// A token to cancel the request. 56 | /// The method result. 57 | /// Thrown when the invoked method throws an exception. 58 | /// Thrown when there is an issue with the pipe communication. 59 | /// Thrown when the cancellation token is invoked. 60 | Task InvokeAsync(Expression> expression, CancellationToken cancellationToken = default); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /PipeMethodCalls/PipeMessageProcessor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace PipeMethodCalls 9 | { 10 | /// 11 | /// Handles the pipe connection state and work processing. 12 | /// 13 | internal sealed class PipeMessageProcessor : IDisposable 14 | { 15 | private TaskCompletionSource pipeCloseCompletionSource; 16 | private CancellationTokenSource workLoopCancellationTokenSource; 17 | 18 | private PipeState state = PipeState.NotOpened; 19 | 20 | /// 21 | /// Gets the state of the pipe. 22 | /// 23 | public PipeState State { 24 | get 25 | { 26 | return this.state; 27 | } 28 | private set 29 | { 30 | this.state = value; 31 | this.StateChanged?.Invoke(this, this.state); 32 | } 33 | } 34 | 35 | /// 36 | /// Gets the pipe fault exception. 37 | /// 38 | public Exception PipeFault { get; private set; } 39 | 40 | public event EventHandler StateChanged; 41 | 42 | /// 43 | /// Starts the processing loop on the pipe. 44 | /// 45 | public async void StartProcessing(PipeStreamWrapper pipeStreamWrapper) 46 | { 47 | if (this.State != PipeState.NotOpened) 48 | { 49 | throw new InvalidOperationException("Can only call connect once"); 50 | } 51 | 52 | this.State = PipeState.Connected; 53 | 54 | try 55 | { 56 | this.workLoopCancellationTokenSource = new CancellationTokenSource(); 57 | 58 | // Process messages until canceled. 59 | while (true) 60 | { 61 | this.workLoopCancellationTokenSource.Token.ThrowIfCancellationRequested(); 62 | await pipeStreamWrapper.ProcessMessageAsync(this.workLoopCancellationTokenSource.Token).ConfigureAwait(false); 63 | } 64 | } 65 | catch (OperationCanceledException) 66 | { 67 | // This is a normal dispose. 68 | this.State = PipeState.Closed; 69 | if (this.pipeCloseCompletionSource != null) 70 | { 71 | this.pipeCloseCompletionSource.TrySetResult(null); 72 | } 73 | } 74 | catch (Exception exception) 75 | { 76 | this.State = PipeState.Faulted; 77 | this.PipeFault = exception; 78 | if (this.pipeCloseCompletionSource != null) 79 | { 80 | if (!(exception is IOException)) 81 | { 82 | exception = new IOException("Pipe closed with error", exception); 83 | } 84 | 85 | this.pipeCloseCompletionSource.TrySetException(exception); 86 | } 87 | } 88 | } 89 | 90 | /// 91 | /// Wait for the other end to close the pipe. 92 | /// 93 | /// A token to cancel the operation. 94 | /// Thrown when the pipe has closed due to an unknown error. 95 | /// This does not throw when the other end closes the pipe. 96 | public Task WaitForRemotePipeCloseAsync(CancellationToken cancellationToken = default) 97 | { 98 | if (this.State == PipeState.Closed) 99 | { 100 | return Task.CompletedTask; 101 | } 102 | 103 | if (this.State == PipeState.Faulted) 104 | { 105 | return Task.FromException(this.PipeFault); 106 | } 107 | 108 | if (this.pipeCloseCompletionSource == null) 109 | { 110 | this.pipeCloseCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 111 | } 112 | 113 | cancellationToken.Register(() => 114 | { 115 | this.pipeCloseCompletionSource.TrySetCanceled(); 116 | }); 117 | 118 | return this.pipeCloseCompletionSource.Task; 119 | } 120 | 121 | #region IDisposable Support 122 | private bool disposed = false; 123 | 124 | /// 125 | /// Disposes resources associated with the instance. 126 | /// 127 | public void Dispose() 128 | { 129 | if (!disposed) 130 | { 131 | if (this.workLoopCancellationTokenSource != null) 132 | { 133 | this.workLoopCancellationTokenSource.Cancel(); 134 | } 135 | 136 | disposed = true; 137 | } 138 | } 139 | #endregion 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /TestScenarioRunner/Program.cs: -------------------------------------------------------------------------------- 1 | // See https://aka.ms/new-console-template for more information 2 | using PipeMethodCalls; 3 | using PipeMethodCalls.MessagePack; 4 | using PipeMethodCalls.NetJson; 5 | using TestCore; 6 | using TestScenarioRunner; 7 | 8 | const string PipeName = "testpipe"; 9 | 10 | Scenario? scenario = null; 11 | PipeSide? side = null; 12 | PipeSerializerType? serializerType = null; 13 | 14 | for (int i = 0; i < args.Length; i++) 15 | { 16 | string arg = args[i]; 17 | 18 | if (arg.StartsWith("--", StringComparison.Ordinal)) 19 | { 20 | string argName = arg.Substring(2); 21 | if (i >= args.Length - 1) 22 | { 23 | PrintUsage(); 24 | return 1; 25 | } 26 | 27 | string argValue = args[i + 1]; 28 | switch (argName) 29 | { 30 | case "scenario": 31 | scenario = Enum.Parse(argValue); 32 | break; 33 | case "side": 34 | side = Enum.Parse(argValue); 35 | break; 36 | case "type": 37 | serializerType = Enum.Parse(argValue); 38 | break; 39 | default: 40 | PrintUsage(); 41 | return 1; 42 | } 43 | 44 | i++; 45 | } 46 | } 47 | 48 | if (scenario == null || side == null || serializerType == null) 49 | { 50 | PrintUsage(); 51 | return 1; 52 | } 53 | 54 | IPipeSerializer pipeSerializer; 55 | 56 | switch (serializerType) 57 | { 58 | case PipeSerializerType.NetJson: 59 | pipeSerializer = new NetJsonPipeSerializer(); 60 | break; 61 | case PipeSerializerType.MessagePack: 62 | pipeSerializer = new MessagePackPipeSerializer(); 63 | break; 64 | default: 65 | PrintUsage(); 66 | return 1; 67 | } 68 | 69 | switch (scenario) 70 | { 71 | case Scenario.WithCallback: 72 | if (side == PipeSide.Server) 73 | { 74 | var pipeServerWithCallback = new PipeServerWithCallback(pipeSerializer, PipeName, () => new Adder()); 75 | pipeServerWithCallback.SetLogger(message => Console.WriteLine(message)); 76 | await WithCallbackScenario.RunServerAsync(pipeServerWithCallback); 77 | } 78 | else 79 | { 80 | var pipeClientWithCallback = new PipeClientWithCallback(pipeSerializer, PipeName, () => new Concatenator()); 81 | pipeClientWithCallback.SetLogger(message => Console.WriteLine(message)); 82 | await WithCallbackScenario.RunClientAsync(pipeClientWithCallback); 83 | } 84 | 85 | break; 86 | case Scenario.NoCallback: 87 | if (side == PipeSide.Server) 88 | { 89 | var pipeServer = new PipeServer(pipeSerializer, PipeName, () => new Adder()); 90 | pipeServer.SetLogger(message => Console.WriteLine(message)); 91 | await NoCallbackScenario.RunServerAsync(pipeServer); 92 | } 93 | else 94 | { 95 | var pipeClient = new PipeClient(pipeSerializer, PipeName); 96 | pipeClient.SetLogger(message => Console.WriteLine(message)); 97 | await NoCallbackScenario.RunClientAsync(pipeClient); 98 | } 99 | break; 100 | case Scenario.Performance: 101 | if (side == PipeSide.Server) 102 | { 103 | var pipeServer = new PipeServer(pipeSerializer, PipeName, () => new Adder()); 104 | await PerformanceScenario.RunServerAsync(pipeServer); 105 | } 106 | else 107 | { 108 | var pipeClient = new PipeClient(pipeSerializer, PipeName); 109 | await PerformanceScenario.RunClientAsync(pipeClient); 110 | } 111 | break; 112 | case Scenario.ServerCrash: 113 | if (side == PipeSide.Server) 114 | { 115 | var pipeServer = new PipeServer(pipeSerializer, PipeName, () => new Crasher()); 116 | await ServerCrashScenario.RunServerAsync(pipeServer); 117 | } 118 | else 119 | { 120 | var pipeClient = new PipeClient(pipeSerializer, PipeName); 121 | await ServerCrashScenario.RunClientAsync(pipeClient); 122 | } 123 | break; 124 | default: 125 | PrintUsage(); 126 | return 1; 127 | } 128 | 129 | return 0; 130 | 131 | void PrintUsage() 132 | { 133 | Console.WriteLine("TestScenarioRunner --scenario --side --type "); 134 | Console.WriteLine(" --scenario "); 135 | Console.WriteLine(" WithCallback, NoCallback"); 136 | Console.WriteLine(" --side "); 137 | Console.WriteLine(" Client, Server"); 138 | Console.WriteLine(" --type "); 139 | Console.WriteLine(" NetJson, MessagePack"); 140 | } -------------------------------------------------------------------------------- /PipeMethodCalls.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32112.339 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PipeMethodCalls", "PipeMethodCalls\PipeMethodCalls.csproj", "{3D455E57-BDF2-4D07-9230-4D8E804F49B8}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PipeMethodCalls.NetJson", "PipeMethodCalls.NetJson\PipeMethodCalls.NetJson.csproj", "{0F66A1E5-E5CB-47EE-A1B5-8B30686B2EBF}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PipeMethodCalls.MessagePack", "PipeMethodCalls.MessagePack\PipeMethodCalls.MessagePack.csproj", "{B46297A4-6216-4B6A-A7E6-5EB7420D5617}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestScenarioRunner", "TestScenarioRunner\TestScenarioRunner.csproj", "{30F3D22E-165F-4F2B-BE90-A588629F0086}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestCore", "TestCore\TestCore.csproj", "{E0240541-888E-4A95-BAEE-7019EE8E642E}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PipeMethodCalls.Tests", "PipeMethodCalls.Tests\PipeMethodCalls.Tests.csproj", "{4BEB5599-E924-4342-B6BE-E8EB19FC3EC0}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0293C327-9084-4D6C-8991-8D2E6CD5E1C2}" 19 | ProjectSection(SolutionItems) = preProject 20 | README.md = README.md 21 | EndProjectSection 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PerformanceTestRunner", "PerformanceTestRunner\PerformanceTestRunner.csproj", "{B3EA295A-09C6-484F-A582-A425BF1D8322}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Release|Any CPU = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {3D455E57-BDF2-4D07-9230-4D8E804F49B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {3D455E57-BDF2-4D07-9230-4D8E804F49B8}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {3D455E57-BDF2-4D07-9230-4D8E804F49B8}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {3D455E57-BDF2-4D07-9230-4D8E804F49B8}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {0F66A1E5-E5CB-47EE-A1B5-8B30686B2EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {0F66A1E5-E5CB-47EE-A1B5-8B30686B2EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {0F66A1E5-E5CB-47EE-A1B5-8B30686B2EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {0F66A1E5-E5CB-47EE-A1B5-8B30686B2EBF}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {B46297A4-6216-4B6A-A7E6-5EB7420D5617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {B46297A4-6216-4B6A-A7E6-5EB7420D5617}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {B46297A4-6216-4B6A-A7E6-5EB7420D5617}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {B46297A4-6216-4B6A-A7E6-5EB7420D5617}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {30F3D22E-165F-4F2B-BE90-A588629F0086}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {30F3D22E-165F-4F2B-BE90-A588629F0086}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {30F3D22E-165F-4F2B-BE90-A588629F0086}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {30F3D22E-165F-4F2B-BE90-A588629F0086}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {E0240541-888E-4A95-BAEE-7019EE8E642E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {E0240541-888E-4A95-BAEE-7019EE8E642E}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {E0240541-888E-4A95-BAEE-7019EE8E642E}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {E0240541-888E-4A95-BAEE-7019EE8E642E}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {4BEB5599-E924-4342-B6BE-E8EB19FC3EC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {4BEB5599-E924-4342-B6BE-E8EB19FC3EC0}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {4BEB5599-E924-4342-B6BE-E8EB19FC3EC0}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {4BEB5599-E924-4342-B6BE-E8EB19FC3EC0}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {B3EA295A-09C6-484F-A582-A425BF1D8322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {B3EA295A-09C6-484F-A582-A425BF1D8322}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {B3EA295A-09C6-484F-A582-A425BF1D8322}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {B3EA295A-09C6-484F-A582-A425BF1D8322}.Release|Any CPU.Build.0 = Release|Any CPU 59 | EndGlobalSection 60 | GlobalSection(SolutionProperties) = preSolution 61 | HideSolutionNode = FALSE 62 | EndGlobalSection 63 | GlobalSection(ExtensibilityGlobals) = postSolution 64 | SolutionGuid = {1C275051-D3BD-4B7A-94CE-16ED7A132A28} 65 | EndGlobalSection 66 | EndGlobal 67 | -------------------------------------------------------------------------------- /PipeMethodCalls/Utilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Pipes; 5 | using System.Runtime.InteropServices.ComTypes; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace PipeMethodCalls 10 | { 11 | /// 12 | /// Utility functions. 13 | /// 14 | internal static class Utilities 15 | { 16 | /// 17 | /// Ensures the pipe state is ready to invoke methods. 18 | /// 19 | /// 20 | /// 21 | public static void EnsureReadyForInvoke(PipeState state, Exception pipeFault) 22 | { 23 | if (state == PipeState.NotOpened) 24 | { 25 | throw new IOException("Can only invoke methods after connecting the pipe."); 26 | } 27 | else if (state == PipeState.Closed) 28 | { 29 | throw new IOException("Cannot invoke methods after the pipe has closed."); 30 | } 31 | else if (state == PipeState.Faulted) 32 | { 33 | throw new IOException("Cannot invoke method. Pipe has faulted.", pipeFault); 34 | } 35 | } 36 | 37 | /// 38 | /// Ensures the provided raw server pipe is compatible with method call functionality. 39 | /// 40 | /// Raw pipe stream to test for method call capability. 41 | /// Throws if is not compatible. 42 | /// The pipe also needs to be set up with PipeOptions.Asynchronous but we cannot check for that directly since IsAsync returns the wrong value. 43 | public static void ValidateRawServerPipe(NamedPipeServerStream rawPipe) 44 | { 45 | ValidateRawPipe(rawPipe); 46 | if (rawPipe.TransmissionMode != PipeTransmissionMode.Byte) 47 | { 48 | throw new ArgumentException("Provided pipe cannot be wrapped. Pipe needs to be setup with PipeTransmissionMode.Byte", nameof(rawPipe)); 49 | } 50 | } 51 | 52 | /// 53 | /// Ensures the provided raw client pipe is compatible with method call functionality. 54 | /// 55 | /// Raw pipe stream to test for method call capability. 56 | /// Throws if is not compatible. 57 | /// The pipe also needs to be set up with PipeOptions.Asynchronous but we cannot check for that directly since IsAsync returns the wrong value. 58 | public static void ValidateRawClientPipe(NamedPipeClientStream rawPipe) 59 | { 60 | ValidateRawPipe(rawPipe); 61 | } 62 | 63 | /// 64 | /// Ensures the provided raw pipe is compatible with method call functionality. 65 | /// 66 | /// Raw pipe stream to test for method call capability. 67 | /// Throws if is not compatible. 68 | private static void ValidateRawPipe(PipeStream rawPipe) 69 | { 70 | if (!rawPipe.CanRead || !rawPipe.CanWrite) 71 | { 72 | throw new ArgumentException("Provided pipe cannot be wrapped. Pipe needs to be setup with PipeDirection.InOut", nameof(rawPipe)); 73 | } 74 | } 75 | 76 | /// 77 | /// Convert a byte array to a hex string. 78 | /// 79 | /// The byte array to convert. 80 | /// The hex string. 81 | public static string BytesToHexString(byte[] bytes) 82 | { 83 | char[] c = new char[bytes.Length * 2]; 84 | int b; 85 | for (int i = 0; i < bytes.Length; i++) 86 | { 87 | b = bytes[i] >> 4; 88 | c[i * 2] = (char)(55 + b + (((b - 10) >> 31) & -7)); 89 | b = bytes[i] & 0xF; 90 | c[i * 2 + 1] = (char)(55 + b + (((b - 10) >> 31) & -7)); 91 | } 92 | return new string(c); 93 | } 94 | 95 | /// 96 | /// Gets varint bytes for the given number. 97 | /// 98 | /// See https://protobuf.dev/programming-guides/encoding/#varints 99 | /// The number to encode. 100 | /// Bytes for the given number. 101 | /// Thrown if the value in negative (varints are supposed to be unsigned) 102 | public static byte[] GetVarInt(int value) 103 | { 104 | if (value < 0) 105 | { 106 | throw new ArgumentOutOfRangeException(nameof(value), "Cannot write negative value as varint"); 107 | } 108 | 109 | var bytes = new List(); 110 | 111 | while (true) 112 | { 113 | // Find the least significant 7 bits of the number 114 | int sizeByteAmount = value & 0x7f; 115 | 116 | value = value >> 7; 117 | byte sizeByte = (byte)sizeByteAmount; 118 | bool useContinuationBit = value > 0; 119 | 120 | if (useContinuationBit) 121 | { 122 | // If there is remaining value left, add a continuation bit 123 | sizeByte += 0x80; 124 | } 125 | 126 | bytes.Add(sizeByte); 127 | 128 | if (!useContinuationBit) 129 | { 130 | break; 131 | } 132 | } 133 | 134 | return bytes.ToArray(); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /PipeMethodCalls/Extensions/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Net; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace PipeMethodCalls 10 | { 11 | internal static class StreamExtensions 12 | { 13 | /// 14 | /// Writes a varint to the stream. 15 | /// 16 | /// See https://protobuf.dev/programming-guides/encoding/#varints 17 | /// The stream to write to. 18 | /// The number to write. 19 | /// Thrown if the value is negative. varints are supposed to be unsigned. 20 | public static void WriteVarInt(this Stream stream, int val) 21 | { 22 | byte[] bytes = Utilities.GetVarInt(val); 23 | stream.Write(bytes, 0, bytes.Length); 24 | } 25 | 26 | /// 27 | /// Synchronously reads a varint from a stream. 28 | /// 29 | /// See https://protobuf.dev/programming-guides/encoding/#varints 30 | /// The stream to read from. 31 | /// The read number. 32 | public static int ReadVarInt(this Stream stream) 33 | { 34 | int size = 0; 35 | int sizeByteShift = 0; 36 | while (true) 37 | { 38 | int sizeByte = stream.ReadByte(); 39 | 40 | // Strip the continuation bit 41 | int sizeBytePayload = sizeByte & 0x7f; 42 | 43 | // Least significant bytes are given first, so we shift left 44 | // further as we continue to read size bytes. 45 | int sizeByteAmount = sizeBytePayload << sizeByteShift; 46 | 47 | // Add the 7 payload bytes to the size 48 | size += sizeByteAmount; 49 | 50 | if ((sizeByte & 0x80) > 0) 51 | { 52 | // The continuation bit is set. Keep on reading bytes to determine the size. 53 | sizeByteShift += 7; 54 | } 55 | else 56 | { 57 | // No continuation bit. We're done determining the size. 58 | break; 59 | } 60 | } 61 | 62 | return size; 63 | } 64 | 65 | /// 66 | /// Asynchronously reads a var int from a stream. 67 | /// 68 | /// See https://protobuf.dev/programming-guides/encoding/#varints 69 | /// The stream to read from. 70 | /// A token to cancel the operation. 71 | /// The read number. 72 | public static async Task ReadVarIntAsync(this Stream stream, CancellationToken cancellationToken) 73 | { 74 | byte[] lengthReadBuffer = new byte[1]; 75 | 76 | int size = 0; 77 | int sizeByteShift = 0; 78 | while (true) 79 | { 80 | await stream.ReadAsync(lengthReadBuffer, 0, 1, cancellationToken).ConfigureAwait(false); 81 | int sizeByte = lengthReadBuffer[0]; 82 | 83 | // Strip the continuation bit 84 | int sizeBytePayload = sizeByte & 0x7f; 85 | 86 | // Least significant bytes are given first, so we shift left 87 | // further as we continue to read size bytes. 88 | int sizeByteAmount = sizeBytePayload << sizeByteShift; 89 | 90 | // Add the 7 payload bytes to the size 91 | size += sizeByteAmount; 92 | 93 | if ((sizeByte & 0x80) > 0) 94 | { 95 | // The continuation bit is set. Keep on reading bytes to determine the size. 96 | sizeByteShift += 7; 97 | } 98 | else 99 | { 100 | // No continuation bit. We're done determining the size. 101 | break; 102 | } 103 | } 104 | 105 | return size; 106 | } 107 | 108 | public static void WriteArray(this Stream stream, byte[][] arr) 109 | { 110 | stream.WriteVarInt(arr.Length); 111 | foreach (byte[] itemBytes in arr) 112 | { 113 | stream.WriteVarInt(itemBytes.Length); 114 | stream.Write(itemBytes, 0, itemBytes.Length); 115 | } 116 | } 117 | 118 | public static byte[][] ReadArray(this Stream stream) 119 | { 120 | int arrayLength = stream.ReadVarInt(); 121 | byte[][] result = new byte[arrayLength][]; 122 | for (int i = 0; i < arrayLength; i++) 123 | { 124 | int payloadLength = stream.ReadVarInt(); 125 | byte[] payloadBytes = new byte[payloadLength]; 126 | stream.Read(payloadBytes, 0, payloadLength); 127 | result[i] = payloadBytes; 128 | } 129 | 130 | return result; 131 | } 132 | 133 | public static void WriteUtf8String(this Stream stream, string str) 134 | { 135 | byte[] strBytes = Encoding.UTF8.GetBytes(str); 136 | stream.Write(strBytes, 0, strBytes.Length); 137 | 138 | // Null-terminate 139 | stream.WriteByte(0); 140 | } 141 | 142 | public static string ReadUtf8String(this Stream stream) 143 | { 144 | long originalPosition = stream.Position; 145 | while (stream.ReadByte() != 0) 146 | { 147 | } 148 | 149 | long positionAfterReadingZero = stream.Position; 150 | 151 | // Go back to where we started and read as a string 152 | stream.Seek(originalPosition, SeekOrigin.Begin); 153 | 154 | byte[] utf8Bytes = new byte[positionAfterReadingZero - originalPosition - 1]; 155 | 156 | stream.Read(utf8Bytes, 0, utf8Bytes.Length); 157 | 158 | // Read the null to get us past it 159 | stream.ReadByte(); 160 | 161 | return Encoding.UTF8.GetString(utf8Bytes); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /PipeMethodCalls/Extensions/PipeInvokerHostExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq.Expressions; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace PipeMethodCalls 10 | { 11 | /// 12 | /// Extension methods for IPipeInvokerHost 13 | /// 14 | public static class PipeInvokerHostExtensions 15 | { 16 | /// 17 | /// Invokes a method on the remote endpoint. 18 | /// 19 | /// The interface to invoke. 20 | /// The invoker host to run the command on. 21 | /// The method to invoke. 22 | /// A token to cancel the request. 23 | /// Thrown when the invoked method throws an exception. 24 | /// Thrown when there is an issue with the pipe communication. 25 | /// Thrown when the cancellation token is invoked. 26 | public static Task InvokeAsync(this IPipeInvokerHost invokerHost, Expression> expression, CancellationToken cancellationToken = default) 27 | where TRequesting : class 28 | { 29 | EnsureInvokerNonNull(invokerHost.Invoker); 30 | return invokerHost.Invoker.InvokeAsync(expression, cancellationToken); 31 | } 32 | 33 | /// 34 | /// Invokes a method on the remote endpoint. 35 | /// 36 | /// The interface to invoke. 37 | /// The invoker host to run the command on. 38 | /// The method to invoke. 39 | /// A token to cancel the request. 40 | /// Thrown when the invoked method throws an exception. 41 | /// Thrown when there is an issue with the pipe communication. 42 | /// Thrown when the cancellation token is invoked. 43 | public static Task InvokeAsync(this IPipeInvokerHost invokerHost, Expression> expression, CancellationToken cancellationToken = default) 44 | where TRequesting : class 45 | { 46 | EnsureInvokerNonNull(invokerHost.Invoker); 47 | return invokerHost.Invoker.InvokeAsync(expression, cancellationToken); 48 | } 49 | 50 | /// 51 | /// Invokes a method on the remote endpoint. 52 | /// 53 | /// The interface to invoke. 54 | /// The type of result from the method. 55 | /// The invoker host to run the command on. 56 | /// The method to invoke. 57 | /// A token to cancel the request. 58 | /// The method result. 59 | /// Thrown when the invoked method throws an exception. 60 | /// Thrown when there is an issue with the pipe communication. 61 | /// Thrown when the cancellation token is invoked. 62 | public static Task InvokeAsync(this IPipeInvokerHost invokerHost, Expression> expression, CancellationToken cancellationToken = default) 63 | where TRequesting : class 64 | { 65 | EnsureInvokerNonNull(invokerHost.Invoker); 66 | return invokerHost.Invoker.InvokeAsync(expression, cancellationToken); 67 | } 68 | 69 | /// 70 | /// Invokes a method on the remote endpoint. 71 | /// 72 | /// The interface to invoke. 73 | /// The type of result from the method. 74 | /// The invoker host to run the command on. 75 | /// The method to invoke. 76 | /// A token to cancel the request. 77 | /// The method result. 78 | /// Thrown when the invoked method throws an exception. 79 | /// Thrown when there is an issue with the pipe communication. 80 | /// Thrown when the cancellation token is invoked. 81 | public static Task InvokeAsync(this IPipeInvokerHost invokerHost, Expression>> expression, CancellationToken cancellationToken = default) 82 | where TRequesting : class 83 | { 84 | EnsureInvokerNonNull(invokerHost.Invoker); 85 | return invokerHost.Invoker.InvokeAsync(expression, cancellationToken); 86 | } 87 | 88 | /// 89 | /// Ensures the given invoker is not null. 90 | /// 91 | /// The interface to invoke. 92 | /// The invoker to check. 93 | private static void EnsureInvokerNonNull(IPipeInvoker invoker) 94 | where TRequesting : class 95 | { 96 | if (invoker == null) 97 | { 98 | throw new PipeInvokeFailedException("Can only invoke methods after connecting the pipe."); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /PipeMethodCalls/RequestHandler/RequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace PipeMethodCalls 10 | { 11 | /// 12 | /// Handles incoming method requests over a pipe stream. 13 | /// 14 | /// The interface for the method requests. 15 | internal class RequestHandler : IRequestHandler 16 | { 17 | private readonly Func handlerFactoryFunc; 18 | private readonly PipeStreamWrapper pipeStreamWrapper; 19 | private readonly IPipeSerializer serializer; 20 | private readonly Action logger; 21 | 22 | /// 23 | /// Initializes a new instance of the class. 24 | /// 25 | /// The underlying pipe stream wrapper. 26 | /// 27 | /// The serializer to use. 28 | /// The action to run to log events. 29 | public RequestHandler(PipeStreamWrapper pipeStreamWrapper, Func handlerFactoryFunc, IPipeSerializer serializer, Action logger) 30 | { 31 | this.pipeStreamWrapper = pipeStreamWrapper; 32 | this.handlerFactoryFunc = handlerFactoryFunc; 33 | this.serializer = serializer; 34 | this.logger = logger; 35 | this.pipeStreamWrapper.RequestHandler = this; 36 | } 37 | 38 | /// 39 | /// Handles a request message received from a remote endpoint. 40 | /// 41 | /// The request message. 42 | public async void HandleRequest(SerializedPipeRequest request) 43 | { 44 | try 45 | { 46 | SerializedPipeResponse response = await this.HandleSerializedRequestAsync(request).ConfigureAwait(false); 47 | await this.pipeStreamWrapper.SendResponseAsync(response, CancellationToken.None).ConfigureAwait(false); 48 | } 49 | catch (Exception) 50 | { 51 | // If the pipe has closed and can't hear the response, we can't let the other end know about it, so we just eat the exception. 52 | } 53 | } 54 | 55 | /// 56 | /// Handles a request from a remote endpoint and returns a serialized pipe response. 57 | /// 58 | /// The request. 59 | /// The serialized response. 60 | private async Task HandleSerializedRequestAsync(SerializedPipeRequest request) 61 | { 62 | try 63 | { 64 | TypedPipeResponse typedResponse = await this.GetTypedResponseForRequest(request).ConfigureAwait(false); 65 | SerializedPipeResponse response = typedResponse.Serialize(this.serializer); 66 | this.logger.Log(() => "Sending " + typedResponse.ToString()); 67 | return response; 68 | } 69 | catch (Exception exception) 70 | { 71 | this.logger.Log(() => { 72 | TypedPipeResponse typedResponse = TypedPipeResponse.Failure(request.CallId, exception.ToString()); 73 | return "Sending " + typedResponse.ToString(); 74 | }); 75 | return SerializedPipeResponse.Failure(request.CallId, exception.ToString()); 76 | } 77 | } 78 | 79 | /// 80 | /// Handles a request from a remote endpoint and returns a typed pipe response. 81 | /// 82 | /// The request. 83 | /// The typed response. 84 | private async Task GetTypedResponseForRequest(SerializedPipeRequest request) 85 | { 86 | if (this.handlerFactoryFunc == null) 87 | { 88 | return TypedPipeResponse.Failure(request.CallId, $"No handler implementation registered for interface '{typeof(THandling).FullName}' found."); 89 | } 90 | 91 | THandling handlerInstance = this.handlerFactoryFunc(); 92 | if (handlerInstance == null) 93 | { 94 | return TypedPipeResponse.Failure(request.CallId, $"Handler implementation returned null for interface '{typeof(THandling).FullName}'"); 95 | } 96 | 97 | // GetMethod() doesn't seem to reliably work for F# 98 | MethodInfo method = handlerInstance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name.Split('.').Last() == request.MethodName); 99 | if (method == null) 100 | { 101 | return TypedPipeResponse.Failure(request.CallId, $"Method '{request.MethodName}' not found in interface '{typeof(THandling).FullName}'."); 102 | } 103 | 104 | ParameterInfo[] paramInfoList = method.GetParameters(); 105 | if (paramInfoList.Length != request.Parameters.Length) 106 | { 107 | return TypedPipeResponse.Failure(request.CallId, $"Parameter count mismatch for method '{request.MethodName}'."); 108 | } 109 | 110 | Type[] genericArguments = method.GetGenericArguments(); 111 | if (genericArguments.Length != request.GenericArguments.Length) 112 | { 113 | return TypedPipeResponse.Failure(request.CallId, $"Generic argument count mismatch for method '{request.MethodName}'."); 114 | } 115 | 116 | if (paramInfoList.Any(info => info.IsOut || info.ParameterType.IsByRef)) 117 | { 118 | return TypedPipeResponse.Failure(request.CallId, $"ref parameters are not supported. Method: '{request.MethodName}'"); 119 | } 120 | 121 | object[] parameters = new object[paramInfoList.Length]; 122 | for (int i = 0; i < parameters.Length; i++) 123 | { 124 | Type destType = paramInfoList[i].ParameterType; 125 | if (destType.IsGenericParameter) 126 | { 127 | destType = request.GenericArguments[destType.GenericParameterPosition]; 128 | } 129 | 130 | byte[] parameterBytes = request.Parameters[i]; 131 | parameters[i] = this.serializer.Deserialize(parameterBytes, destType); 132 | } 133 | 134 | try 135 | { 136 | // Log - handling request 137 | this.logger.Log(() => 138 | { 139 | TypedPipeRequest typedRequest = new TypedPipeRequest 140 | { 141 | CallId = request.CallId, 142 | MethodName = request.MethodName, 143 | Parameters = parameters, 144 | GenericArguments = request.GenericArguments 145 | }; 146 | 147 | return "Received " + typedRequest.ToString(); 148 | }); 149 | 150 | if (method.IsGenericMethod) 151 | { 152 | method = method.MakeGenericMethod(request.GenericArguments); 153 | } 154 | 155 | object resultOrTask = method.Invoke(handlerInstance, parameters); 156 | object result; 157 | 158 | if (resultOrTask is Task) 159 | { 160 | await ((Task)resultOrTask).ConfigureAwait(false); 161 | 162 | var resultProperty = resultOrTask.GetType().GetProperty("Result"); 163 | result = resultProperty?.GetValue(resultOrTask); 164 | } 165 | else 166 | { 167 | result = resultOrTask; 168 | } 169 | 170 | return TypedPipeResponse.Success(request.CallId, result); 171 | } 172 | catch (Exception exception) 173 | { 174 | return TypedPipeResponse.Failure(request.CallId, exception.ToString()); 175 | } 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /PipeMethodCalls/Endpoints/PipeServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Pipes; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace PipeMethodCalls 8 | { 9 | /// 10 | /// A named pipe server. 11 | /// 12 | /// The interface for requests that this server will be handling. 13 | public class PipeServer : IPipeServer, IDisposable 14 | where THandling : class 15 | { 16 | private readonly IPipeSerializer serializer; 17 | private readonly string pipeName; 18 | private readonly Func handlerFactoryFunc; 19 | private readonly PipeOptions? options; 20 | private readonly int maxNumberOfServerInstances; 21 | private NamedPipeServerStream rawPipeStream; 22 | private Action logger; 23 | private PipeMessageProcessor messageProcessor = new PipeMessageProcessor(); 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// 29 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 30 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 31 | /// 32 | /// The pipe name. 33 | /// A factory function to provide the handler implementation. 34 | /// Extra options for the pipe. 35 | /// The maximum number of server instances that can be created for this pipe name. -1 for unlimited. Default is 1. 36 | public PipeServer(IPipeSerializer serializer, string pipeName, Func handlerFactoryFunc, PipeOptions? options = null, int maxNumberOfServerInstances = 1) 37 | { 38 | this.serializer = serializer; 39 | this.pipeName = pipeName; 40 | this.handlerFactoryFunc = handlerFactoryFunc; 41 | this.options = options; 42 | this.maxNumberOfServerInstances = maxNumberOfServerInstances; 43 | } 44 | 45 | /// 46 | /// Initializes a new instance of the class. 47 | /// 48 | /// 49 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 50 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 51 | /// 52 | /// Raw pipe stream to wrap with method call capability. Must be set up with PipeDirection - , PipeOptions - , and PipeTransmissionMode - 53 | /// A factory function to provide the handler implementation. 54 | /// Provided pipe cannot be wrapped. Provided pipe must be setup with the following: PipeDirection - , PipeOptions - , and PipeTransmissionMode - 55 | public PipeServer(IPipeSerializer serializer, NamedPipeServerStream rawPipe, Func handlerFactoryFunc) 56 | { 57 | Utilities.ValidateRawServerPipe(rawPipe); 58 | 59 | this.serializer = serializer; 60 | this.rawPipeStream = rawPipe; 61 | this.handlerFactoryFunc = handlerFactoryFunc; 62 | } 63 | 64 | /// 65 | /// Get the raw named pipe. This will automatically create if it hasn't been instantiated yet and is accessed. 66 | /// 67 | public NamedPipeServerStream RawPipe 68 | { 69 | get 70 | { 71 | if (this.rawPipeStream == null) 72 | { 73 | this.CreatePipe(); 74 | } 75 | 76 | return this.rawPipeStream; 77 | } 78 | } 79 | 80 | /// 81 | /// Gets the state of the pipe. 82 | /// 83 | public PipeState State => this.messageProcessor.State; 84 | 85 | /// 86 | /// Sets up the given action as a logger for the module. 87 | /// 88 | /// The logger action. 89 | public void SetLogger(Action logger) 90 | { 91 | this.logger = logger; 92 | } 93 | 94 | /// 95 | /// Waits for a client to connect to the pipe. 96 | /// 97 | /// A token to cancel the request. 98 | /// Thrown when the connection fails. 99 | public async Task WaitForConnectionAsync(CancellationToken cancellationToken = default) 100 | { 101 | if (this.rawPipeStream == null) 102 | { 103 | this.CreatePipe(); 104 | } 105 | 106 | await this.rawPipeStream.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false); 107 | 108 | this.logger.Log(() => "Connected to client."); 109 | 110 | var wrappedPipeStream = new PipeStreamWrapper(this.rawPipeStream, this.logger); 111 | var requestHandler = new RequestHandler(wrappedPipeStream, this.handlerFactoryFunc, this.serializer, this.logger); 112 | 113 | this.messageProcessor.StartProcessing(wrappedPipeStream); 114 | } 115 | 116 | /// 117 | /// Wait for the other end to close the pipe. 118 | /// 119 | /// A token to cancel the operation. 120 | /// Thrown when the pipe has closed due to an unknown error. 121 | /// This does not throw when the other end closes the pipe. 122 | public Task WaitForRemotePipeCloseAsync(CancellationToken cancellationToken = default) 123 | { 124 | return this.messageProcessor.WaitForRemotePipeCloseAsync(cancellationToken); 125 | } 126 | 127 | /// 128 | /// Initialize new named pipe stream with preset options respected 129 | /// 130 | private void CreatePipe() 131 | { 132 | PipeOptions pipeOptionsToPass; 133 | if (this.options == null) 134 | { 135 | pipeOptionsToPass = PipeOptions.Asynchronous; 136 | } 137 | else 138 | { 139 | pipeOptionsToPass = this.options.Value | PipeOptions.Asynchronous; 140 | } 141 | 142 | this.rawPipeStream = new NamedPipeServerStream( 143 | this.pipeName, 144 | PipeDirection.InOut, 145 | this.maxNumberOfServerInstances, 146 | PipeTransmissionMode.Byte, 147 | pipeOptionsToPass); 148 | 149 | this.logger.Log(() => $"Set up named pipe server '{this.pipeName}'."); 150 | } 151 | 152 | #region IDisposable Support 153 | private bool disposed = false; 154 | 155 | protected virtual void Dispose(bool disposing) 156 | { 157 | if (!this.disposed) 158 | { 159 | if (disposing) 160 | { 161 | this.messageProcessor?.Dispose(); 162 | this.rawPipeStream?.Dispose(); 163 | } 164 | 165 | this.disposed = true; 166 | } 167 | } 168 | 169 | /// 170 | /// Closes the pipe. 171 | /// 172 | public void Dispose() 173 | { 174 | this.Dispose(true); 175 | } 176 | #endregion 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /PipeMethodCalls/Endpoints/PipeServerWithCallback.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Pipes; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace PipeMethodCalls 8 | { 9 | /// 10 | /// A named pipe server with a callback channel. 11 | /// 12 | /// The callback channel interface that the client will be handling. 13 | /// The interface for requests that this server will be handling. 14 | public class PipeServerWithCallback : IDisposable, IPipeServerWithCallback, IPipeInvokerHost 15 | where TRequesting : class 16 | where THandling : class 17 | { 18 | private readonly IPipeSerializer serializer; 19 | private readonly string pipeName; 20 | private readonly Func handlerFactoryFunc; 21 | private readonly PipeOptions? options; 22 | private readonly int maxNumberOfServerInstances; 23 | private NamedPipeServerStream rawPipeStream; 24 | private PipeStreamWrapper wrappedPipeStream; 25 | private Action logger; 26 | private PipeMessageProcessor messageProcessor = new PipeMessageProcessor(); 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// 32 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 33 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 34 | /// 35 | /// The pipe name. 36 | /// A factory function to provide the handler implementation. 37 | /// Extra options for the pipe. 38 | /// The maximum number of server instances that can be created for this pipe name. -1 for unlimited. Default is 1. 39 | public PipeServerWithCallback(IPipeSerializer serializer, string pipeName, Func handlerFactoryFunc, PipeOptions? options = null, int maxNumberOfServerInstances = 1) 40 | { 41 | this.serializer = serializer; 42 | this.pipeName = pipeName; 43 | this.handlerFactoryFunc = handlerFactoryFunc; 44 | this.options = options; 45 | this.maxNumberOfServerInstances = maxNumberOfServerInstances; 46 | } 47 | 48 | /// 49 | /// Initializes a new instance of the class. 50 | /// 51 | /// 52 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 53 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 54 | /// 55 | /// Raw pipe stream to wrap with method call capability. Must be set up with PipeDirection - , PipeOptions - , and PipeTransmissionMode - 56 | /// A factory function to provide the handler implementation. 57 | /// Provided pipe cannot be wrapped. Provided pipe must be setup with the following: PipeDirection - , PipeOptions - , and PipeTransmissionMode - 58 | public PipeServerWithCallback(IPipeSerializer serializer, NamedPipeServerStream rawPipe, Func handlerFactoryFunc) 59 | { 60 | Utilities.ValidateRawServerPipe(rawPipe); 61 | 62 | this.serializer = serializer; 63 | this.rawPipeStream = rawPipe; 64 | this.handlerFactoryFunc = handlerFactoryFunc; 65 | } 66 | 67 | /// 68 | /// Get the raw named pipe. This will automatically create if it hasn't been instantiated yet and is accessed. 69 | /// 70 | public NamedPipeServerStream RawPipe 71 | { 72 | get 73 | { 74 | if (this.rawPipeStream == null) 75 | { 76 | this.CreatePipe(); 77 | } 78 | 79 | return this.rawPipeStream; 80 | } 81 | } 82 | 83 | /// 84 | /// Gets the state of the pipe. 85 | /// 86 | public PipeState State => this.messageProcessor.State; 87 | 88 | /// 89 | /// Gets the method invoker. 90 | /// 91 | /// This is null before connecting. 92 | public IPipeInvoker Invoker { get; private set; } 93 | 94 | /// 95 | /// Sets up the given action as a logger for the module. 96 | /// 97 | /// The logger action. 98 | public void SetLogger(Action logger) 99 | { 100 | this.logger = logger; 101 | } 102 | 103 | /// 104 | /// Waits for a client to connect to the pipe. 105 | /// 106 | /// A token to cancel the request. 107 | /// Thrown when the connection fails. 108 | public async Task WaitForConnectionAsync(CancellationToken cancellationToken = default) 109 | { 110 | if (this.rawPipeStream == null) 111 | { 112 | this.CreatePipe(); 113 | } 114 | 115 | await this.rawPipeStream.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false); 116 | 117 | this.logger.Log(() => "Connected to client."); 118 | 119 | this.wrappedPipeStream = new PipeStreamWrapper(this.rawPipeStream, this.logger); 120 | this.Invoker = new MethodInvoker(this.wrappedPipeStream, this.messageProcessor, this.serializer, this.logger); 121 | var requestHandler = new RequestHandler(this.wrappedPipeStream, this.handlerFactoryFunc, this.serializer, this.logger); 122 | 123 | this.messageProcessor.StartProcessing(this.wrappedPipeStream); 124 | } 125 | 126 | /// 127 | /// Wait for the other end to close the pipe. 128 | /// 129 | /// A token to cancel the operation. 130 | /// Thrown when the pipe has closed due to an unknown error. 131 | /// This does not throw when the other end closes the pipe. 132 | public Task WaitForRemotePipeCloseAsync(CancellationToken cancellationToken = default) 133 | { 134 | return this.messageProcessor.WaitForRemotePipeCloseAsync(cancellationToken); 135 | } 136 | 137 | /// 138 | /// Initialize new named pipe stream with preset options respected 139 | /// 140 | private void CreatePipe() 141 | { 142 | PipeOptions pipeOptionsToPass; 143 | if (this.options == null) 144 | { 145 | pipeOptionsToPass = PipeOptions.Asynchronous; 146 | } 147 | else 148 | { 149 | pipeOptionsToPass = this.options.Value | PipeOptions.Asynchronous; 150 | } 151 | 152 | this.rawPipeStream = new NamedPipeServerStream( 153 | this.pipeName, 154 | PipeDirection.InOut, 155 | this.maxNumberOfServerInstances, 156 | PipeTransmissionMode.Byte, 157 | pipeOptionsToPass); 158 | 159 | this.logger.Log(() => $"Set up named pipe server '{this.pipeName}'."); 160 | } 161 | 162 | #region IDisposable Support 163 | private bool disposed = false; 164 | 165 | protected virtual void Dispose(bool disposing) 166 | { 167 | if (!this.disposed) 168 | { 169 | if (disposing) 170 | { 171 | this.messageProcessor?.Dispose(); 172 | this.rawPipeStream?.Dispose(); 173 | } 174 | 175 | this.disposed = true; 176 | } 177 | } 178 | 179 | /// 180 | /// Closes the pipe. 181 | /// 182 | public void Dispose() 183 | { 184 | this.Dispose(true); 185 | } 186 | #endregion 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /PipeMethodCalls/PipeStreamWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.IO; 5 | using System.IO.Pipes; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace PipeMethodCalls 13 | { 14 | /// 15 | /// Wraps the raw pipe stream with messaging and request/response capability. 16 | /// 17 | internal class PipeStreamWrapper 18 | { 19 | private readonly PipeStream stream; 20 | private readonly Action logger; 21 | 22 | // Prevents more than one thread from writing to the pipe stream at once 23 | private readonly SemaphoreSlim writeLock = new SemaphoreSlim(1, 1); 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// The raw pipe stream to wrap. 29 | /// The action to run to log events. 30 | public PipeStreamWrapper(PipeStream stream, Action logger) 31 | { 32 | this.stream = stream; 33 | this.logger = logger; 34 | } 35 | 36 | /// 37 | /// Gets or sets the registered request handler. 38 | /// 39 | public IRequestHandler RequestHandler { get; set; } 40 | 41 | /// 42 | /// Gets or sets the registered response handler. 43 | /// 44 | public IResponseHandler ResponseHandler { get; set; } 45 | 46 | /// 47 | /// Sends a request. 48 | /// 49 | /// The request to send. 50 | /// A token to cancel the operation. 51 | public Task SendRequestAsync(SerializedPipeRequest request, CancellationToken cancellationToken) 52 | { 53 | return this.SendMessageAsync(MessageType.Request, request, cancellationToken); 54 | } 55 | 56 | /// 57 | /// Sends a response. 58 | /// 59 | /// The response to send. 60 | /// A token to cancel the operation. 61 | public Task SendResponseAsync(SerializedPipeResponse response, CancellationToken cancellationToken) 62 | { 63 | return this.SendMessageAsync(MessageType.Response, response, cancellationToken); 64 | } 65 | 66 | /// 67 | /// Sends a message. 68 | /// 69 | /// The type of message. 70 | /// The massage payload object. 71 | /// A token to cancel the operation. 72 | private async Task SendMessageAsync(MessageType messageType, IMessage payloadObject, CancellationToken cancellationToken) 73 | { 74 | // We use a custom binary format to avoid reliance on a specific serialization technology. 75 | // Strings are UTF-8 null-terminated. 76 | // 77 | // # of Bytes - Description 78 | // varint - Payload length - number of bytes in message (excluding the payload length itself) 79 | // 1 - MessageType 80 | // varint - CallId 81 | // If Request 82 | // string - MethodName 83 | // varint - number of parameters 84 | // Repeat N times 85 | // varint - parameter length in bytes 86 | // N - parameter bytes 87 | // varint - generic arguments types length 88 | // Repeat N times 89 | // string - generic argument type 90 | // If Response 91 | // 1 - succeeded boolean 92 | // If succeeded 93 | // varint - result length in bytes 94 | // N - result bytes 95 | // If failed 96 | // string - error 97 | 98 | using (var messageStream = new MemoryStream(35)) 99 | { 100 | messageStream.WriteByte((byte)messageType); 101 | 102 | // Write the call ID 103 | messageStream.WriteVarInt(payloadObject.CallId); 104 | 105 | if (payloadObject.GetType() == typeof(SerializedPipeRequest)) 106 | { 107 | SerializedPipeRequest request = (SerializedPipeRequest)payloadObject; 108 | messageStream.WriteUtf8String(request.MethodName); 109 | 110 | messageStream.WriteArray(request.Parameters); 111 | 112 | messageStream.WriteVarInt(request.GenericArguments.Length); 113 | foreach (Type genericArgument in request.GenericArguments) 114 | { 115 | messageStream.WriteUtf8String(genericArgument.AssemblyQualifiedName); 116 | } 117 | } 118 | else 119 | { 120 | SerializedPipeResponse response = (SerializedPipeResponse)payloadObject; 121 | 122 | messageStream.WriteByte(BitConverter.GetBytes(response.Succeeded)[0]); 123 | 124 | if (response.Succeeded) 125 | { 126 | messageStream.WriteVarInt(response.Data.Length); 127 | messageStream.Write(response.Data, 0, response.Data.Length); 128 | } 129 | else 130 | { 131 | messageStream.WriteUtf8String(response.Error); 132 | } 133 | } 134 | 135 | byte[] messageBytes = messageStream.ToArray(); 136 | 137 | byte[] messageLengthBytes = Utilities.GetVarInt(messageBytes.Length); 138 | 139 | this.logger.Log(() => "Sending message bytes: 0x" + Utilities.BytesToHexString(messageLengthBytes) + Utilities.BytesToHexString(messageBytes)); 140 | 141 | await this.writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); 142 | try 143 | { 144 | // Write out the message length 145 | await this.stream.WriteAsync(messageLengthBytes, 0, messageLengthBytes.Length, cancellationToken).ConfigureAwait(false); 146 | 147 | // Write the message payload 148 | await this.stream.WriteAsync(messageBytes, 0, messageBytes.Length, cancellationToken).ConfigureAwait(false); 149 | } 150 | finally 151 | { 152 | this.writeLock.Release(); 153 | } 154 | } 155 | } 156 | 157 | /// 158 | /// Processes the next message on the input stream. 159 | /// 160 | /// A token to cancel the operation. 161 | public async Task ProcessMessageAsync(CancellationToken cancellationToken) 162 | { 163 | var message = await this.ReadMessageAsync(cancellationToken).ConfigureAwait(false); 164 | 165 | switch (message.messageType) 166 | { 167 | case MessageType.Request: 168 | SerializedPipeRequest request = (SerializedPipeRequest)message.messageObject; 169 | 170 | if (this.RequestHandler == null) 171 | { 172 | throw new InvalidOperationException("Request received but this endpoint is not set up to handle requests."); 173 | } 174 | 175 | this.RequestHandler.HandleRequest(request); 176 | break; 177 | case MessageType.Response: 178 | SerializedPipeResponse response = (SerializedPipeResponse)message.messageObject; 179 | 180 | if (this.ResponseHandler == null) 181 | { 182 | throw new InvalidOperationException("Response received but this endpoint is not set up to make requests."); 183 | } 184 | 185 | this.ResponseHandler.HandleResponse(response); 186 | break; 187 | default: 188 | throw new InvalidOperationException($"Unrecognized message type: {message.messageType}"); 189 | } 190 | } 191 | 192 | /// 193 | /// Reads the message off the input stream. 194 | /// 195 | /// A token to cancel the operation. 196 | /// The read message type and payload. 197 | private async Task<(MessageType messageType, object messageObject)> ReadMessageAsync(CancellationToken cancellationToken) 198 | { 199 | // Read the length to see how long the message is 200 | int messagePayloadLength = await this.stream.ReadVarIntAsync(cancellationToken).ConfigureAwait(false); 201 | if (messagePayloadLength == 0) 202 | { 203 | this.ClosePipe(); 204 | } 205 | 206 | this.logger.Log(() => "Reading message with length " + messagePayloadLength); 207 | 208 | byte[] messagePayloadBytes = new byte[messagePayloadLength]; 209 | 210 | int payloadBytesRead = 0; 211 | while (payloadBytesRead < messagePayloadLength) 212 | { 213 | int readBytes = await this.stream.ReadAsync(messagePayloadBytes, payloadBytesRead, messagePayloadLength - payloadBytesRead, cancellationToken).ConfigureAwait(false); 214 | if (readBytes == 0) 215 | { 216 | this.ClosePipe(); 217 | } 218 | 219 | payloadBytesRead += readBytes; 220 | } 221 | 222 | // We've read in the whole message. Now parse it out into a message object. 223 | using (MemoryStream messageStream = new MemoryStream(messagePayloadBytes)) 224 | { 225 | var messageType = (MessageType)messageStream.ReadByte(); 226 | int callId = messageStream.ReadVarInt(); 227 | 228 | object messageObject; 229 | if (messageType == MessageType.Request) 230 | { 231 | string methodName = messageStream.ReadUtf8String(); 232 | byte[][] parameters = messageStream.ReadArray(); 233 | 234 | int genericArgumentCount = messageStream.ReadVarInt(); 235 | Type[] genericArguments = new Type[genericArgumentCount]; 236 | for (int i = 0; i < genericArgumentCount; i++) 237 | { 238 | string genericArgumentString = messageStream.ReadUtf8String(); 239 | genericArguments[i] = Type.GetType(genericArgumentString, throwOnError: true); 240 | } 241 | 242 | messageObject = new SerializedPipeRequest { CallId = callId, MethodName = methodName, Parameters = parameters, GenericArguments = genericArguments }; 243 | } 244 | else 245 | { 246 | // Response 247 | bool success = BitConverter.ToBoolean(new byte[] { (byte)messageStream.ReadByte() }, 0); 248 | if (success) 249 | { 250 | int resultPayloadLength = messageStream.ReadVarInt(); 251 | byte[] resultBytes = new byte[resultPayloadLength]; 252 | messageStream.Read(resultBytes, 0, resultPayloadLength); 253 | 254 | messageObject = SerializedPipeResponse.Success(callId, resultBytes); 255 | } 256 | else 257 | { 258 | string error = messageStream.ReadUtf8String(); 259 | messageObject = SerializedPipeResponse.Failure(callId, error); 260 | } 261 | } 262 | 263 | return (messageType, messageObject); 264 | } 265 | } 266 | 267 | /// 268 | /// Logs that the pipe has closed and throws exception to triggure graceful closure. 269 | /// 270 | private void ClosePipe() 271 | { 272 | string message = "Pipe has closed."; 273 | this.logger.Log(() => message); 274 | 275 | // OperationCanceledException is handled as pipe closing gracefully. 276 | throw new OperationCanceledException(message); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /PipeMethodCalls/Endpoints/PipeClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Pipes; 4 | using System.Security.Principal; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace PipeMethodCalls 9 | { 10 | /// 11 | /// A named pipe client. 12 | /// 13 | /// The interface that the client will be invoking on the server. 14 | public class PipeClient : IPipeClient, IDisposable 15 | where TRequesting : class 16 | { 17 | private readonly string pipeName; 18 | private readonly string serverName; 19 | private readonly PipeOptions? options; 20 | private readonly TokenImpersonationLevel? impersonationLevel; 21 | private readonly HandleInheritability? inheritability; 22 | private readonly IPipeSerializer serializer; 23 | private NamedPipeClientStream rawPipeStream; 24 | private PipeStreamWrapper wrappedPipeStream; 25 | private Action logger; 26 | private PipeMessageProcessor messageProcessor = new PipeMessageProcessor(); 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// 32 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 33 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 34 | /// 35 | /// The name of the pipe. 36 | public PipeClient(IPipeSerializer serializer, string pipeName) 37 | : this(serializer, ".", pipeName) 38 | { 39 | } 40 | 41 | /// 42 | /// Initializes a new instance of the class. 43 | /// 44 | /// 45 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 46 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 47 | /// 48 | /// The name of the pipe. 49 | /// One of the enumeration values that determines how to open or create the pipe. 50 | /// One of the enumeration values that determines the security impersonation level. 51 | /// One of the enumeration values that determines whether the underlying handle will be inheritable by child processes. 52 | public PipeClient(IPipeSerializer serializer, string pipeName, PipeOptions options, TokenImpersonationLevel impersonationLevel, HandleInheritability inheritability) 53 | : this(serializer, ".", pipeName, options, impersonationLevel, inheritability) 54 | { 55 | } 56 | 57 | /// 58 | /// Initializes a new instance of the class. 59 | /// 60 | /// 61 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 62 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 63 | /// 64 | /// The name of the server to connect to. 65 | /// The name of the pipe. 66 | public PipeClient(IPipeSerializer serializer, string serverName, string pipeName) 67 | { 68 | this.serializer = serializer; 69 | this.pipeName = pipeName; 70 | this.serverName = serverName; 71 | } 72 | 73 | /// 74 | /// Initializes a new instance of the class. 75 | /// 76 | /// 77 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 78 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 79 | /// 80 | /// The name of the server to connect to. 81 | /// The name of the pipe. 82 | /// One of the enumeration values that determines how to open or create the pipe. 83 | /// One of the enumeration values that determines the security impersonation level. 84 | /// One of the enumeration values that determines whether the underlying handle will be inheritable by child processes. 85 | public PipeClient(IPipeSerializer serializer, string serverName, string pipeName, PipeOptions options, TokenImpersonationLevel impersonationLevel, HandleInheritability inheritability) 86 | { 87 | this.serializer = serializer; 88 | this.pipeName = pipeName; 89 | this.serverName = serverName; 90 | this.options = options; 91 | this.impersonationLevel = impersonationLevel; 92 | this.inheritability = inheritability; 93 | } 94 | 95 | /// 96 | /// Initializes a new instance of the class. 97 | /// 98 | /// 99 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 100 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 101 | /// 102 | /// Raw pipe stream to wrap with method call capability. Must be set up with PipeDirection - and PipeOptions - 103 | /// Provided pipe cannot be wrapped. Provided pipe must be setup with the following: PipeDirection - and PipeOptions - 104 | public PipeClient(IPipeSerializer serializer, NamedPipeClientStream rawPipe) 105 | { 106 | Utilities.ValidateRawClientPipe(rawPipe); 107 | 108 | this.serializer = serializer; 109 | this.rawPipeStream = rawPipe; 110 | } 111 | 112 | /// 113 | /// Get the raw named pipe. This will automatically create if it hasn't been instantiated yet and is accessed. 114 | /// 115 | public NamedPipeClientStream RawPipe 116 | { 117 | get 118 | { 119 | if (this.rawPipeStream == null) 120 | { 121 | this.CreatePipe(); 122 | } 123 | 124 | return this.rawPipeStream; 125 | } 126 | } 127 | 128 | /// 129 | /// Gets the state of the pipe. 130 | /// 131 | public PipeState State => this.messageProcessor.State; 132 | 133 | /// 134 | /// Gets the method invoker. 135 | /// 136 | /// This is null before connecting. 137 | public IPipeInvoker Invoker { get; private set; } 138 | 139 | /// 140 | /// Sets up the given action as a logger for the module. 141 | /// 142 | /// The logger action. 143 | public void SetLogger(Action logger) 144 | { 145 | this.logger = logger; 146 | } 147 | 148 | /// 149 | /// Connects the pipe to the server. 150 | /// 151 | /// A token to cancel the request. 152 | /// Thrown when the connection fails. 153 | /// Thrown when the cancellation token has been invoked. 154 | public async Task ConnectAsync(CancellationToken cancellationToken = default) 155 | { 156 | this.PrepareForConnect(); 157 | await this.rawPipeStream.ConnectAsync(cancellationToken).ConfigureAwait(false); 158 | this.PostConnect(); 159 | } 160 | 161 | /// 162 | /// Connects the pipe to the server. 163 | /// 164 | /// The request timeout in milliseconds. 165 | /// A token to cancel the request. 166 | /// Thrown when the connection fails. 167 | /// Thrown when the cancellation token has been invoked. 168 | /// Thrown when the specified timeout has been reached. 169 | public async Task ConnectAsync(int timeoutMs, CancellationToken cancellationToken = default) 170 | { 171 | this.PrepareForConnect(); 172 | await this.rawPipeStream.ConnectAsync(timeoutMs, cancellationToken).ConfigureAwait(false); 173 | this.PostConnect(); 174 | } 175 | 176 | /// 177 | /// Prepares for calling ConnectAsync on the raw pipe. 178 | /// 179 | /// Thrown when not in a valid state. 180 | private void PrepareForConnect() 181 | { 182 | if (this.State != PipeState.NotOpened) 183 | { 184 | throw new InvalidOperationException("Can only call ConnectAsync once"); 185 | } 186 | 187 | if (this.pipeName != null) 188 | { 189 | this.logger.Log(() => $"Connecting to named pipe '{this.pipeName}' on machine '{this.serverName}'"); 190 | } 191 | else 192 | { 193 | this.logger.Log(() => $"Connecting to named pipe"); 194 | } 195 | 196 | if (this.rawPipeStream == null) 197 | { 198 | this.CreatePipe(); 199 | } 200 | } 201 | 202 | /// 203 | /// Finishes setup after the raw pipe has connected. 204 | /// 205 | private void PostConnect() 206 | { 207 | this.logger.Log(() => "Connected."); 208 | 209 | this.wrappedPipeStream = new PipeStreamWrapper(this.rawPipeStream, this.logger); 210 | this.Invoker = new MethodInvoker(this.wrappedPipeStream, this.messageProcessor, this.serializer, this.logger); 211 | 212 | this.messageProcessor.StartProcessing(this.wrappedPipeStream); 213 | } 214 | 215 | /// 216 | /// Wait for the other end to close the pipe. 217 | /// 218 | /// A token to cancel the operation. 219 | /// Thrown when the pipe has closed due to an unknown error. 220 | /// This does not throw when the other end closes the pipe. 221 | public Task WaitForRemotePipeCloseAsync(CancellationToken cancellationToken = default) 222 | { 223 | return this.messageProcessor.WaitForRemotePipeCloseAsync(cancellationToken); 224 | } 225 | 226 | /// 227 | /// Initialize new named pipe stream with preset options respected 228 | /// 229 | private void CreatePipe() 230 | { 231 | if (this.options != null) 232 | { 233 | this.rawPipeStream = new NamedPipeClientStream( 234 | this.serverName, 235 | this.pipeName, 236 | PipeDirection.InOut, 237 | this.options.Value | PipeOptions.Asynchronous, 238 | this.impersonationLevel.Value, 239 | this.inheritability.Value); 240 | } 241 | else 242 | { 243 | this.rawPipeStream = new NamedPipeClientStream(this.serverName, this.pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); 244 | } 245 | } 246 | 247 | #region IDisposable Support 248 | private bool disposed = false; 249 | 250 | protected virtual void Dispose(bool disposing) 251 | { 252 | if (!this.disposed) 253 | { 254 | if (disposing) 255 | { 256 | this.messageProcessor?.Dispose(); 257 | this.rawPipeStream?.Dispose(); 258 | } 259 | 260 | this.disposed = true; 261 | } 262 | } 263 | 264 | /// 265 | /// Closes the pipe. 266 | /// 267 | public void Dispose() 268 | { 269 | this.Dispose(true); 270 | } 271 | #endregion 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /PipeMethodCalls/Invoker/MethodInvoker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Reflection; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace PipeMethodCalls 12 | { 13 | /// 14 | /// Handles invoking methods over a remote pipe stream. 15 | /// 16 | /// The request interface. 17 | internal class MethodInvoker : IResponseHandler, IPipeInvoker 18 | where TRequesting : class 19 | { 20 | private readonly PipeStreamWrapper pipeStreamWrapper; 21 | private readonly PipeMessageProcessor pipeHost; 22 | private readonly IPipeSerializer serializer; 23 | private readonly Action logger; 24 | private Dictionary pendingCalls = new Dictionary(); 25 | 26 | // Lock object for accessing pending calls dictionary. 27 | private object pendingCallsLock = new object(); 28 | 29 | private int currentCall; 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The pipe stream wrapper to use for invocation and response handling. 35 | /// The host for the pipe. 36 | /// The serializer to use on the parameters. 37 | /// The action to run to log events. 38 | public MethodInvoker(PipeStreamWrapper pipeStreamWrapper, PipeMessageProcessor pipeHost, IPipeSerializer serializer, Action logger) 39 | { 40 | this.pipeStreamWrapper = pipeStreamWrapper; 41 | this.pipeStreamWrapper.ResponseHandler = this; 42 | this.serializer = serializer; 43 | this.logger = logger; 44 | this.pipeHost = pipeHost; 45 | 46 | this.pipeHost.StateChanged += this.OnPipeHostStateChanged; 47 | } 48 | 49 | /// 50 | /// Handles a response message received from a remote endpoint. 51 | /// 52 | /// The response message to handle. 53 | public void HandleResponse(SerializedPipeResponse response) 54 | { 55 | PendingCall pendingCall = null; 56 | 57 | lock (this.pendingCallsLock) 58 | { 59 | if (this.pendingCalls.TryGetValue(response.CallId, out pendingCall)) 60 | { 61 | // Call has completed. Remove from pending list. 62 | this.pendingCalls.Remove(response.CallId); 63 | } 64 | else 65 | { 66 | throw new InvalidOperationException($"No pending call found for ID {response.CallId}. {response}"); 67 | } 68 | } 69 | 70 | // Mark method call task as completed. 71 | pendingCall.TaskCompletionSource.TrySetResult(response); 72 | } 73 | 74 | /// 75 | /// Invokes a method on the server. 76 | /// 77 | /// The method to invoke. 78 | /// A token to cancel the request. 79 | /// Thrown when the invoked method throws an exception. 80 | /// Thrown when there is an issue with the pipe communication. 81 | /// Thrown when the cancellation token is invoked. 82 | public async Task InvokeAsync(Expression> expression, CancellationToken cancellationToken = default) 83 | { 84 | // Sync, no result 85 | 86 | Utilities.EnsureReadyForInvoke(this.pipeHost.State, this.pipeHost.PipeFault); 87 | 88 | SerializedPipeResponse response = await this.GetResponseFromExpressionAsync(expression, cancellationToken).ConfigureAwait(false); 89 | 90 | if (!response.Succeeded) 91 | { 92 | throw new PipeInvokeFailedException(response.Error); 93 | } 94 | } 95 | 96 | /// 97 | /// Invokes a method on the server. 98 | /// 99 | /// The method to invoke. 100 | /// A token to cancel the request. 101 | /// Thrown when the invoked method throws an exception. 102 | /// Thrown when there is an issue with the pipe communication. 103 | /// Thrown when the cancellation token is invoked. 104 | public async Task InvokeAsync(Expression> expression, CancellationToken cancellationToken = default) 105 | { 106 | // Async, no result 107 | 108 | Utilities.EnsureReadyForInvoke(this.pipeHost.State, this.pipeHost.PipeFault); 109 | 110 | SerializedPipeResponse response = await this.GetResponseFromExpressionAsync(expression, cancellationToken).ConfigureAwait(false); 111 | 112 | if (!response.Succeeded) 113 | { 114 | throw new PipeInvokeFailedException(response.Error); 115 | } 116 | } 117 | 118 | /// 119 | /// Invokes a method on the server. 120 | /// 121 | /// The type of result from the method. 122 | /// The method to invoke. 123 | /// A token to cancel the request. 124 | /// The method result. 125 | /// Thrown when the invoked method throws an exception. 126 | /// Thrown when there is an issue with the pipe communication. 127 | /// Thrown when the cancellation token is invoked. 128 | public async Task InvokeAsync(Expression> expression, CancellationToken cancellationToken = default) 129 | { 130 | // Sync with result 131 | 132 | Utilities.EnsureReadyForInvoke(this.pipeHost.State, this.pipeHost.PipeFault); 133 | 134 | SerializedPipeResponse response = await this.GetResponseFromExpressionAsync(expression, cancellationToken).ConfigureAwait(false); 135 | TypedPipeResponse typedResponse; 136 | if (response.Succeeded) 137 | { 138 | typedResponse = TypedPipeResponse.Success(response.CallId, this.serializer.Deserialize(response.Data, typeof(TResult))); 139 | } 140 | else 141 | { 142 | typedResponse = TypedPipeResponse.Failure(response.CallId, response.Error); 143 | } 144 | 145 | this.logger.Log(() => 146 | { 147 | var builder = new StringBuilder("Received "); 148 | builder.AppendLine(typedResponse.ToString()); 149 | builder.Append(" Response Bytes: "); 150 | 151 | if (response.Data == null) 152 | { 153 | builder.Append(""); 154 | } 155 | else 156 | { 157 | builder.Append("0x"); 158 | builder.Append(Utilities.BytesToHexString(response.Data)); 159 | } 160 | 161 | return builder.ToString(); 162 | }); 163 | 164 | if (typedResponse.Succeeded) 165 | { 166 | return (TResult)typedResponse.Data; 167 | } 168 | else 169 | { 170 | throw new PipeInvokeFailedException(typedResponse.Error); 171 | } 172 | } 173 | 174 | /// 175 | /// Invokes a method on the server. 176 | /// 177 | /// The type of result from the method. 178 | /// The method to invoke. 179 | /// A token to cancel the request. 180 | /// The method result. 181 | /// Thrown when the invoked method throws an exception. 182 | /// Thrown when there is an issue with the pipe communication. 183 | /// Thrown when the cancellation token is invoked. 184 | public async Task InvokeAsync(Expression>> expression, CancellationToken cancellationToken = default) 185 | { 186 | // Async with result 187 | 188 | Utilities.EnsureReadyForInvoke(this.pipeHost.State, this.pipeHost.PipeFault); 189 | 190 | SerializedPipeResponse response = await this.GetResponseFromExpressionAsync(expression, cancellationToken).ConfigureAwait(false); 191 | 192 | if (response.Succeeded) 193 | { 194 | return (TResult)this.serializer.Deserialize(response.Data, typeof(TResult)); 195 | } 196 | else 197 | { 198 | throw new PipeInvokeFailedException(response.Error); 199 | } 200 | } 201 | 202 | /// 203 | /// Gets a response from the given expression. 204 | /// 205 | /// The expression to execute. 206 | /// A token to cancel the request. 207 | /// A response for the given expression. 208 | private async Task GetResponseFromExpressionAsync(Expression expression, CancellationToken cancellationToken) 209 | { 210 | SerializedPipeRequest request = this.CreateRequest(expression); 211 | return await this.GetResponseAsync(request, cancellationToken).ConfigureAwait(false); 212 | } 213 | 214 | /// 215 | /// Creates a pipe request from the given expression. 216 | /// 217 | /// The expression to execute. 218 | /// The request to send over the pipe to execute that expression. 219 | private SerializedPipeRequest CreateRequest(Expression expression) 220 | { 221 | int callId = Interlocked.Increment(ref this.currentCall); 222 | 223 | if (!(expression is LambdaExpression lamdaExp)) 224 | { 225 | throw new ArgumentException("Only supports lambda expresions, ex: x => x.GetData(a, b)"); 226 | } 227 | 228 | if (!(lamdaExp.Body is MethodCallExpression methodCallExp)) 229 | { 230 | throw new ArgumentException("Only supports calling methods, ex: x => x.GetData(a, b)"); 231 | } 232 | 233 | string methodName = methodCallExp.Method.Name; 234 | 235 | object[] argumentList = methodCallExp.Arguments.Select(ArgumentResolver.GetValue).ToArray(); 236 | Type[] genericArguments = methodCallExp.Method.GetGenericArguments(); 237 | 238 | var typedRequest = new TypedPipeRequest 239 | { 240 | CallId = callId, 241 | MethodName = methodName, 242 | Parameters = argumentList, 243 | GenericArguments = genericArguments 244 | }; 245 | 246 | SerializedPipeRequest serializedRequest = typedRequest.Serialize(this.serializer); 247 | 248 | this.logger.Log(() => 249 | { 250 | var builder = new StringBuilder("Sending "); 251 | builder.Append(typedRequest.ToString()); 252 | if (serializedRequest.Parameters.Length > 0) 253 | { 254 | builder.AppendLine(); 255 | builder.Append(" Serialized Parameters:"); 256 | foreach (byte[] parameterBytes in serializedRequest.Parameters) 257 | { 258 | builder.AppendLine(); 259 | builder.Append(" "); 260 | 261 | if (parameterBytes == null) 262 | { 263 | builder.Append(""); 264 | } 265 | else 266 | { 267 | builder.Append("0x"); 268 | builder.Append(Utilities.BytesToHexString(parameterBytes)); 269 | } 270 | } 271 | } 272 | 273 | return builder.ToString(); 274 | }); 275 | 276 | return serializedRequest; 277 | } 278 | 279 | /// 280 | /// Handles state changes for the pipe host. Cleans up in-progress pending calls if the pipe unexpectedly closes. 281 | /// 282 | /// The sending object. 283 | /// The new pipe state. 284 | private void OnPipeHostStateChanged(object sender, PipeState state) 285 | { 286 | if (state == PipeState.Closed) 287 | { 288 | lock (this.pendingCallsLock) 289 | { 290 | // Throw an IOException on all pending calls. 291 | foreach (var pendingCall in this.pendingCalls.Values) 292 | { 293 | pendingCall.TaskCompletionSource.TrySetException(new IOException("Pipe closed during invocation")); 294 | } 295 | 296 | this.pendingCalls.Clear(); 297 | } 298 | } 299 | } 300 | 301 | /// 302 | /// Gets a pipe response for the given pipe request. 303 | /// 304 | /// The request to send. 305 | /// A token to cancel the request. 306 | /// The pipe response. 307 | private async Task GetResponseAsync(SerializedPipeRequest request, CancellationToken cancellationToken) 308 | { 309 | var pendingCall = new PendingCall(); 310 | 311 | lock (this.pendingCallsLock) 312 | { 313 | this.pendingCalls.Add(request.CallId, pendingCall); 314 | } 315 | 316 | await this.pipeStreamWrapper.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); 317 | 318 | cancellationToken.Register( 319 | () => 320 | { 321 | pendingCall.TaskCompletionSource.TrySetException(new OperationCanceledException("Request has been canceled.")); 322 | }, 323 | false); 324 | 325 | return await pendingCall.TaskCompletionSource.Task.ConfigureAwait(false); 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /PipeMethodCalls/Endpoints/PipeClientWithCallback.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Pipes; 4 | using System.Security.Principal; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace PipeMethodCalls 9 | { 10 | /// 11 | /// A named pipe client with a callback channel. 12 | /// 13 | /// The interface that the client will be invoking on the server. 14 | /// The callback channel interface that this client will be handling. 15 | public class PipeClientWithCallback : IDisposable, IPipeClient 16 | where TRequesting : class 17 | where THandling : class 18 | { 19 | private readonly IPipeSerializer serializer; 20 | private readonly string pipeName; 21 | private readonly string serverName; 22 | private readonly Func handlerFactoryFunc; 23 | private readonly PipeOptions? options; 24 | private readonly TokenImpersonationLevel? impersonationLevel; 25 | private readonly HandleInheritability? inheritability; 26 | private NamedPipeClientStream rawPipeStream; 27 | private PipeStreamWrapper wrappedPipeStream; 28 | private Action logger; 29 | private PipeMessageProcessor messageProcessor = new PipeMessageProcessor(); 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// 35 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 36 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 37 | /// 38 | /// The name of the pipe. 39 | /// A factory function to provide the handler implementation. 40 | public PipeClientWithCallback(IPipeSerializer serializer, string pipeName, Func handlerFactoryFunc) 41 | : this(serializer, ".", pipeName, handlerFactoryFunc) 42 | { 43 | } 44 | 45 | /// 46 | /// Initializes a new instance of the class. 47 | /// 48 | /// 49 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 50 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 51 | /// 52 | /// The name of the pipe. 53 | /// A factory function to provide the handler implementation. 54 | /// One of the enumeration values that determines how to open or create the pipe. 55 | /// One of the enumeration values that determines the security impersonation level. 56 | /// One of the enumeration values that determines whether the underlying handle will be inheritable by child processes. 57 | public PipeClientWithCallback(IPipeSerializer serializer, string pipeName, Func handlerFactoryFunc, PipeOptions options, TokenImpersonationLevel impersonationLevel, HandleInheritability inheritability) 58 | : this(serializer, ".", pipeName, handlerFactoryFunc, options, impersonationLevel, inheritability) 59 | { 60 | } 61 | 62 | /// 63 | /// Initializes a new instance of the class. 64 | /// 65 | /// 66 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 67 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 68 | /// 69 | /// The name of the server to connect to. 70 | /// The name of the pipe. 71 | /// A factory function to provide the handler implementation. 72 | public PipeClientWithCallback(IPipeSerializer serializer, string serverName, string pipeName, Func handlerFactoryFunc) 73 | { 74 | this.serializer = serializer; 75 | this.pipeName = pipeName; 76 | this.serverName = serverName; 77 | this.handlerFactoryFunc = handlerFactoryFunc; 78 | } 79 | 80 | /// 81 | /// Initializes a new instance of the class. 82 | /// 83 | /// 84 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 85 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 86 | /// 87 | /// The name of the server to connect to. 88 | /// The name of the pipe. 89 | /// A factory function to provide the handler implementation. 90 | /// One of the enumeration values that determines how to open or create the pipe. 91 | /// One of the enumeration values that determines the security impersonation level. 92 | /// One of the enumeration values that determines whether the underlying handle will be inheritable by child processes. 93 | public PipeClientWithCallback(IPipeSerializer serializer, string serverName, string pipeName, Func handlerFactoryFunc, PipeOptions options, TokenImpersonationLevel impersonationLevel, HandleInheritability inheritability) 94 | { 95 | this.serializer = serializer; 96 | this.pipeName = pipeName; 97 | this.serverName = serverName; 98 | this.handlerFactoryFunc = handlerFactoryFunc; 99 | this.options = options; 100 | this.impersonationLevel = impersonationLevel; 101 | this.inheritability = inheritability; 102 | } 103 | 104 | /// 105 | /// Initializes a new instance of the class. 106 | /// 107 | /// 108 | /// The serializer to use for the pipe. You can include a library like PipeMethodCalls.NetJson and pass in new NetJsonPipeSerializer(). 109 | /// This will serialize and deserialize method parameters and return values so they can be passed over the pipe. 110 | /// 111 | /// Raw pipe stream to wrap with method call capability. Must be set up with PipeDirection - and PipeOptions - 112 | /// A factory function to provide the handler implementation. 113 | /// Provided pipe cannot be wrapped. Provided pipe must be setup with the following: PipeDirection - and PipeOptions - 114 | public PipeClientWithCallback(IPipeSerializer serializer, NamedPipeClientStream rawPipe, Func handlerFactoryFunc) 115 | { 116 | Utilities.ValidateRawClientPipe(rawPipe); 117 | 118 | this.serializer = serializer; 119 | this.rawPipeStream = rawPipe; 120 | this.handlerFactoryFunc = handlerFactoryFunc; 121 | } 122 | 123 | /// 124 | /// Get the raw named pipe. This will automatically create if it hasn't been instantiated yet and is accessed. 125 | /// 126 | public NamedPipeClientStream RawPipe 127 | { 128 | get 129 | { 130 | if (this.rawPipeStream == null) 131 | { 132 | this.CreatePipe(); 133 | } 134 | 135 | return this.rawPipeStream; 136 | } 137 | } 138 | 139 | /// 140 | /// Gets the state of the pipe. 141 | /// 142 | public PipeState State => this.messageProcessor.State; 143 | 144 | /// 145 | /// Gets the method invoker. 146 | /// 147 | /// This is null before connecting. 148 | public IPipeInvoker Invoker { get; private set; } 149 | 150 | /// 151 | /// Sets up the given action as a logger for the module. 152 | /// 153 | /// The logger action. 154 | public void SetLogger(Action logger) 155 | { 156 | this.logger = logger; 157 | } 158 | 159 | /// 160 | /// Connects the pipe to the server. 161 | /// 162 | /// A token to cancel the request. 163 | /// Thrown when the connection fails. 164 | /// Thrown when the cancellation token has been invoked. 165 | public async Task ConnectAsync(CancellationToken cancellationToken = default) 166 | { 167 | this.PrepareForConnect(); 168 | await this.rawPipeStream.ConnectAsync(cancellationToken).ConfigureAwait(false); 169 | this.PostConnect(); 170 | } 171 | 172 | /// 173 | /// Connects the pipe to the server. 174 | /// 175 | /// The request timeout in milliseconds. 176 | /// A token to cancel the request. 177 | /// Thrown when the connection fails. 178 | /// Thrown when the cancellation token has been invoked. 179 | /// Thrown when the specified timeout has been reached. 180 | public async Task ConnectAsync(int timeoutMs, CancellationToken cancellationToken = default) 181 | { 182 | this.PrepareForConnect(); 183 | await this.rawPipeStream.ConnectAsync(timeoutMs, cancellationToken).ConfigureAwait(false); 184 | this.PostConnect(); 185 | } 186 | 187 | /// 188 | /// Prepares for calling ConnectAsync on the raw pipe. 189 | /// 190 | /// Thrown when not in a valid state. 191 | private void PrepareForConnect() 192 | { 193 | if (this.State != PipeState.NotOpened) 194 | { 195 | throw new InvalidOperationException("Can only call ConnectAsync once"); 196 | } 197 | 198 | if (this.pipeName != null) 199 | { 200 | this.logger.Log(() => $"Connecting to named pipe '{this.pipeName}' on machine '{this.serverName}'"); 201 | } 202 | else 203 | { 204 | this.logger.Log(() => $"Connecting to named pipe"); 205 | } 206 | 207 | if (this.rawPipeStream == null) 208 | { 209 | this.CreatePipe(); 210 | } 211 | } 212 | 213 | /// 214 | /// Finishes setup after the raw pipe has connected. 215 | /// 216 | private void PostConnect() 217 | { 218 | this.logger.Log(() => "Connected."); 219 | 220 | this.wrappedPipeStream = new PipeStreamWrapper(this.rawPipeStream, this.logger); 221 | this.Invoker = new MethodInvoker(this.wrappedPipeStream, this.messageProcessor, this.serializer, this.logger); 222 | var requestHandler = new RequestHandler(this.wrappedPipeStream, handlerFactoryFunc, this.serializer, this.logger); 223 | 224 | this.messageProcessor.StartProcessing(wrappedPipeStream); 225 | } 226 | 227 | /// 228 | /// Wait for the other end to close the pipe. 229 | /// 230 | /// A token to cancel the operation. 231 | /// Thrown when the pipe has closed due to an unknown error. 232 | /// This does not throw when the other end closes the pipe. 233 | public Task WaitForRemotePipeCloseAsync(CancellationToken cancellationToken = default) 234 | { 235 | return this.messageProcessor.WaitForRemotePipeCloseAsync(cancellationToken); 236 | } 237 | 238 | /// 239 | /// Initialize new named pipe stream with preset options respected 240 | /// 241 | private void CreatePipe() 242 | { 243 | if (this.options != null) 244 | { 245 | this.rawPipeStream = new NamedPipeClientStream( 246 | this.serverName, 247 | this.pipeName, 248 | PipeDirection.InOut, 249 | this.options.Value | PipeOptions.Asynchronous, 250 | this.impersonationLevel.Value, 251 | this.inheritability.Value); 252 | } 253 | else 254 | { 255 | this.rawPipeStream = new NamedPipeClientStream(this.serverName, this.pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); 256 | } 257 | } 258 | 259 | #region IDisposable Support 260 | private bool disposed = false; 261 | 262 | protected virtual void Dispose(bool disposing) 263 | { 264 | if (!this.disposed) 265 | { 266 | if (disposing) 267 | { 268 | this.messageProcessor?.Dispose(); 269 | this.rawPipeStream?.Dispose(); 270 | } 271 | 272 | this.disposed = true; 273 | } 274 | } 275 | 276 | /// 277 | /// Closes the pipe. 278 | /// 279 | public void Dispose() 280 | { 281 | this.Dispose(true); 282 | } 283 | #endregion 284 | } 285 | } 286 | --------------------------------------------------------------------------------