├── .github └── workflows │ └── dotnetcore.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── GrpcDotNetNamedPipes.PerfTests ├── GrpcDotNetNamedPipes.PerfTests.csproj ├── GrpcPerformanceTests.cs └── Helpers │ ├── AspNetHttpContextFactory.cs │ ├── AspNetPipeContextFactory.cs │ ├── AspNetUdsContextFactory.cs │ └── MultiChannelClassData.cs ├── GrpcDotNetNamedPipes.Tests ├── GrpcDotNetNamedPipes.Tests.csproj ├── GrpcNamedPipeTests.cs ├── Helpers │ ├── ChannelContext.cs │ ├── ChannelContextFactory.cs │ ├── MultiChannelClassData.cs │ ├── NamedPipeChannelContextFactory.cs │ └── NamedPipeClassData.cs ├── TestService.proto └── TestServiceImpl.cs ├── GrpcDotNetNamedPipes.sln ├── GrpcDotNetNamedPipes ├── GrpcDotNetNamedPipes.csproj ├── Internal │ ├── ClientConnectionContext.cs │ ├── Deadline.cs │ ├── EndOfPipeException.cs │ ├── Helpers │ │ ├── ByteArrayBufferWriter.cs │ │ ├── ByteArrayDeserializationContext.cs │ │ ├── ByteArraySerializationContext.cs │ │ ├── ConnectionLogger.cs │ │ ├── PipeInterop.cs │ │ └── SerializationHelpers.cs │ ├── MessageReader.cs │ ├── PayloadQueue.cs │ ├── PipeReader.cs │ ├── PlatformConfig.cs │ ├── Protocol │ │ ├── NamedPipeTransport.cs │ │ ├── WriteTransaction.cs │ │ ├── WriteTransactionQueue.cs │ │ └── transport.proto │ ├── RequestStreamWriterImpl.cs │ ├── ResponseStreamWriterImpl.cs │ ├── ServerConnectionContext.cs │ ├── ServerStreamPool.cs │ ├── SimpleAsyncLock.cs │ ├── StreamWriterImpl.cs │ └── TransportMessageHandler.cs ├── NamedPipeCallContext.cs ├── NamedPipeChannel.cs ├── NamedPipeChannelOptions.cs ├── NamedPipeErrorEventArgs.cs ├── NamedPipeServer.cs ├── NamedPipeServerOptions.cs └── public_signing_key.snk ├── LICENSE ├── README.md └── nuget.config /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [windows-2022, ubuntu-24.04, macos-14] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Setup .NET 9 14 | uses: actions/setup-dotnet@v4 15 | with: 16 | dotnet-version: 9.0.x 17 | - name: Install dependencies 18 | run: dotnet restore 19 | - name: Build 20 | run: dotnet build GrpcDotNetNamedPipes.Tests --configuration Release --no-restore 21 | - name: Test 22 | run: dotnet test GrpcDotNetNamedPipes.Tests --no-restore -l "console;verbosity=normal" 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | *.sln.ide/ 6 | _ReSharper.*/ 7 | .idea/ 8 | packages/ 9 | artifacts/ 10 | PublishProfiles/ 11 | .vs/ 12 | *.user 13 | *.suo 14 | *.cache 15 | *.docstates 16 | _ReSharper.* 17 | nuget.exe 18 | *net45.csproj 19 | *net451.csproj 20 | *k10.csproj 21 | *.psess 22 | *.vsp 23 | *.pidb 24 | *.userprefs 25 | *DS_Store 26 | *.ncrunchsolution 27 | *.*sdf 28 | *.ipch 29 | *.swp 30 | *~ 31 | .build/ 32 | .testPublish/ 33 | launchSettings.json 34 | BenchmarkDotNet.Artifacts/ 35 | BDN.Generated/ 36 | binaries/ 37 | global.json 38 | .vscode/ 39 | *.binlog 40 | build/feed 41 | .dotnet/ 42 | *.log.txt 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.1.0 4 | - Add NET 8 targets 5 | - Fix an extraneous error when the pipe is closed ([#65](https://github.com/cyanfish/grpc-dotnet-namedpipes/pull/65)) 6 | - Update dependency versions 7 | 8 | ## 3.0.0 9 | - Potential breaking changes: 10 | - Async calls are now fully async (they used to block until the pipe was connected) 11 | - Using a single NamedPipeChannel object is now recommended for parallel calls 12 | - Fix an issue where heavily multi-threaded calls can stall on Windows 13 | - Use custom retry logic for pipe connections for more reliable connections 14 | - Fix a small memory leak 15 | - Fix trailers not being included in responses with errors 16 | - Fix invalid connections staying open forever 17 | 18 | ## 2.1.1 19 | - Improve connection reliability in some cases 20 | - Update dependency versions 21 | 22 | ## 2.1.0 23 | - Improve streaming performance 24 | - Improve connection reliability in some cases 25 | - Implement ServerCallContext.Peer ([#37](https://github.com/cyanfish/grpc-dotnet-namedpipes/issues/37)) 26 | - Set cancellation token on client disconnect ([#30](https://github.com/cyanfish/grpc-dotnet-namedpipes/issues/30)) 27 | - The [readme](https://github.com/cyanfish/grpc-dotnet-namedpipes) now has a comparison matrix for ASP.NET gRPC 28 | 29 | ## 2.0.0 30 | - Add macOS and Linux support 31 | - Change build targets to: net462, net6, netstandard2.0 32 | - Bump assembly version 33 | - Set a default connection timeout of 30s (instead of unlimited) 34 | - Add NamedPipeServer.Error event for previously unlogged errors 35 | 36 | ## 1.4.4 37 | - Add strong naming to the assembly 38 | 39 | ## 1.4.2 40 | - Throw an exception when starting an already-killed server 41 | 42 | ## 1.4.1 43 | - Fix server cleanup issues 44 | 45 | ## 1.4.0 46 | - Fix cancellation issues 47 | 48 | ## 1.3.0 49 | - Add a .NET 5 build with support for pipe security options 50 | 51 | ## 1.2.0 52 | - Update to newer gRPC API (2.32) 53 | 54 | ## 1.1.2 55 | - Fix gRPC dependency version range to <2.32 56 | 57 | ## 1.1.1 58 | - Update project metadata 59 | 60 | ## 1.1.0 61 | - Add a ConnectionTimeout client option (defaults to infinite) 62 | 63 | ## 1.0.2 64 | - Improve server connection error handling 65 | 66 | ## 1.0.1 67 | - Initial public release with core functionality -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.PerfTests/GrpcDotNetNamedPipes.PerfTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8;net462 5 | net8 6 | 13 7 | true 8 | ../GrpcDotNetNamedPipes/public_signing_key.snk 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.PerfTests/GrpcPerformanceTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | using Google.Protobuf; 18 | 19 | namespace GrpcDotNetNamedPipes.PerfTests; 20 | 21 | public class GrpcPerformanceTests 22 | { 23 | private const int Timeout = 60_000; 24 | 25 | private readonly ITestOutputHelper _testOutputHelper; 26 | 27 | public GrpcPerformanceTests(ITestOutputHelper testOutputHelper) 28 | { 29 | _testOutputHelper = testOutputHelper; 30 | } 31 | 32 | [Theory(Timeout = Timeout)] 33 | [ClassData(typeof(MultiChannelWithAspNetClassData))] 34 | public async Task ServerStreamingManyMessagesPerformance(ChannelContextFactory factory) 35 | { 36 | using var ctx = factory.Create(); 37 | var stopwatch = Stopwatch.StartNew(); 38 | var call = ctx.Client.ServerStreaming(new RequestMessage { Value = 100_000 }); 39 | while (await call.ResponseStream.MoveNext()) 40 | { 41 | } 42 | 43 | stopwatch.Stop(); 44 | _testOutputHelper.WriteLine(stopwatch.ElapsedMilliseconds.ToString()); 45 | } 46 | 47 | [Theory(Timeout = Timeout)] 48 | [ClassData(typeof(MultiChannelWithAspNetClassData))] 49 | public void UnarySequentialChannelsPerformance(ChannelContextFactory factory) 50 | { 51 | using var ctx = factory.Create(); 52 | var stopwatch = Stopwatch.StartNew(); 53 | for (int i = 0; i < 1_000; i++) 54 | { 55 | var client = factory.CreateClient(); 56 | client.SimpleUnary(new RequestMessage()); 57 | } 58 | 59 | stopwatch.Stop(); 60 | _testOutputHelper.WriteLine(stopwatch.ElapsedMilliseconds.ToString()); 61 | } 62 | 63 | // Windows seems to stall when 32+ threads try to connect to a named pipe at once 64 | [Theory(Timeout = Timeout, Skip = "named pipes fail with too many parallel channels")] 65 | [ClassData(typeof(MultiChannelWithAspNetClassData))] 66 | public async Task UnaryParallelChannelsPerformance(ChannelContextFactory factory) 67 | { 68 | using var ctx = factory.Create(); 69 | var stopwatch = Stopwatch.StartNew(); 70 | var tasks = new Task[1_000]; 71 | for (int i = 0; i < tasks.Length; i++) 72 | { 73 | var client = factory.CreateClient(); 74 | tasks[i] = client.SimpleUnaryAsync(new RequestMessage()).ResponseAsync; 75 | } 76 | 77 | await Task.WhenAll(tasks); 78 | stopwatch.Stop(); 79 | _testOutputHelper.WriteLine(stopwatch.ElapsedMilliseconds.ToString()); 80 | } 81 | 82 | [Theory(Timeout = Timeout)] 83 | [ClassData(typeof(MultiChannelWithAspNetClassData))] 84 | public void UnarySequentialCallsPerformance(ChannelContextFactory factory) 85 | { 86 | using var ctx = factory.Create(); 87 | var stopwatch = Stopwatch.StartNew(); 88 | for (int i = 0; i < 1_000; i++) 89 | { 90 | ctx.Client.SimpleUnary(new RequestMessage()); 91 | } 92 | 93 | stopwatch.Stop(); 94 | _testOutputHelper.WriteLine(stopwatch.ElapsedMilliseconds.ToString()); 95 | } 96 | 97 | [Theory(Timeout = Timeout)] 98 | [ClassData(typeof(MultiChannelWithAspNetClassData))] 99 | public async Task UnaryParallelCallsPerformance(ChannelContextFactory factory) 100 | { 101 | using var ctx = factory.Create(); 102 | var stopwatch = Stopwatch.StartNew(); 103 | var tasks = new Task[1_000]; 104 | for (int i = 0; i < tasks.Length; i++) 105 | { 106 | tasks[i] = ctx.Client.SimpleUnaryAsync(new RequestMessage()).ResponseAsync; 107 | } 108 | 109 | await Task.WhenAll(tasks); 110 | stopwatch.Stop(); 111 | _testOutputHelper.WriteLine(stopwatch.ElapsedMilliseconds.ToString()); 112 | } 113 | 114 | [Theory(Timeout = Timeout)] 115 | [ClassData(typeof(MultiChannelWithAspNetClassData))] 116 | public void UnaryLargePayloadPerformance(ChannelContextFactory factory) 117 | { 118 | using var ctx = factory.Create(); 119 | var bytes = new byte[100 * 1024 * 1024]; 120 | var byteString = ByteString.CopyFrom(bytes); 121 | var stopwatch = Stopwatch.StartNew(); 122 | ctx.Client.SimpleUnary(new RequestMessage { Binary = byteString }); 123 | stopwatch.Stop(); 124 | _testOutputHelper.WriteLine(stopwatch.ElapsedMilliseconds.ToString()); 125 | } 126 | 127 | [Theory(Timeout = Timeout)] 128 | [ClassData(typeof(MultiChannelWithAspNetClassData))] 129 | public void ChannelColdStartPerformance(ChannelContextFactory factory) 130 | { 131 | // Note: This test needs to be run on its own for accurate cold start measurements. 132 | var stopwatch = Stopwatch.StartNew(); 133 | using var ctx = factory.Create(); 134 | ctx.Client.SimpleUnary(new RequestMessage()); 135 | stopwatch.Stop(); 136 | _testOutputHelper.WriteLine(stopwatch.ElapsedMilliseconds.ToString()); 137 | } 138 | 139 | [Theory(Timeout = Timeout)] 140 | [ClassData(typeof(MultiChannelWithAspNetClassData))] 141 | public void ChannelWarmStartPerformance(ChannelContextFactory factory) 142 | { 143 | using var tempChannel = factory.Create(); 144 | var stopwatch = Stopwatch.StartNew(); 145 | using var ctx = factory.Create(); 146 | ctx.Client.SimpleUnary(new RequestMessage()); 147 | stopwatch.Stop(); 148 | _testOutputHelper.WriteLine(stopwatch.ElapsedMilliseconds.ToString()); 149 | } 150 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.PerfTests/Helpers/AspNetHttpContextFactory.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if NET6_0_OR_GREATER 18 | using System.Net.Http; 19 | using Grpc.Net.Client; 20 | using Microsoft.AspNetCore.Builder; 21 | using Microsoft.AspNetCore.Hosting; 22 | using Microsoft.AspNetCore.Hosting.Server; 23 | using Microsoft.AspNetCore.Hosting.Server.Features; 24 | using Microsoft.AspNetCore.Server.Kestrel.Core; 25 | using Microsoft.Extensions.DependencyInjection; 26 | using Microsoft.Extensions.Hosting; 27 | 28 | namespace GrpcDotNetNamedPipes.PerfTests.Helpers; 29 | 30 | public class AspNetHttpContextFactory : ChannelContextFactory 31 | { 32 | private int _port; 33 | 34 | public override ChannelContext Create(ITestOutputHelper output = null) 35 | { 36 | var builder = WebApplication.CreateBuilder(); 37 | builder.Services.AddGrpc(opts => opts.MaxReceiveMessageSize = int.MaxValue); 38 | builder.WebHost.UseUrls("https://127.0.0.1:0"); 39 | builder.WebHost.ConfigureKestrel(opts => 40 | { 41 | opts.Limits.MaxRequestBodySize = int.MaxValue; 42 | opts.ConfigureEndpointDefaults(c => c.Protocols = HttpProtocols.Http2); 43 | }); 44 | var app = builder.Build(); 45 | app.MapGrpcService(); 46 | app.Start(); 47 | var server = app.Services.GetRequiredService(); 48 | var addressFeature = server.Features.Get(); 49 | foreach (var address in addressFeature.Addresses) 50 | { 51 | _port = int.Parse(address.Split(":").Last()); 52 | } 53 | 54 | return new ChannelContext 55 | { 56 | Impl = new TestServiceImpl(), // TODO: Match instance 57 | Client = CreateClient(output), 58 | OnDispose = () => app.StopAsync() 59 | }; 60 | } 61 | 62 | public override TestService.TestServiceClient CreateClient(ITestOutputHelper output = null) 63 | { 64 | var httpHandler = new HttpClientHandler(); 65 | httpHandler.ServerCertificateCustomValidationCallback = 66 | HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; 67 | return new TestService.TestServiceClient(GrpcChannel.ForAddress($"https://127.0.0.1:{_port}", 68 | new GrpcChannelOptions { HttpHandler = httpHandler, MaxReceiveMessageSize = int.MaxValue })); 69 | } 70 | 71 | public override string ToString() 72 | { 73 | return "aspnet-http"; 74 | } 75 | } 76 | #endif -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.PerfTests/Helpers/AspNetPipeContextFactory.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if NET8_0_OR_GREATER 18 | using System.Net.Http; 19 | using System.Security.Principal; 20 | using Grpc.Net.Client; 21 | using Microsoft.AspNetCore.Builder; 22 | using Microsoft.AspNetCore.Hosting; 23 | using Microsoft.AspNetCore.Server.Kestrel.Core; 24 | using Microsoft.Extensions.DependencyInjection; 25 | using Microsoft.Extensions.Hosting; 26 | 27 | namespace GrpcDotNetNamedPipes.PerfTests.Helpers; 28 | 29 | public class AspNetPipeContextFactory : ChannelContextFactory 30 | { 31 | private string _pipe; 32 | 33 | public override ChannelContext Create(ITestOutputHelper output = null) 34 | { 35 | _pipe = $"pipe/{Guid.NewGuid()}"; 36 | var builder = WebApplication.CreateBuilder(); 37 | builder.Services.AddGrpc(opts => opts.MaxReceiveMessageSize = int.MaxValue); 38 | builder.WebHost.UseUrls("https://127.0.0.1:0"); 39 | builder.WebHost.ConfigureKestrel(opts => 40 | { 41 | opts.Limits.MaxRequestBodySize = int.MaxValue; 42 | opts.ListenNamedPipe(_pipe, c => c.Protocols = HttpProtocols.Http2); 43 | }); 44 | var app = builder.Build(); 45 | app.MapGrpcService(); 46 | app.Start(); 47 | 48 | return new ChannelContext 49 | { 50 | Impl = new TestServiceImpl(), // TODO: Match instance 51 | Client = CreateClient(output), 52 | OnDispose = () => app.StopAsync() 53 | }; 54 | } 55 | 56 | public override TestService.TestServiceClient CreateClient(ITestOutputHelper output = null) 57 | { 58 | var connectionFactory = new NamedPipesConnectionFactory(_pipe); 59 | var socketsHttpHandler = new SocketsHttpHandler 60 | { 61 | ConnectCallback = connectionFactory.ConnectAsync 62 | }; 63 | return new TestService.TestServiceClient(GrpcChannel.ForAddress("http://localhost", 64 | new GrpcChannelOptions { HttpHandler = socketsHttpHandler, MaxReceiveMessageSize = int.MaxValue })); 65 | } 66 | 67 | public override string ToString() 68 | { 69 | return "aspnet-pipe"; 70 | } 71 | 72 | private class NamedPipesConnectionFactory 73 | { 74 | private readonly string pipeName; 75 | 76 | public NamedPipesConnectionFactory(string pipeName) 77 | { 78 | this.pipeName = pipeName; 79 | } 80 | 81 | public async ValueTask ConnectAsync(SocketsHttpConnectionContext _, 82 | CancellationToken cancellationToken = default) 83 | { 84 | var clientStream = new NamedPipeClientStream( 85 | serverName: ".", 86 | pipeName: this.pipeName, 87 | direction: PipeDirection.InOut, 88 | options: PipeOptions.WriteThrough | PipeOptions.Asynchronous, 89 | impersonationLevel: TokenImpersonationLevel.Anonymous); 90 | 91 | try 92 | { 93 | await clientStream.ConnectAsync(cancellationToken).ConfigureAwait(false); 94 | return clientStream; 95 | } 96 | catch 97 | { 98 | clientStream.Dispose(); 99 | throw; 100 | } 101 | } 102 | } 103 | } 104 | #endif -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.PerfTests/Helpers/AspNetUdsContextFactory.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | #if NET6_0_OR_GREATER 18 | using System.Net; 19 | using System.Net.Http; 20 | using System.Net.Sockets; 21 | using Grpc.Net.Client; 22 | using Microsoft.AspNetCore.Builder; 23 | using Microsoft.AspNetCore.Hosting; 24 | using Microsoft.AspNetCore.Server.Kestrel.Core; 25 | using Microsoft.Extensions.DependencyInjection; 26 | using Microsoft.Extensions.Hosting; 27 | 28 | namespace GrpcDotNetNamedPipes.PerfTests.Helpers; 29 | 30 | public class AspNetUdsContextFactory : ChannelContextFactory 31 | { 32 | private string _path; 33 | 34 | public override ChannelContext Create(ITestOutputHelper output = null) 35 | { 36 | _path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); 37 | var builder = WebApplication.CreateBuilder(); 38 | builder.Services.AddGrpc(opts => opts.MaxReceiveMessageSize = int.MaxValue); 39 | builder.WebHost.UseUrls("https://127.0.0.1:0"); 40 | builder.WebHost.ConfigureKestrel(opts => 41 | { 42 | opts.Limits.MaxRequestBodySize = int.MaxValue; 43 | opts.ListenUnixSocket(_path, c => c.Protocols = HttpProtocols.Http2); 44 | }); 45 | var app = builder.Build(); 46 | app.MapGrpcService(); 47 | app.Start(); 48 | 49 | return new ChannelContext 50 | { 51 | Impl = new TestServiceImpl(), // TODO: Match instance 52 | Client = CreateClient(output), 53 | OnDispose = () => app.StopAsync() 54 | }; 55 | } 56 | 57 | public override TestService.TestServiceClient CreateClient(ITestOutputHelper output = null) 58 | { 59 | var udsEndPoint = new UnixDomainSocketEndPoint(_path); 60 | var connectionFactory = new UnixDomainSocketsConnectionFactory(udsEndPoint); 61 | var socketsHttpHandler = new SocketsHttpHandler 62 | { 63 | ConnectCallback = connectionFactory.ConnectAsync 64 | }; 65 | return new TestService.TestServiceClient(GrpcChannel.ForAddress("http://localhost", 66 | new GrpcChannelOptions { HttpHandler = socketsHttpHandler, MaxReceiveMessageSize = int.MaxValue })); 67 | } 68 | 69 | public override string ToString() 70 | { 71 | return "aspnet-uds"; 72 | } 73 | 74 | private class UnixDomainSocketsConnectionFactory 75 | { 76 | private readonly EndPoint endPoint; 77 | 78 | public UnixDomainSocketsConnectionFactory(EndPoint endPoint) 79 | { 80 | this.endPoint = endPoint; 81 | } 82 | 83 | public async ValueTask ConnectAsync(SocketsHttpConnectionContext _, 84 | CancellationToken cancellationToken = default) 85 | { 86 | var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); 87 | 88 | try 89 | { 90 | await socket.ConnectAsync(this.endPoint, cancellationToken).ConfigureAwait(false); 91 | return new NetworkStream(socket, true); 92 | } 93 | catch 94 | { 95 | socket.Dispose(); 96 | throw; 97 | } 98 | } 99 | } 100 | } 101 | #endif -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.PerfTests/Helpers/MultiChannelClassData.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | using System.Collections; 18 | using System.Runtime.InteropServices; 19 | 20 | namespace GrpcDotNetNamedPipes.PerfTests.Helpers; 21 | 22 | public class MultiChannelWithAspNetClassData : IEnumerable 23 | { 24 | public IEnumerator GetEnumerator() 25 | { 26 | yield return new object[] { new NamedPipeChannelContextFactory() }; 27 | #if NET6_0_OR_GREATER 28 | if (RuntimeInformation.OSArchitecture == Architecture.Arm64 && !OperatingSystem.IsLinux()) 29 | { 30 | // No grpc implementation available for comparison 31 | yield break; 32 | } 33 | yield return new object[] { new AspNetHttpContextFactory() }; 34 | yield return new object[] { new AspNetUdsContextFactory() }; 35 | #endif 36 | #if NET8_0_OR_GREATER 37 | yield return new object[] { new AspNetPipeContextFactory() }; 38 | #endif 39 | // The legacy HTTP gRPC is obsolete and much slower than anything else, so not much point comparing 40 | // yield return new object[] { new HttpChannelContextFactory() }; 41 | } 42 | 43 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 44 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.Tests/GrpcDotNetNamedPipes.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8;net462 5 | net8 6 | 13 7 | true 8 | ../GrpcDotNetNamedPipes/public_signing_key.snk 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.Tests/GrpcNamedPipeTests.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | using System.Security.AccessControl; 18 | using System.Security.Principal; 19 | using Google.Protobuf; 20 | 21 | namespace GrpcDotNetNamedPipes.Tests; 22 | 23 | public class GrpcNamedPipeTests 24 | { 25 | private const int Timeout = 60_000; 26 | 27 | private readonly ITestOutputHelper _output; 28 | 29 | public GrpcNamedPipeTests(ITestOutputHelper output) 30 | { 31 | _output = output; 32 | } 33 | 34 | [Theory(Timeout = Timeout)] 35 | [ClassData(typeof(MultiChannelClassData))] 36 | public void SimpleUnary(ChannelContextFactory factory) 37 | { 38 | using var ctx = factory.Create(_output); 39 | var response = ctx.Client.SimpleUnary(new RequestMessage { Value = 10 }); 40 | Assert.Equal(10, response.Value); 41 | Assert.True(ctx.Impl.SimplyUnaryCalled); 42 | } 43 | 44 | [Theory(Timeout = Timeout)] 45 | [ClassData(typeof(MultiChannelClassData))] 46 | public async Task SimpleUnaryAsync(ChannelContextFactory factory) 47 | { 48 | using var ctx = factory.Create(_output); 49 | var response = await ctx.Client.SimpleUnaryAsync(new RequestMessage { Value = 10 }); 50 | Assert.Equal(10, response.Value); 51 | Assert.True(ctx.Impl.SimplyUnaryCalled); 52 | } 53 | 54 | [Theory(Timeout = Timeout)] 55 | [ClassData(typeof(MultiChannelClassData))] 56 | public async Task LargePayload(ChannelContextFactory factory) 57 | { 58 | var bytes = new byte[1024 * 1024]; 59 | new Random(1234).NextBytes(bytes); 60 | var byteString = ByteString.CopyFrom(bytes); 61 | 62 | using var ctx = factory.Create(_output); 63 | var response = await ctx.Client.SimpleUnaryAsync(new RequestMessage { Binary = byteString }); 64 | Assert.Equal(byteString, response.Binary); 65 | } 66 | 67 | [Theory(Timeout = Timeout)] 68 | [ClassData(typeof(MultiChannelClassData))] 69 | public async Task CancelUnary(ChannelContextFactory factory) 70 | { 71 | using var ctx = factory.Create(_output); 72 | var cts = new CancellationTokenSource(); 73 | var responseTask = 74 | ctx.Client.DelayedUnaryAsync(new RequestMessage { Value = 10 }, cancellationToken: cts.Token); 75 | cts.Cancel(); 76 | var exception = await Assert.ThrowsAsync(async () => await responseTask); 77 | Assert.Equal(StatusCode.Cancelled, exception.StatusCode); 78 | } 79 | 80 | [Theory(Timeout = Timeout)] 81 | [ClassData(typeof(MultiChannelClassData))] 82 | public async Task CancelUnaryBeforeCall(ChannelContextFactory factory) 83 | { 84 | using var ctx = factory.Create(_output); 85 | var cts = new CancellationTokenSource(); 86 | cts.Cancel(); 87 | var responseTask = 88 | ctx.Client.SimpleUnaryAsync(new RequestMessage { Value = 10 }, cancellationToken: cts.Token); 89 | var exception = await Assert.ThrowsAsync(async () => await responseTask); 90 | Assert.Equal(StatusCode.Cancelled, exception.StatusCode); 91 | Assert.False(ctx.Impl.SimplyUnaryCalled); 92 | } 93 | 94 | [Theory(Timeout = Timeout)] 95 | [ClassData(typeof(MultiChannelClassData))] 96 | public async Task ThrowingUnary(ChannelContextFactory factory) 97 | { 98 | using var ctx = factory.Create(_output); 99 | var responseTask = ctx.Client.ThrowingUnaryAsync(new RequestMessage { Value = 10 }); 100 | var exception = await Assert.ThrowsAsync(async () => await responseTask); 101 | Assert.Equal(StatusCode.Unknown, exception.StatusCode); 102 | Assert.Equal("Exception was thrown by handler.", exception.Status.Detail); 103 | Assert.Equal("test_value", exception.Trailers.Get("test_key")?.Value); 104 | } 105 | 106 | [Theory(Timeout = Timeout)] 107 | [ClassData(typeof(MultiChannelClassData))] 108 | public async Task ThrowCanceledExceptionUnary(ChannelContextFactory factory) 109 | { 110 | using var ctx = factory.Create(_output); 111 | ctx.Impl.ExceptionToThrow = new OperationCanceledException(); 112 | var responseTask = ctx.Client.ThrowingUnaryAsync(new RequestMessage { Value = 10 }); 113 | var exception = await Assert.ThrowsAsync(async () => await responseTask); 114 | Assert.Equal(StatusCode.Unknown, exception.StatusCode); 115 | Assert.Equal("Exception was thrown by handler.", exception.Status.Detail); 116 | } 117 | 118 | [Theory(Timeout = Timeout)] 119 | [ClassData(typeof(MultiChannelClassData))] 120 | public async Task ThrowRpcExceptionUnary(ChannelContextFactory factory) 121 | { 122 | using var ctx = factory.Create(_output); 123 | ctx.Impl.ExceptionToThrow = new RpcException(new Status(StatusCode.InvalidArgument, "Bad arg")); 124 | var responseTask = ctx.Client.ThrowingUnaryAsync(new RequestMessage { Value = 10 }); 125 | var exception = await Assert.ThrowsAsync(async () => await responseTask); 126 | Assert.Equal(StatusCode.InvalidArgument, exception.StatusCode); 127 | Assert.Equal("Bad arg", exception.Status.Detail); 128 | } 129 | 130 | [Theory(Timeout = Timeout)] 131 | [ClassData(typeof(MultiChannelClassData))] 132 | public async Task ThrowAfterCancelUnary(ChannelContextFactory factory) 133 | { 134 | using var ctx = factory.Create(_output); 135 | var cts = new CancellationTokenSource(); 136 | var responseTask = 137 | ctx.Client.DelayedThrowingUnaryAsync(new RequestMessage { Value = 10 }, cancellationToken: cts.Token); 138 | cts.Cancel(); 139 | var exception = await Assert.ThrowsAsync(async () => await responseTask); 140 | Assert.Equal(StatusCode.Cancelled, exception.StatusCode); 141 | } 142 | 143 | 144 | [Theory(Timeout = Timeout)] 145 | [ClassData(typeof(NamedPipeClassData))] 146 | public async Task UnaryParallelStress(NamedPipeChannelContextFactory factory) 147 | { 148 | using var ctx = factory.Create(_output); 149 | var tasks = new List(); 150 | for (var i = 0; i < 100; i++) 151 | { 152 | tasks.Add(Task.Run(() => ctx.Client.SimpleUnaryAsync(new RequestMessage { Value = 10 }).ResponseAsync)); 153 | } 154 | await Task.WhenAll(tasks); 155 | } 156 | 157 | [Theory(Timeout = Timeout)] 158 | [ClassData(typeof(MultiChannelClassData))] 159 | public async Task ClientStreaming(ChannelContextFactory factory) 160 | { 161 | using var ctx = factory.Create(_output); 162 | var call = ctx.Client.ClientStreaming(); 163 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 3 }); 164 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 2 }); 165 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 1 }); 166 | await call.RequestStream.CompleteAsync(); 167 | var response = await call.ResponseAsync; 168 | Assert.Equal(6, response.Value); 169 | } 170 | 171 | [Theory(Timeout = Timeout)] 172 | [ClassData(typeof(MultiChannelClassData))] 173 | public async Task CancelClientStreaming(ChannelContextFactory factory) 174 | { 175 | using var ctx = factory.Create(_output); 176 | var cts = new CancellationTokenSource(); 177 | var call = ctx.Client.ClientStreaming(cancellationToken: cts.Token); 178 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 1 }); 179 | cts.Cancel(); 180 | await Assert.ThrowsAsync(async () => 181 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 1 })); 182 | await Assert.ThrowsAsync(async () => 183 | await call.RequestStream.CompleteAsync()); 184 | var exception = await Assert.ThrowsAsync(async () => await call.ResponseAsync); 185 | Assert.Equal(StatusCode.Cancelled, exception.StatusCode); 186 | } 187 | 188 | [Theory(Timeout = Timeout)] 189 | [ClassData(typeof(MultiChannelClassData))] 190 | public async Task CancelClientStreamingBeforeCall(ChannelContextFactory factory) 191 | { 192 | using var ctx = factory.Create(_output); 193 | var cts = new CancellationTokenSource(); 194 | cts.Cancel(); 195 | var call = ctx.Client.ClientStreaming(cancellationToken: cts.Token); 196 | await Assert.ThrowsAsync(async () => 197 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 1 })); 198 | var exception = await Assert.ThrowsAsync(async () => await call.ResponseAsync); 199 | Assert.Equal(StatusCode.Cancelled, exception.StatusCode); 200 | } 201 | 202 | [Theory(Timeout = Timeout)] 203 | [ClassData(typeof(MultiChannelClassData))] 204 | public async Task ClientStreamWriteAfterCompletion(ChannelContextFactory factory) 205 | { 206 | using var ctx = factory.Create(_output); 207 | var call = ctx.Client.ClientStreaming(); 208 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 1 }); 209 | await call.RequestStream.CompleteAsync(); 210 | void WriteAction() => call.RequestStream.WriteAsync(new RequestMessage { Value = 1 }); 211 | var exception = Assert.Throws(WriteAction); 212 | Assert.Equal("Request stream has already been completed.", exception.Message); 213 | } 214 | 215 | [Theory(Timeout = Timeout)] 216 | [ClassData(typeof(MultiChannelClassData))] 217 | public async Task ServerStreaming(ChannelContextFactory factory) 218 | { 219 | using var ctx = factory.Create(_output); 220 | var call = ctx.Client.ServerStreaming(new RequestMessage { Value = 3 }); 221 | Assert.True(await call.ResponseStream.MoveNext()); 222 | Assert.Equal(3, call.ResponseStream.Current.Value); 223 | Assert.True(await call.ResponseStream.MoveNext()); 224 | Assert.Equal(2, call.ResponseStream.Current.Value); 225 | Assert.True(await call.ResponseStream.MoveNext()); 226 | Assert.Equal(1, call.ResponseStream.Current.Value); 227 | Assert.False(await call.ResponseStream.MoveNext()); 228 | } 229 | 230 | [Theory(Timeout = Timeout)] 231 | [ClassData(typeof(MultiChannelClassData))] 232 | public async Task CancelServerStreaming(ChannelContextFactory factory) 233 | { 234 | using var ctx = factory.Create(_output); 235 | var cts = new CancellationTokenSource(); 236 | var call = ctx.Client.DelayedServerStreaming(new RequestMessage { Value = 3 }, 237 | cancellationToken: cts.Token); 238 | Assert.True(await call.ResponseStream.MoveNext()); 239 | Assert.Equal(3, call.ResponseStream.Current.Value); 240 | cts.Cancel(); 241 | var exception = await Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext()); 242 | Assert.Equal(StatusCode.Cancelled, exception.StatusCode); 243 | } 244 | 245 | [Theory(Timeout = Timeout)] 246 | [ClassData(typeof(MultiChannelClassData))] 247 | public async Task CancelServerStreamingBeforeCall(ChannelContextFactory factory) 248 | { 249 | using var ctx = factory.Create(_output); 250 | var cts = new CancellationTokenSource(); 251 | cts.Cancel(); 252 | var call = ctx.Client.DelayedServerStreaming(new RequestMessage { Value = 3 }, 253 | cancellationToken: cts.Token); 254 | var exception = await Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext()); 255 | Assert.Equal(StatusCode.Cancelled, exception.StatusCode); 256 | } 257 | 258 | [Theory(Timeout = Timeout)] 259 | [ClassData(typeof(MultiChannelClassData))] 260 | public async Task ThrowingServerStreaming(ChannelContextFactory factory) 261 | { 262 | using var ctx = factory.Create(_output); 263 | var call = ctx.Client.ThrowingServerStreaming(new RequestMessage { Value = 1 }); 264 | Assert.True(await call.ResponseStream.MoveNext()); 265 | var exception = await Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext()); 266 | Assert.Equal(StatusCode.Unknown, exception.StatusCode); 267 | Assert.Equal("Exception was thrown by handler.", exception.Status.Detail); 268 | } 269 | 270 | [Theory(Timeout = Timeout)] 271 | [ClassData(typeof(MultiChannelClassData))] 272 | public async Task ServerStreamWriteAfterCompletion(ChannelContextFactory factory) 273 | { 274 | using var ctx = factory.Create(_output); 275 | var call = ctx.Client.ServerStreaming(new RequestMessage { Value = 1 }); 276 | Assert.True(await call.ResponseStream.MoveNext()); 277 | Assert.False(await call.ResponseStream.MoveNext()); 278 | void WriteAction() => ctx.Impl.ServerStream.WriteAsync(new ResponseMessage { Value = 1 }); 279 | var exception = Assert.Throws(WriteAction); 280 | Assert.Equal("Response stream has already been completed.", exception.Message); 281 | } 282 | 283 | [Theory(Timeout = Timeout)] 284 | [ClassData(typeof(MultiChannelClassData))] 285 | public async Task ServerStreamWriteAfterError(ChannelContextFactory factory) 286 | { 287 | using var ctx = factory.Create(_output); 288 | var call = ctx.Client.ThrowingServerStreaming(new RequestMessage { Value = 1 }); 289 | Assert.True(await call.ResponseStream.MoveNext()); 290 | await Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext()); 291 | void WriteAction() => ctx.Impl.ServerStream.WriteAsync(new ResponseMessage { Value = 1 }); 292 | var exception = Assert.Throws(WriteAction); 293 | Assert.Equal("Response stream has already been completed.", exception.Message); 294 | } 295 | 296 | [Theory(Timeout = Timeout)] 297 | [ClassData(typeof(MultiChannelClassData))] 298 | public async Task DuplexStreaming(ChannelContextFactory factory) 299 | { 300 | using var ctx = factory.Create(_output); 301 | var call = ctx.Client.DuplexStreaming(); 302 | 303 | Assert.True(await call.ResponseStream.MoveNext()); 304 | Assert.Equal(10, call.ResponseStream.Current.Value); 305 | Assert.True(await call.ResponseStream.MoveNext()); 306 | Assert.Equal(11, call.ResponseStream.Current.Value); 307 | 308 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 3 }); 309 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 2 }); 310 | Assert.True(await call.ResponseStream.MoveNext()); 311 | Assert.Equal(3, call.ResponseStream.Current.Value); 312 | Assert.True(await call.ResponseStream.MoveNext()); 313 | Assert.Equal(2, call.ResponseStream.Current.Value); 314 | 315 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 1 }); 316 | await call.RequestStream.CompleteAsync(); 317 | Assert.True(await call.ResponseStream.MoveNext()); 318 | Assert.Equal(1, call.ResponseStream.Current.Value); 319 | Assert.False(await call.ResponseStream.MoveNext()); 320 | } 321 | 322 | [Theory(Timeout = Timeout)] 323 | [ClassData(typeof(MultiChannelClassData))] 324 | public async Task CancelDuplexStreaming(ChannelContextFactory factory) 325 | { 326 | using var ctx = factory.Create(_output); 327 | var cts = new CancellationTokenSource(); 328 | var call = ctx.Client.DelayedDuplexStreaming(cancellationToken: cts.Token); 329 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 1 }); 330 | Assert.True(await call.ResponseStream.MoveNext()); 331 | Assert.Equal(1, call.ResponseStream.Current.Value); 332 | cts.Cancel(); 333 | var exception = await Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext()); 334 | Assert.Equal(StatusCode.Cancelled, exception.StatusCode); 335 | } 336 | 337 | [Theory(Timeout = Timeout)] 338 | [ClassData(typeof(MultiChannelClassData))] 339 | public async Task CancelDuplexStreamingBeforeCall(ChannelContextFactory factory) 340 | { 341 | using var ctx = factory.Create(_output); 342 | var cts = new CancellationTokenSource(); 343 | cts.Cancel(); 344 | var call = ctx.Client.DelayedDuplexStreaming(cancellationToken: cts.Token); 345 | await Assert.ThrowsAsync(async () => 346 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 1 })); 347 | var exception = await Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext()); 348 | Assert.Equal(StatusCode.Cancelled, exception.StatusCode); 349 | } 350 | 351 | [Theory(Timeout = Timeout)] 352 | [ClassData(typeof(MultiChannelClassData))] 353 | public async Task ThrowingDuplexStreaming(ChannelContextFactory factory) 354 | { 355 | using var ctx = factory.Create(_output); 356 | var call = ctx.Client.ThrowingDuplexStreaming(); 357 | await call.RequestStream.WriteAsync(new RequestMessage { Value = 1 }); 358 | await call.RequestStream.CompleteAsync(); 359 | Assert.True(await call.ResponseStream.MoveNext()); 360 | var exception = await Assert.ThrowsAsync(async () => await call.ResponseStream.MoveNext()); 361 | Assert.Equal(StatusCode.Unknown, exception.StatusCode); 362 | Assert.Equal("Exception was thrown by handler.", exception.Status.Detail); 363 | } 364 | 365 | [Theory(Timeout = Timeout)] 366 | [ClassData(typeof(MultiChannelClassData))] 367 | public async Task SetStatus(ChannelContextFactory factory) 368 | { 369 | using var ctx = factory.Create(_output); 370 | var call = ctx.Client.SetStatusAsync(new RequestMessage()); 371 | var exception = await Assert.ThrowsAsync(async () => await call); 372 | Assert.Equal(StatusCode.InvalidArgument, exception.Status.StatusCode); 373 | Assert.Equal("invalid argument", exception.Status.Detail); 374 | } 375 | 376 | [Theory(Timeout = Timeout)] 377 | [ClassData(typeof(MultiChannelClassData))] 378 | public async Task Deadline(ChannelContextFactory factory) 379 | { 380 | using var ctx = factory.Create(_output); 381 | var deadline = DateTime.UtcNow + TimeSpan.FromSeconds(0.1); 382 | var call = ctx.Client.DelayedUnaryAsync(new RequestMessage(), deadline: deadline); 383 | var exception = await Assert.ThrowsAsync(async () => await call); 384 | Assert.Equal(StatusCode.DeadlineExceeded, exception.StatusCode); 385 | } 386 | 387 | [Theory(Timeout = Timeout)] 388 | [ClassData(typeof(MultiChannelClassData))] 389 | public async Task AlreadyExpiredDeadline(ChannelContextFactory factory) 390 | { 391 | using var ctx = factory.Create(_output); 392 | var deadline = DateTime.UtcNow - TimeSpan.FromSeconds(0.1); 393 | var call = ctx.Client.SimpleUnaryAsync(new RequestMessage(), deadline: deadline); 394 | var exception = await Assert.ThrowsAsync(async () => await call); 395 | Assert.Equal(StatusCode.DeadlineExceeded, exception.StatusCode); 396 | Assert.False(ctx.Impl.SimplyUnaryCalled); 397 | } 398 | 399 | [Theory(Timeout = Timeout)] 400 | [ClassData(typeof(MultiChannelClassData))] 401 | public async Task HeadersAndTrailers(ChannelContextFactory factory) 402 | { 403 | using var ctx = factory.Create(_output); 404 | var requestHeaders = new Metadata 405 | { 406 | { "A1", "1" }, 407 | { "A2-bin", new[] { (byte) 2 } }, 408 | }; 409 | var responseHeaders = new Metadata 410 | { 411 | { "B1", "1" }, 412 | { "B2-bin", new[] { (byte) 2 } }, 413 | }; 414 | var responseTrailers = new Metadata 415 | { 416 | { "C1", "1" }, 417 | { "C2-bin", new[] { (byte) 2 } }, 418 | }; 419 | 420 | ctx.Impl.ResponseHeaders = responseHeaders; 421 | ctx.Impl.ResponseTrailers = responseTrailers; 422 | var call = ctx.Client.HeadersTrailersAsync(new RequestMessage { Value = 1 }, requestHeaders); 423 | 424 | var actualResponseHeaders = await call.ResponseHeadersAsync; 425 | await call.ResponseAsync; 426 | var actualResponseTrailers = call.GetTrailers(); 427 | var actualStatus = call.GetStatus(); 428 | var actualRequestHeaders = ctx.Impl.RequestHeaders; 429 | 430 | AssertHasMetadata(requestHeaders, actualRequestHeaders); 431 | AssertHasMetadata(responseHeaders, actualResponseHeaders); 432 | AssertHasMetadata(responseTrailers, actualResponseTrailers); 433 | Assert.Equal(StatusCode.OK, actualStatus.StatusCode); 434 | } 435 | 436 | private void AssertHasMetadata(Metadata expected, Metadata actual) 437 | { 438 | var actualDict = actual.ToDictionary(x => x.Key); 439 | foreach (var expectedEntry in expected) 440 | { 441 | Assert.True(actualDict.ContainsKey(expectedEntry.Key)); 442 | var actualEntry = actualDict[expectedEntry.Key]; 443 | Assert.Equal(expectedEntry.IsBinary, actualEntry.IsBinary); 444 | if (expectedEntry.IsBinary) 445 | { 446 | Assert.Equal(expectedEntry.ValueBytes.AsEnumerable(), actualEntry.ValueBytes.AsEnumerable()); 447 | } 448 | else 449 | { 450 | Assert.Equal(expectedEntry.Value, actualEntry.Value); 451 | } 452 | } 453 | } 454 | 455 | [Theory(Timeout = Timeout)] 456 | [ClassData(typeof(NamedPipeClassData))] 457 | public void CallInfo(NamedPipeChannelContextFactory factory) 458 | { 459 | #if NET6_0_OR_GREATER 460 | if (!OperatingSystem.IsWindows()) return; 461 | #endif 462 | using var ctx = factory.Create(_output); 463 | ctx.Client.GetCallInfo(new RequestMessage()); 464 | Assert.Equal($"net.pipe://localhost/pid/{Process.GetCurrentProcess().Id}", ctx.Impl.Peer); 465 | } 466 | 467 | [Theory(Timeout = Timeout)] 468 | [ClassData(typeof(MultiChannelClassData))] 469 | public void ConnectionTimeout(ChannelContextFactory factory) 470 | { 471 | var client = factory.CreateClient(_output); 472 | var exception = Assert.Throws(() => client.SimpleUnary(new RequestMessage { Value = 10 })); 473 | Assert.Equal(StatusCode.Unavailable, exception.StatusCode); 474 | Assert.Equal("failed to connect to all addresses", exception.Status.Detail); 475 | } 476 | 477 | [Theory(Timeout = Timeout)] 478 | [ClassData(typeof(MultiChannelClassData))] 479 | public async Task DroppedServerConnection(ChannelContextFactory factory) 480 | { 481 | using var ctx = factory.Create(_output); 482 | var task = ctx.Client.DropConnectionAsync(new RequestMessage()); 483 | ctx.Dispose(); 484 | var exception = await Assert.ThrowsAsync(async () => await task); 485 | Assert.Equal(StatusCode.Unavailable, exception.StatusCode); 486 | } 487 | 488 | [Theory(Timeout = Timeout)] 489 | [ClassData(typeof(MultiChannelClassData))] 490 | public async Task DroppedServerConnectionClientStreaming(ChannelContextFactory factory) 491 | { 492 | using var ctx = factory.Create(_output); 493 | var call = ctx.Client.DropConnectionClientStreaming(); 494 | ctx.Dispose(); 495 | await Task.Delay(500); 496 | var exception = await Assert.ThrowsAsync( 497 | async () => await call.RequestStream.WriteAsync(new RequestMessage())); 498 | Assert.Equal(StatusCode.Unavailable, exception.StatusCode); 499 | } 500 | 501 | [Theory(Timeout = Timeout)] 502 | [ClassData(typeof(NamedPipeClassData))] 503 | public async Task DroppedClientConnection(NamedPipeChannelContextFactory factory) 504 | { 505 | NamedPipeClientStream pipe = null; 506 | factory.PipeCallback = p => pipe = p; 507 | using var ctx = factory.Create(_output); 508 | var _ = ctx.Client.WaitForCancellationAsync(new RequestMessage()); 509 | await Task.Delay(100); 510 | pipe.Close(); 511 | await Task.Delay(500); 512 | Assert.True(ctx.Impl.CancellationOccurred); 513 | } 514 | 515 | [Theory(Timeout = Timeout)] 516 | [ClassData(typeof(NamedPipeClassData))] 517 | public async Task CancellationRace(NamedPipeChannelContextFactory factory) 518 | { 519 | using var ctx = factory.Create(_output); 520 | var random = new Random(); 521 | for (int i = 0; i < 200; i++) 522 | { 523 | var cts = new CancellationTokenSource(); 524 | var response = ctx.Client.SimpleUnaryAsync(new RequestMessage { Value = 10 }, cancellationToken: cts.Token); 525 | Thread.Sleep(random.Next(10)); 526 | cts.Cancel(); 527 | // Either a result or cancellation is okay, but we shouldn't get any other errors 528 | try 529 | { 530 | Assert.Equal(10, (await response).Value); 531 | } 532 | catch (RpcException ex) 533 | { 534 | Assert.Equal(StatusCode.Cancelled, ex.StatusCode); 535 | } 536 | } 537 | } 538 | 539 | [Theory(Timeout = Timeout)] 540 | [ClassData(typeof(NamedPipeClassData))] 541 | public async Task CallImmediatelyAfterKillingServer(NamedPipeChannelContextFactory factory) 542 | { 543 | using var ctx = factory.Create(_output); 544 | ctx.Dispose(); 545 | var exception = await Assert.ThrowsAsync( 546 | async () => await ctx.Client.SimpleUnaryAsync(new RequestMessage { Value = 10 })); 547 | Assert.Equal(StatusCode.Unavailable, exception.StatusCode); 548 | Assert.Equal("failed to connect to all addresses", exception.Status.Detail); 549 | } 550 | 551 | [Theory(Timeout = Timeout)] 552 | [ClassData(typeof(MultiChannelClassData))] 553 | public async Task RestartServerAfterCall(ChannelContextFactory factory) 554 | { 555 | using var ctx1 = factory.Create(_output); 556 | var response1 = await ctx1.Client.SimpleUnaryAsync(new RequestMessage { Value = 10 }); 557 | Assert.Equal(10, response1.Value); 558 | 559 | await Task.Delay(500); 560 | ctx1.Dispose(); 561 | await Task.Delay(500); 562 | 563 | using var ctx2 = factory.Create(_output); 564 | var response2 = await ctx2.Client.SimpleUnaryAsync(new RequestMessage { Value = 10 }); 565 | Assert.Equal(10, response2.Value); 566 | } 567 | 568 | [Theory(Timeout = Timeout)] 569 | [ClassData(typeof(MultiChannelClassData))] 570 | public async Task RestartServerAfterNoCalls(ChannelContextFactory factory) 571 | { 572 | using var ctx1 = factory.Create(_output); 573 | 574 | await Task.Delay(500); 575 | ctx1.Dispose(); 576 | await Task.Delay(500); 577 | 578 | using var ctx2 = factory.Create(_output); 579 | var response2 = await ctx2.Client.SimpleUnaryAsync(new RequestMessage { Value = 10 }); 580 | Assert.Equal(10, response2.Value); 581 | } 582 | 583 | [Theory(Timeout = Timeout)] 584 | [ClassData(typeof(NamedPipeClassData))] 585 | public void StartServerAfterStop(NamedPipeChannelContextFactory factory) 586 | { 587 | var server = factory.CreateServer(); 588 | server.Start(); 589 | server.Kill(); 590 | Assert.Throws(() => server.Start()); 591 | } 592 | 593 | #if NET6_0_OR_GREATER || NETFRAMEWORK 594 | [Theory(Timeout = Timeout)] 595 | [ClassData(typeof(NamedPipeClassData))] 596 | public void SimpleUnaryWithACLs(NamedPipeChannelContextFactory factory) 597 | { 598 | #if NET6_0_OR_GREATER 599 | if (!OperatingSystem.IsWindows()) return; 600 | #endif 601 | PipeSecurity security = new PipeSecurity(); 602 | SecurityIdentifier sid = new SecurityIdentifier(WellKnownSidType.WorldSid, null); 603 | security.AddAccessRule(new PipeAccessRule(sid, PipeAccessRights.ReadWrite, AccessControlType.Allow)); 604 | security.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User, PipeAccessRights.FullControl, 605 | AccessControlType.Allow)); 606 | 607 | NamedPipeServerOptions options = new NamedPipeServerOptions { PipeSecurity = security }; 608 | 609 | using var ctx = factory.Create(options, _output); 610 | var response = ctx.Client.SimpleUnary(new RequestMessage { Value = 10 }); 611 | Assert.Equal(10, response.Value); 612 | Assert.True(ctx.Impl.SimplyUnaryCalled); 613 | } 614 | 615 | [Theory(Timeout = Timeout)] 616 | [ClassData(typeof(NamedPipeClassData))] 617 | public void SimpleUnaryWithACLsDenied(NamedPipeChannelContextFactory factory) 618 | { 619 | #if NET6_0_OR_GREATER 620 | if (!OperatingSystem.IsWindows()) return; 621 | #endif 622 | PipeSecurity security = new PipeSecurity(); 623 | SecurityIdentifier sid = new SecurityIdentifier(WellKnownSidType.WorldSid, null); 624 | security.AddAccessRule(new PipeAccessRule(sid, PipeAccessRights.ReadWrite, AccessControlType.Allow)); 625 | security.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User, PipeAccessRights.ReadWrite, 626 | AccessControlType.Deny)); 627 | 628 | NamedPipeServerOptions options = new NamedPipeServerOptions { PipeSecurity = security }; 629 | 630 | using var ctx = factory.Create(options, _output); 631 | var exception = 632 | Assert.Throws(() => ctx.Client.SimpleUnary(new RequestMessage { Value = 10 })); 633 | } 634 | #endif 635 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.Tests/Helpers/ChannelContext.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Tests.Helpers; 18 | 19 | public class ChannelContext : IDisposable 20 | { 21 | public Action OnDispose { get; set; } 22 | 23 | public TestService.TestServiceClient Client { get; set; } 24 | 25 | public TestServiceImpl Impl { get; set; } 26 | 27 | public void Dispose() 28 | { 29 | OnDispose.Invoke(); 30 | } 31 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.Tests/Helpers/ChannelContextFactory.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Tests.Helpers; 18 | 19 | public abstract class ChannelContextFactory 20 | { 21 | public abstract ChannelContext Create(ITestOutputHelper output = null); 22 | public abstract TestService.TestServiceClient CreateClient(ITestOutputHelper output = null); 23 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.Tests/Helpers/MultiChannelClassData.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | using System.Collections; 18 | using System.Runtime.InteropServices; 19 | 20 | namespace GrpcDotNetNamedPipes.Tests.Helpers; 21 | 22 | public class MultiChannelClassData : IEnumerable 23 | { 24 | public IEnumerator GetEnumerator() 25 | { 26 | yield return new object[] { new NamedPipeChannelContextFactory() }; 27 | } 28 | 29 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 30 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.Tests/Helpers/NamedPipeChannelContextFactory.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Tests.Helpers; 18 | 19 | public class NamedPipeChannelContextFactory : ChannelContextFactory 20 | { 21 | private readonly string _pipeName = $"{Guid.NewGuid().ToString().Replace("-", "")}"; 22 | private const int _connectionTimeout = 100; 23 | 24 | public ChannelContext Create(NamedPipeServerOptions options, ITestOutputHelper output) 25 | { 26 | var impl = new TestServiceImpl(); 27 | var server = new NamedPipeServer(_pipeName, options, output != null ? output.WriteLine : null); 28 | TestService.BindService(server.ServiceBinder, impl); 29 | server.Start(); 30 | return new ChannelContext 31 | { 32 | Impl = impl, 33 | Client = CreateClient(output), 34 | OnDispose = () => server.Kill() 35 | }; 36 | } 37 | 38 | public override ChannelContext Create(ITestOutputHelper output = null) 39 | { 40 | return Create(new NamedPipeServerOptions(), output); 41 | } 42 | 43 | public override TestService.TestServiceClient CreateClient(ITestOutputHelper output = null) 44 | { 45 | var channel = new NamedPipeChannel(".", _pipeName, 46 | new NamedPipeChannelOptions { ConnectionTimeout = _connectionTimeout }, 47 | output != null ? output.WriteLine : null); 48 | channel.PipeCallback = PipeCallback; 49 | return new TestService.TestServiceClient(channel); 50 | } 51 | 52 | public Action PipeCallback { get; set; } 53 | 54 | public NamedPipeServer CreateServer() => new(_pipeName, new NamedPipeServerOptions()); 55 | 56 | public override string ToString() 57 | { 58 | return "pipe"; 59 | } 60 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.Tests/Helpers/NamedPipeClassData.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | using System.Collections; 18 | 19 | namespace GrpcDotNetNamedPipes.Tests.Helpers; 20 | 21 | class NamedPipeClassData : IEnumerable 22 | { 23 | public IEnumerator GetEnumerator() 24 | { 25 | yield return new object[] { new NamedPipeChannelContextFactory() }; 26 | } 27 | 28 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 29 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.Tests/TestService.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | 19 | package GrpcDotNetNamedPipes.Tests.Generated; 20 | 21 | service TestService { 22 | rpc SimpleUnary (RequestMessage) returns (ResponseMessage) {} 23 | rpc DelayedUnary (RequestMessage) returns (ResponseMessage) {} 24 | rpc ThrowingUnary (RequestMessage) returns (ResponseMessage) {} 25 | rpc DelayedThrowingUnary (RequestMessage) returns (ResponseMessage) {} 26 | rpc ClientStreaming (stream RequestMessage) returns (ResponseMessage) {} 27 | rpc ServerStreaming (RequestMessage) returns (stream ResponseMessage) {} 28 | rpc DelayedServerStreaming (RequestMessage) returns (stream ResponseMessage) {} 29 | rpc ThrowingServerStreaming (RequestMessage) returns (stream ResponseMessage) {} 30 | rpc DuplexStreaming (stream RequestMessage) returns (stream ResponseMessage) {} 31 | rpc DelayedDuplexStreaming (stream RequestMessage) returns (stream ResponseMessage) {} 32 | rpc ThrowingDuplexStreaming (stream RequestMessage) returns (stream ResponseMessage) {} 33 | rpc HeadersTrailers (RequestMessage) returns (ResponseMessage) {} 34 | rpc SetStatus (RequestMessage) returns (ResponseMessage) {} 35 | rpc GetCallInfo (RequestMessage) returns (ResponseMessage) {} 36 | rpc DropConnection (RequestMessage) returns (ResponseMessage) {} 37 | rpc DropConnectionClientStreaming (stream RequestMessage) returns (ResponseMessage) {} 38 | rpc WaitForCancellation (RequestMessage) returns (ResponseMessage) {} 39 | } 40 | 41 | message RequestMessage { 42 | int32 value = 1; 43 | bytes binary = 2; 44 | } 45 | 46 | message ResponseMessage { 47 | int32 value = 1; 48 | bytes binary = 2; 49 | } 50 | -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.Tests/TestServiceImpl.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Tests; 18 | 19 | public class TestServiceImpl : TestService.TestServiceBase 20 | { 21 | public Exception ExceptionToThrow { get; set; } = new InvalidOperationException("Test exception"); 22 | 23 | public bool SimplyUnaryCalled { get; private set; } 24 | 25 | public IServerStreamWriter ServerStream { get; private set; } 26 | 27 | public override Task SimpleUnary(RequestMessage request, ServerCallContext context) 28 | { 29 | SimplyUnaryCalled = true; 30 | return Task.FromResult(new ResponseMessage 31 | { 32 | Value = request.Value, 33 | Binary = request.Binary 34 | }); 35 | } 36 | 37 | public override async Task DelayedUnary(RequestMessage request, ServerCallContext context) 38 | { 39 | await Task.Delay(2000, context.CancellationToken); 40 | return new ResponseMessage(); 41 | } 42 | 43 | public override Task ThrowingUnary(RequestMessage request, ServerCallContext context) 44 | { 45 | context.ResponseTrailers.Add("test_key", "test_value"); 46 | throw ExceptionToThrow; 47 | } 48 | 49 | public override async Task DelayedThrowingUnary(RequestMessage request, 50 | ServerCallContext context) 51 | { 52 | await Task.Delay(2000, context.CancellationToken); 53 | throw ExceptionToThrow; 54 | } 55 | 56 | public override async Task ClientStreaming(IAsyncStreamReader requestStream, 57 | ServerCallContext context) 58 | { 59 | int total = 0; 60 | while (await requestStream.MoveNext()) 61 | { 62 | total += requestStream.Current.Value; 63 | } 64 | 65 | return new ResponseMessage { Value = total }; 66 | } 67 | 68 | public override async Task ServerStreaming(RequestMessage request, 69 | IServerStreamWriter responseStream, ServerCallContext context) 70 | { 71 | ServerStream = responseStream; 72 | for (int i = request.Value; i > 0; i--) 73 | { 74 | await responseStream.WriteAsync(new ResponseMessage { Value = i }); 75 | } 76 | } 77 | 78 | public override async Task DelayedServerStreaming(RequestMessage request, 79 | IServerStreamWriter responseStream, ServerCallContext context) 80 | { 81 | for (int i = request.Value; i > 0; i--) 82 | { 83 | await responseStream.WriteAsync(new ResponseMessage { Value = i }); 84 | await Task.Delay(2000, context.CancellationToken); 85 | if (context.CancellationToken.IsCancellationRequested) 86 | { 87 | break; 88 | } 89 | } 90 | } 91 | 92 | public override async Task ThrowingServerStreaming(RequestMessage request, 93 | IServerStreamWriter responseStream, ServerCallContext context) 94 | { 95 | ServerStream = responseStream; 96 | for (int i = request.Value; i > 0; i--) 97 | { 98 | await responseStream.WriteAsync(new ResponseMessage { Value = i }); 99 | } 100 | throw new Exception("blah"); 101 | } 102 | 103 | public override async Task DuplexStreaming(IAsyncStreamReader requestStream, 104 | IServerStreamWriter responseStream, ServerCallContext context) 105 | { 106 | await responseStream.WriteAsync(new ResponseMessage { Value = 10 }); 107 | await responseStream.WriteAsync(new ResponseMessage { Value = 11 }); 108 | await Task.Delay(100); 109 | while (await requestStream.MoveNext()) 110 | { 111 | await responseStream.WriteAsync(new ResponseMessage { Value = requestStream.Current.Value }); 112 | } 113 | } 114 | 115 | public override async Task DelayedDuplexStreaming(IAsyncStreamReader requestStream, 116 | IServerStreamWriter responseStream, ServerCallContext context) 117 | { 118 | while (await requestStream.MoveNext(context.CancellationToken)) 119 | { 120 | await responseStream.WriteAsync(new ResponseMessage { Value = requestStream.Current.Value }); 121 | await Task.Delay(2000, context.CancellationToken); 122 | } 123 | } 124 | 125 | public override async Task ThrowingDuplexStreaming(IAsyncStreamReader requestStream, 126 | IServerStreamWriter responseStream, ServerCallContext context) 127 | { 128 | while (await requestStream.MoveNext()) 129 | { 130 | await responseStream.WriteAsync(new ResponseMessage { Value = requestStream.Current.Value }); 131 | } 132 | throw new Exception("blah"); 133 | } 134 | 135 | public override async Task HeadersTrailers(RequestMessage request, ServerCallContext context) 136 | { 137 | RequestHeaders = context.RequestHeaders; 138 | await context.WriteResponseHeadersAsync(ResponseHeaders); 139 | foreach (var entry in ResponseTrailers) 140 | { 141 | if (entry.IsBinary) 142 | { 143 | context.ResponseTrailers.Add(entry.Key, entry.ValueBytes); 144 | } 145 | else 146 | { 147 | context.ResponseTrailers.Add(entry.Key, entry.Value); 148 | } 149 | } 150 | return new ResponseMessage 151 | { 152 | Value = request.Value, 153 | Binary = request.Binary 154 | }; 155 | } 156 | 157 | public override Task SetStatus(RequestMessage request, ServerCallContext context) 158 | { 159 | context.Status = new Status(StatusCode.InvalidArgument, "invalid argument"); 160 | return Task.FromResult(new ResponseMessage()); 161 | } 162 | 163 | public override Task GetCallInfo(RequestMessage request, ServerCallContext context) 164 | { 165 | Peer = context.Peer; 166 | return Task.FromResult(new ResponseMessage()); 167 | } 168 | 169 | public override Task DropConnection(RequestMessage request, ServerCallContext context) 170 | { 171 | Thread.Sleep(100); 172 | if (context is NamedPipeCallContext namedPipeCallContext) 173 | { 174 | namedPipeCallContext.DisconnectPipeStream(); 175 | } 176 | return Task.FromResult(new ResponseMessage()); 177 | } 178 | 179 | public override Task DropConnectionClientStreaming(IAsyncStreamReader requestStream, 180 | ServerCallContext context) 181 | { 182 | Thread.Sleep(100); 183 | if (context is NamedPipeCallContext namedPipeCallContext) 184 | { 185 | namedPipeCallContext.DisconnectPipeStream(); 186 | } 187 | return Task.FromResult(new ResponseMessage()); 188 | } 189 | 190 | public override async Task WaitForCancellation(RequestMessage request, ServerCallContext context) 191 | { 192 | try 193 | { 194 | await Task.Delay(2000, context.CancellationToken); 195 | } 196 | catch (OperationCanceledException) 197 | { 198 | CancellationOccurred = true; 199 | throw; 200 | } 201 | return new ResponseMessage(); 202 | } 203 | 204 | public bool CancellationOccurred { get; private set; } 205 | 206 | public Metadata RequestHeaders { get; private set; } 207 | 208 | public Metadata ResponseHeaders { private get; set; } 209 | 210 | public Metadata ResponseTrailers { private get; set; } 211 | 212 | public string Peer { get; private set; } 213 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrpcDotNetNamedPipes", "GrpcDotNetNamedPipes\GrpcDotNetNamedPipes.csproj", "{5BB6ABD5-4B0C-4EB8-AE7A-6196107FA612}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrpcDotNetNamedPipes.Tests", "GrpcDotNetNamedPipes.Tests\GrpcDotNetNamedPipes.Tests.csproj", "{D7566E69-DA28-4997-9C4F-E0C0F8DD3F13}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrpcDotNetNamedPipes.PerfTests", "GrpcDotNetNamedPipes.PerfTests\GrpcDotNetNamedPipes.PerfTests.csproj", "{688ADBBA-FEDD-48C4-A05C-BB1E1BF8DB84}" 8 | EndProject 9 | Global 10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 11 | Debug|Any CPU = Debug|Any CPU 12 | Release|Any CPU = Release|Any CPU 13 | EndGlobalSection 14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 15 | {5BB6ABD5-4B0C-4EB8-AE7A-6196107FA612}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 16 | {5BB6ABD5-4B0C-4EB8-AE7A-6196107FA612}.Debug|Any CPU.Build.0 = Debug|Any CPU 17 | {5BB6ABD5-4B0C-4EB8-AE7A-6196107FA612}.Release|Any CPU.ActiveCfg = Release|Any CPU 18 | {5BB6ABD5-4B0C-4EB8-AE7A-6196107FA612}.Release|Any CPU.Build.0 = Release|Any CPU 19 | {D7566E69-DA28-4997-9C4F-E0C0F8DD3F13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 20 | {D7566E69-DA28-4997-9C4F-E0C0F8DD3F13}.Debug|Any CPU.Build.0 = Debug|Any CPU 21 | {D7566E69-DA28-4997-9C4F-E0C0F8DD3F13}.Release|Any CPU.ActiveCfg = Release|Any CPU 22 | {D7566E69-DA28-4997-9C4F-E0C0F8DD3F13}.Release|Any CPU.Build.0 = Release|Any CPU 23 | {688ADBBA-FEDD-48C4-A05C-BB1E1BF8DB84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {688ADBBA-FEDD-48C4-A05C-BB1E1BF8DB84}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {688ADBBA-FEDD-48C4-A05C-BB1E1BF8DB84}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {688ADBBA-FEDD-48C4-A05C-BB1E1BF8DB84}.Release|Any CPU.Build.0 = Release|Any CPU 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/GrpcDotNetNamedPipes.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net462;netstandard2.0;net6;net8 5 | 13 6 | true 7 | 8 | 3.1.0 9 | 3.1.0 10 | 2.0.0.0 11 | 12 | Ben Olden-Cooligan 13 | Google 14 | Unofficial windows named pipe transport for gRPC 15 | Copyright 2020 Google LLC 16 | LICENSE 17 | README.md 18 | https://github.com/cyanfish/grpc-dotnet-namedpipes 19 | GrpcDotNetNamedPipes 20 | https://github.com/cyanfish/grpc-dotnet-namedpipes 21 | git 22 | grpc namedpipe namedpipes named pipe pipes 23 | true 24 | public_signing_key.snk 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | <_Parameter1>GrpcDotNetNamedPipes.Tests, PublicKey=002400000480000014010000060200000024000052534131000800000100010047369c3d385b4e621dce099e51785be1eed3cefd4fca3ae9879ebbf7d8ab940d1bcd7dafc1ca499458094ad7484642318db10044c1d9cf647fa601865f0bec6f836d8a36db364f296880e4f900da5cd4e6eba826fd0672ae855bef275bc4f281d95ccdb9fc2f5f2b8299b07aceab4473e3ac6ce724ded956badb765c98d0b5c1514b9dc1dff4da73057a6105421de97b93ccbd692aa10d6c3ffb13e3e19440827e1c9b08c8a591bbb29327762b0d6eaf96a644c0bedddda25e9087047eca607001869fcd44eca48b058b288c67598fddeabff8f2a239e5dcd9df5c40656e65bd5fff61e6f875c84d1850c8446f016f435eb3f036de4c5a820b649cebafbb2aaa 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/ClientConnectionContext.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class ClientConnectionContext : TransportMessageHandler, IDisposable 20 | { 21 | private readonly NamedPipeClientStream _pipeStream; 22 | private readonly CallOptions _callOptions; 23 | private readonly bool _isServerUnary; 24 | private readonly PayloadQueue _payloadQueue; 25 | private readonly Deadline _deadline; 26 | private readonly int _connectionTimeout; 27 | private readonly SimpleAsyncLock _connectLock; 28 | private readonly ConnectionLogger _logger; 29 | 30 | private readonly TaskCompletionSource _responseHeadersTcs = 31 | new(TaskCreationOptions.RunContinuationsAsynchronously); 32 | private CancellationTokenRegistration _cancelReg; 33 | private byte[] _pendingPayload; 34 | private Metadata _responseTrailers; 35 | private Status _status; 36 | 37 | public ClientConnectionContext(NamedPipeClientStream pipeStream, CallOptions callOptions, bool isServerUnary, 38 | int connectionTimeout, SimpleAsyncLock connectLock, ConnectionLogger logger) 39 | { 40 | _pipeStream = pipeStream; 41 | _callOptions = callOptions; 42 | _isServerUnary = isServerUnary; 43 | Transport = new NamedPipeTransport(pipeStream, logger); 44 | _payloadQueue = new PayloadQueue(); 45 | _deadline = new Deadline(callOptions.Deadline); 46 | _connectionTimeout = connectionTimeout; 47 | _connectLock = connectLock; 48 | _logger = logger; 49 | } 50 | 51 | public Task InitTask { get; private set; } 52 | 53 | public NamedPipeTransport Transport { get; } 54 | 55 | public Task ResponseHeadersAsync => _responseHeadersTcs.Task; 56 | 57 | public void InitCall(Method method, TRequest request) 58 | { 59 | if (_callOptions.CancellationToken.IsCancellationRequested || _deadline.IsExpired) 60 | { 61 | InitTask = Task.CompletedTask; 62 | return; 63 | } 64 | 65 | InitTask = Task.Run(async () => 66 | { 67 | try 68 | { 69 | await ConnectPipeWithRetries(); 70 | _pipeStream.ReadMode = PlatformConfig.TransmissionMode; 71 | 72 | if (request != null) 73 | { 74 | var payload = SerializationHelpers.Serialize(method.RequestMarshaller, request); 75 | Transport.Write() 76 | .RequestInit(method.FullName, _callOptions.Deadline) 77 | .Headers(_callOptions.Headers) 78 | .Payload(payload) 79 | .Commit(); 80 | _cancelReg = _callOptions.CancellationToken.Register(DisposeCall); 81 | } 82 | else 83 | { 84 | Transport.Write() 85 | .RequestInit(method.FullName, _callOptions.Deadline) 86 | .Headers(_callOptions.Headers) 87 | .Commit(); 88 | _cancelReg = _callOptions.CancellationToken.Register(DisposeCall); 89 | } 90 | } 91 | catch (Exception ex) 92 | { 93 | _pipeStream.Dispose(); 94 | 95 | if (ex is TimeoutException || ex is IOException) 96 | { 97 | _payloadQueue.SetError(new RpcException(new Status(StatusCode.Unavailable, "failed to connect to all addresses"))); 98 | } 99 | else 100 | { 101 | _payloadQueue.SetError(ex); 102 | } 103 | } 104 | }); 105 | } 106 | 107 | private async Task ConnectPipeWithRetries() 108 | { 109 | // In theory .Connect is supposed to have a timeout and not need retries, but it turns 110 | // out that in practice sometimes it does. 111 | var elapsed = Stopwatch.StartNew(); 112 | int TimeLeft() => Math.Max(_connectionTimeout - (int) elapsed.ElapsedMilliseconds, 0); 113 | var fallback = 100; 114 | while (true) 115 | { 116 | try 117 | { 118 | await _connectLock.Take(); 119 | var waitTime = _connectionTimeout == -1 ? 1000 : Math.Min(1000, TimeLeft()); 120 | await _pipeStream.ConnectAsync(waitTime).ConfigureAwait(false); 121 | _connectLock.Release(); 122 | break; 123 | } 124 | catch (Exception ex) 125 | { 126 | _connectLock.Release(); 127 | if (ex is not (TimeoutException or IOException)) 128 | { 129 | throw; 130 | } 131 | if (_connectionTimeout != -1 && TimeLeft() == 0) 132 | { 133 | throw; 134 | } 135 | var delayTime = _connectionTimeout == -1 ? fallback : Math.Min(fallback, TimeLeft()); 136 | await Task.Delay(delayTime); 137 | fallback = Math.Min(fallback * 2, 1000); 138 | } 139 | } 140 | } 141 | 142 | public override void HandleHeaders(Metadata headers) 143 | { 144 | EnsureResponseHeadersSet(headers); 145 | } 146 | 147 | public override void HandleTrailers(Metadata trailers, Status status) 148 | { 149 | EnsureResponseHeadersSet(); 150 | _responseTrailers = trailers ?? new Metadata(); 151 | _status = status; 152 | 153 | if (_pendingPayload != null) 154 | { 155 | _payloadQueue.AppendPayload(_pendingPayload); 156 | } 157 | 158 | if (status.StatusCode == StatusCode.OK) 159 | { 160 | _payloadQueue.SetCompleted(); 161 | } 162 | else 163 | { 164 | _payloadQueue.SetError(new RpcException(status, _responseTrailers)); 165 | } 166 | } 167 | 168 | public override void HandlePayload(byte[] payload) 169 | { 170 | EnsureResponseHeadersSet(); 171 | 172 | if (_isServerUnary) 173 | { 174 | // Wait to process the payload until we've received the trailers 175 | _pendingPayload = payload; 176 | } 177 | else 178 | { 179 | _payloadQueue.AppendPayload(payload); 180 | } 181 | } 182 | 183 | private void EnsureResponseHeadersSet(Metadata headers = null) 184 | { 185 | if (!_responseHeadersTcs.Task.IsCompleted) 186 | { 187 | _responseHeadersTcs.SetResult(headers ?? new Metadata()); 188 | } 189 | } 190 | 191 | public Metadata GetTrailers() => _responseTrailers ?? throw new InvalidOperationException(); 192 | 193 | public Status GetStatus() => _responseTrailers != null ? _status : throw new InvalidOperationException(); 194 | 195 | public MessageReader GetMessageReader(Marshaller responseMarshaller) 196 | { 197 | return new MessageReader(_payloadQueue, responseMarshaller, _callOptions.CancellationToken, 198 | _deadline); 199 | } 200 | 201 | public IClientStreamWriter CreateRequestStream(Marshaller requestMarshaller) 202 | { 203 | return new RequestStreamWriterImpl(Transport, _callOptions.CancellationToken, requestMarshaller, 204 | InitTask); 205 | } 206 | 207 | public void DisposeCall() 208 | { 209 | InitTask.ContinueWith(_ => 210 | { 211 | try 212 | { 213 | Transport.Write().Cancel().Commit(); 214 | } 215 | catch (Exception) 216 | { 217 | // Assume the connection is already terminated 218 | } 219 | }, TaskContinuationOptions.OnlyOnRanToCompletion); 220 | } 221 | 222 | public void Dispose() 223 | { 224 | _cancelReg.Dispose(); 225 | _payloadQueue.Dispose(); 226 | _pipeStream.Dispose(); 227 | } 228 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Deadline.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class Deadline 20 | { 21 | private readonly DateTime? _deadline; 22 | private readonly CancellationTokenSource _cts; 23 | 24 | public Deadline(DateTime? deadline) 25 | { 26 | _deadline = deadline; 27 | _cts = new CancellationTokenSource(); 28 | 29 | if (_deadline != null) 30 | { 31 | long millis = (long) (_deadline.Value - DateTime.UtcNow).TotalMilliseconds; 32 | if (millis <= 0) 33 | { 34 | _cts.Cancel(); 35 | } 36 | else if (millis < int.MaxValue) 37 | { 38 | _cts.CancelAfter((int) millis); 39 | } 40 | } 41 | } 42 | 43 | public DateTime Value => _deadline ?? DateTime.MaxValue; 44 | 45 | public CancellationToken Token => _cts.Token; 46 | 47 | public bool IsExpired => _cts.IsCancellationRequested; 48 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/EndOfPipeException.cs: -------------------------------------------------------------------------------- 1 | namespace GrpcDotNetNamedPipes.Internal; 2 | 3 | internal class EndOfPipeException : Exception 4 | { 5 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Helpers/ByteArrayBufferWriter.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal.Helpers; 18 | 19 | internal class ByteArrayBufferWriter : IBufferWriter 20 | { 21 | private readonly byte[] _buffer; 22 | private int _position; 23 | 24 | public ByteArrayBufferWriter(int size) 25 | { 26 | _buffer = new byte[size]; 27 | } 28 | 29 | public void Advance(int count) 30 | { 31 | _position += count; 32 | } 33 | 34 | public Memory GetMemory(int sizeHint = 0) => _buffer.AsMemory(_position); 35 | 36 | public Span GetSpan(int sizeHint = 0) => _buffer.AsSpan(_position); 37 | 38 | public byte[] Buffer => _buffer; 39 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Helpers/ByteArrayDeserializationContext.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal.Helpers; 18 | 19 | internal class ByteArrayDeserializationContext : DeserializationContext 20 | { 21 | private readonly byte[] _payload; 22 | 23 | public ByteArrayDeserializationContext(byte[] payload) 24 | { 25 | _payload = payload; 26 | } 27 | 28 | public override int PayloadLength => _payload.Length; 29 | 30 | public override byte[] PayloadAsNewBuffer() => _payload.ToArray(); 31 | 32 | public override ReadOnlySequence PayloadAsReadOnlySequence() => new(_payload); 33 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Helpers/ByteArraySerializationContext.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal.Helpers; 18 | 19 | internal class ByteArraySerializationContext : SerializationContext 20 | { 21 | private int _payloadLength; 22 | private ByteArrayBufferWriter _bufferWriter; 23 | 24 | public override void Complete(byte[] payload) 25 | { 26 | SerializedData = payload; 27 | } 28 | 29 | public override IBufferWriter GetBufferWriter() 30 | { 31 | return _bufferWriter ??= new ByteArrayBufferWriter(_payloadLength); 32 | } 33 | 34 | public override void SetPayloadLength(int payloadLength) 35 | { 36 | _payloadLength = payloadLength; 37 | } 38 | 39 | public override void Complete() 40 | { 41 | Debug.Assert(_bufferWriter.Buffer.Length == _payloadLength); 42 | SerializedData = _bufferWriter.Buffer; 43 | } 44 | 45 | public byte[] SerializedData { get; private set; } 46 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Helpers/ConnectionLogger.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal.Helpers; 18 | 19 | internal class ConnectionLogger 20 | { 21 | private static int _lastId; 22 | 23 | private static int NextId() => Interlocked.Increment(ref _lastId); 24 | public static ConnectionLogger Client(Action log) => new(log, "CLIENT", log != null ? NextId() : 0); 25 | public static ConnectionLogger Server(Action log) => new(log, "SERVER", 0); 26 | 27 | private readonly Action _log; 28 | private readonly string _type; 29 | 30 | private ConnectionLogger(Action log, string type, int id) 31 | { 32 | _log = log; 33 | _type = type; 34 | ConnectionId = id; 35 | } 36 | 37 | public int ConnectionId { get; set; } 38 | 39 | public void Log(string message) 40 | { 41 | if (_log == null) return; 42 | var id = ConnectionId > 0 ? ConnectionId.ToString() : "?"; 43 | _log($"[{_type}][{id}] {message}"); 44 | } 45 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Helpers/PipeInterop.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | using System.Runtime.InteropServices; 18 | using System.Text; 19 | 20 | namespace GrpcDotNetNamedPipes.Internal.Helpers; 21 | 22 | internal class PipeInterop 23 | { 24 | private const int ErrorPipeLocal = 229; 25 | 26 | public static string GetClientComputerName(IntPtr pipeHandle) 27 | { 28 | var buffer = new StringBuilder(1024); 29 | if (!GetNamedPipeClientComputerName(pipeHandle, buffer, 1024)) 30 | { 31 | if (Marshal.GetLastWin32Error() == ErrorPipeLocal) 32 | { 33 | return "localhost"; 34 | } 35 | throw new InvalidOperationException($"Error retrieving client computer name: {Marshal.GetLastWin32Error()}"); 36 | } 37 | return buffer.ToString(); 38 | } 39 | 40 | public static uint GetClientProcessId(IntPtr pipeHandle) 41 | { 42 | if (!GetNamedPipeClientProcessId(pipeHandle, out var processId)) 43 | { 44 | throw new InvalidOperationException($"Error retrieving client process id: {Marshal.GetLastWin32Error()}"); 45 | } 46 | return processId; 47 | } 48 | 49 | [DllImport("kernel32.dll", SetLastError = true)] 50 | private static extern bool GetNamedPipeClientComputerName(IntPtr pipeHandle, StringBuilder buffer, uint bufferLen); 51 | 52 | [DllImport("kernel32.dll", SetLastError = true)] 53 | private static extern bool GetNamedPipeClientProcessId(IntPtr pipeHandle, out uint processId); 54 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Helpers/SerializationHelpers.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal.Helpers; 18 | 19 | internal static class SerializationHelpers 20 | { 21 | public static byte[] Serialize(Marshaller marshaller, T message) 22 | { 23 | var serializationContext = new ByteArraySerializationContext(); 24 | marshaller.ContextualSerializer(message, serializationContext); 25 | return serializationContext.SerializedData; 26 | } 27 | 28 | public static T Deserialize(Marshaller marshaller, byte[] payload) 29 | { 30 | var deserializationContext = new ByteArrayDeserializationContext(payload); 31 | return marshaller.ContextualDeserializer(deserializationContext); 32 | } 33 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/MessageReader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class MessageReader : IAsyncStreamReader 20 | { 21 | private readonly PayloadQueue _payloadQueue; 22 | private readonly Marshaller _marshaller; 23 | private readonly CancellationToken _callCancellationToken; 24 | private readonly Deadline _deadline; 25 | 26 | public MessageReader(PayloadQueue payloadQueue, Marshaller marshaller, 27 | CancellationToken callCancellationToken, Deadline deadline) 28 | { 29 | _payloadQueue = payloadQueue; 30 | _marshaller = marshaller; 31 | _callCancellationToken = callCancellationToken; 32 | _deadline = deadline; 33 | } 34 | 35 | public async Task MoveNext(CancellationToken cancellationToken) 36 | { 37 | try 38 | { 39 | using var combined = 40 | CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _callCancellationToken, 41 | _deadline.Token); 42 | return await _payloadQueue.MoveNext(combined.Token).ConfigureAwait(false); 43 | } 44 | catch (OperationCanceledException) 45 | { 46 | if (_deadline.IsExpired) 47 | { 48 | throw new RpcException(new Status(StatusCode.DeadlineExceeded, "")); 49 | } 50 | else 51 | { 52 | throw new RpcException(Status.DefaultCancelled); 53 | } 54 | } 55 | } 56 | 57 | public TMessage Current => SerializationHelpers.Deserialize(_marshaller, _payloadQueue.Current); 58 | 59 | public Task ReadNextMessage() 60 | { 61 | return ReadNextMessage(CancellationToken.None); 62 | } 63 | 64 | public Task ReadNextMessage(CancellationToken cancellationToken) 65 | { 66 | return Task.Run(async () => 67 | { 68 | if (!await MoveNext(cancellationToken).ConfigureAwait(false)) 69 | { 70 | throw new InvalidOperationException("Expected payload"); 71 | } 72 | 73 | return Current; 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/PayloadQueue.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class PayloadQueue : IAsyncStreamReader 20 | { 21 | private readonly Queue _internalQueue = new(); 22 | private TaskCompletionSource _tcs; 23 | private CancellationTokenRegistration _cancelReg; 24 | private Exception _error; 25 | private bool _completed; 26 | private bool _terminated; 27 | 28 | public void AppendPayload(byte[] payload) 29 | { 30 | lock (this) 31 | { 32 | _internalQueue.Enqueue(payload); 33 | if (_tcs != null) 34 | { 35 | Current = _internalQueue.Dequeue(); 36 | _tcs.SetResult(true); 37 | ResetTcs(); 38 | } 39 | } 40 | } 41 | 42 | private void ResetTcs() 43 | { 44 | _cancelReg.Dispose(); 45 | _tcs = null; 46 | } 47 | 48 | public void SetCompleted() 49 | { 50 | lock (this) 51 | { 52 | _terminated = true; 53 | _completed = true; 54 | if (_tcs != null) 55 | { 56 | _tcs.SetResult(false); 57 | ResetTcs(); 58 | } 59 | } 60 | } 61 | 62 | public void SetError(Exception ex) 63 | { 64 | lock (this) 65 | { 66 | _terminated = true; 67 | _error = ex; 68 | if (_tcs != null) 69 | { 70 | _tcs.SetException(_error); 71 | ResetTcs(); 72 | } 73 | } 74 | } 75 | 76 | public void SetCanceled() 77 | { 78 | lock (this) 79 | { 80 | _terminated = true; 81 | if (_tcs != null) 82 | { 83 | _tcs.SetCanceled(); 84 | ResetTcs(); 85 | } 86 | } 87 | } 88 | 89 | public Task MoveNext(CancellationToken cancellationToken) 90 | { 91 | lock (this) 92 | { 93 | if (_tcs != null) 94 | { 95 | throw new InvalidOperationException("Overlapping MoveNext calls"); 96 | } 97 | 98 | if (cancellationToken.IsCancellationRequested) 99 | { 100 | return Task.FromCanceled(cancellationToken); 101 | } 102 | 103 | if (_internalQueue.Count > 0) 104 | { 105 | Current = _internalQueue.Dequeue(); 106 | return Task.FromResult(true); 107 | } 108 | 109 | if (_error != null) 110 | { 111 | throw _error; 112 | } 113 | 114 | if (_completed) 115 | { 116 | return Task.FromResult(false); 117 | } 118 | 119 | _tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 120 | var task = _tcs.Task; 121 | _cancelReg = cancellationToken.Register(SetCanceled); 122 | return task; 123 | } 124 | } 125 | 126 | public byte[] Current { get; private set; } 127 | 128 | public void Dispose() 129 | { 130 | if (!_terminated) 131 | { 132 | // Unexpected pipe termination 133 | SetError(new RpcException(new Status(StatusCode.Unavailable, "failed to connect to all addresses"))); 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/PipeReader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class PipeReader 20 | { 21 | private readonly PipeStream _pipeStream; 22 | private readonly TransportMessageHandler _messageHandler; 23 | private readonly ConnectionLogger _logger; 24 | private readonly Action _onDisconnected; 25 | private readonly Action _onError; 26 | private readonly NamedPipeTransport _transport; 27 | 28 | public PipeReader(PipeStream pipeStream, TransportMessageHandler messageHandler, ConnectionLogger logger, 29 | Action onDisconnected, Action onError = null) 30 | { 31 | _pipeStream = pipeStream; 32 | _messageHandler = messageHandler; 33 | _logger = logger; 34 | _onDisconnected = onDisconnected; 35 | _onError = onError; 36 | _transport = new NamedPipeTransport(_pipeStream, logger); 37 | } 38 | 39 | public async Task ReadLoop() 40 | { 41 | try 42 | { 43 | while (_pipeStream.IsConnected && 44 | await _transport.Read(_messageHandler).ConfigureAwait(false)) 45 | { 46 | } 47 | _logger.Log("Pipe disconnected"); 48 | } 49 | catch (EndOfPipeException) 50 | { 51 | _logger.Log("End of pipe"); 52 | } 53 | catch (Exception error) 54 | { 55 | _logger.Log("Pipe read error"); 56 | _onError?.Invoke(error); 57 | } 58 | finally 59 | { 60 | _onDisconnected?.Invoke(); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/PlatformConfig.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal static class PlatformConfig 20 | { 21 | public static PipeTransmissionMode TransmissionMode { get; } = 22 | #if NET6_0_OR_GREATER 23 | OperatingSystem.IsWindows() ? PipeTransmissionMode.Message : PipeTransmissionMode.Byte; 24 | #elif NETSTANDARD 25 | Environment.OSVersion.Platform == PlatformID.Win32NT ? PipeTransmissionMode.Message : PipeTransmissionMode.Byte; 26 | #else 27 | PipeTransmissionMode.Message; 28 | #endif 29 | 30 | public static bool SizePrefix { get; } = TransmissionMode == PipeTransmissionMode.Byte; 31 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Protocol/NamedPipeTransport.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | using Google.Protobuf; 18 | using Google.Protobuf.Collections; 19 | 20 | namespace GrpcDotNetNamedPipes.Internal.Protocol; 21 | 22 | internal class NamedPipeTransport 23 | { 24 | private const int MessageBufferSize = 16 * 1024; // 16 kiB 25 | 26 | private readonly byte[] _messageBuffer = new byte[MessageBufferSize]; 27 | private readonly PipeStream _pipeStream; 28 | private readonly ConnectionLogger _logger; 29 | private readonly WriteTransactionQueue _txQueue; 30 | 31 | public NamedPipeTransport(PipeStream pipeStream, ConnectionLogger logger) 32 | { 33 | _pipeStream = pipeStream; 34 | _logger = logger; 35 | _txQueue = new WriteTransactionQueue(pipeStream); 36 | } 37 | 38 | private async Task ReadPacketFromPipe() 39 | { 40 | var packet = new MemoryStream(); 41 | if (PlatformConfig.SizePrefix) 42 | { 43 | await ReadPacketWithSizePrefix(packet); 44 | } 45 | else 46 | { 47 | await ReadPacketWithMessage(packet); 48 | } 49 | 50 | packet.Position = 0; 51 | return packet; 52 | } 53 | 54 | private async Task ReadPacketWithMessage(MemoryStream packet) 55 | { 56 | do 57 | { 58 | int readBytes = await _pipeStream.ReadAsync(_messageBuffer, 0, MessageBufferSize).ConfigureAwait(false); 59 | packet.Write(_messageBuffer, 0, readBytes); 60 | } while (!_pipeStream.IsMessageComplete); 61 | } 62 | 63 | private async Task ReadPacketWithSizePrefix(MemoryStream packet) 64 | { 65 | int bytesToRead = await ReadSizePrefix(); 66 | do 67 | { 68 | var bytesToReadIntoBuffer = Math.Min(bytesToRead, MessageBufferSize); 69 | int readBytes = await _pipeStream.ReadAsync(_messageBuffer, 0, bytesToReadIntoBuffer) 70 | .ConfigureAwait(false); 71 | if (readBytes == 0) 72 | { 73 | throw new EndOfPipeException(); 74 | } 75 | packet.Write(_messageBuffer, 0, readBytes); 76 | bytesToRead -= readBytes; 77 | } while (bytesToRead > 0); 78 | } 79 | 80 | private async Task ReadSizePrefix() 81 | { 82 | int readBytes = await _pipeStream.ReadAsync(_messageBuffer, 0, 4).ConfigureAwait(false); 83 | if (readBytes == 0) 84 | { 85 | throw new EndOfPipeException(); 86 | } 87 | if (readBytes != 4) 88 | { 89 | throw new InvalidOperationException("Unexpected size prefix"); 90 | } 91 | return BitConverter.ToInt32(_messageBuffer, 0); 92 | } 93 | 94 | public async Task Read(TransportMessageHandler messageHandler) 95 | { 96 | var packet = await ReadPacketFromPipe().ConfigureAwait(false); 97 | while (packet.Position < packet.Length) 98 | { 99 | var message = new TransportMessage(); 100 | message.MergeDelimitedFrom(packet); 101 | switch (message.DataCase) 102 | { 103 | case TransportMessage.DataOneofCase.RequestInit: 104 | _logger.ConnectionId = message.RequestInit.ConnectionId; 105 | _logger.Log($"Received for '{message.RequestInit.MethodFullName}'"); 106 | messageHandler.HandleRequestInit(message.RequestInit.MethodFullName, 107 | message.RequestInit.Deadline?.ToDateTime()); 108 | break; 109 | case TransportMessage.DataOneofCase.Headers: 110 | _logger.Log("Received "); 111 | var headerMetadata = ConstructMetadata(message.Headers.Metadata); 112 | messageHandler.HandleHeaders(headerMetadata); 113 | break; 114 | case TransportMessage.DataOneofCase.PayloadInfo: 115 | _logger.Log($"Received with {message.PayloadInfo.Size} bytes"); 116 | var payload = new byte[message.PayloadInfo.Size]; 117 | if (message.PayloadInfo.InSamePacket) 118 | { 119 | packet.Read(payload, 0, payload.Length); 120 | } 121 | else 122 | { 123 | _pipeStream.Read(payload, 0, payload.Length); 124 | } 125 | 126 | messageHandler.HandlePayload(payload); 127 | break; 128 | case TransportMessage.DataOneofCase.RequestControl: 129 | switch (message.RequestControl) 130 | { 131 | case RequestControl.Cancel: 132 | _logger.Log("Received "); 133 | messageHandler.HandleCancel(); 134 | break; 135 | case RequestControl.StreamEnd: 136 | _logger.Log("Received "); 137 | messageHandler.HandleStreamEnd(); 138 | break; 139 | } 140 | 141 | break; 142 | case TransportMessage.DataOneofCase.Trailers: 143 | _logger.Log($"Received with status '{message.Trailers.StatusCode}'"); 144 | var trailerMetadata = ConstructMetadata(message.Trailers.Metadata); 145 | var status = new Status((StatusCode) message.Trailers.StatusCode, 146 | message.Trailers.StatusDetail); 147 | messageHandler.HandleTrailers(trailerMetadata, status); 148 | // Stop reading after receiving trailers 149 | return false; 150 | } 151 | } 152 | return true; 153 | } 154 | 155 | private static Metadata ConstructMetadata(RepeatedField entries) 156 | { 157 | var metadata = new Metadata(); 158 | foreach (var entry in entries) 159 | { 160 | switch (entry.ValueCase) 161 | { 162 | case MetadataEntry.ValueOneofCase.ValueString: 163 | metadata.Add(new Metadata.Entry(entry.Name, entry.ValueString)); 164 | break; 165 | case MetadataEntry.ValueOneofCase.ValueBytes: 166 | metadata.Add(new Metadata.Entry(entry.Name, entry.ValueBytes.ToByteArray())); 167 | break; 168 | } 169 | } 170 | 171 | return metadata; 172 | } 173 | 174 | public WriteTransaction Write() 175 | { 176 | return new WriteTransaction(_txQueue, _logger); 177 | } 178 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Protocol/WriteTransaction.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | using Google.Protobuf; 18 | using Google.Protobuf.Collections; 19 | using Google.Protobuf.WellKnownTypes; 20 | 21 | namespace GrpcDotNetNamedPipes.Internal.Protocol; 22 | 23 | internal class WriteTransaction 24 | { 25 | private const int PayloadInSeparatePacketThreshold = 15 * 1024; // 15 kiB 26 | 27 | private readonly WriteTransactionQueue _txQueue; 28 | private readonly ConnectionLogger _logger; 29 | private readonly MemoryStream _packetBuffer = new(); 30 | private readonly List _trailingPayloads = new(); 31 | 32 | public WriteTransaction(WriteTransactionQueue txQueue, ConnectionLogger logger) 33 | { 34 | _txQueue = txQueue; 35 | _logger = logger; 36 | } 37 | 38 | public void Commit() 39 | { 40 | _txQueue.Add(this); 41 | } 42 | 43 | public void WriteTo(PipeStream pipeStream) 44 | { 45 | lock (pipeStream) 46 | { 47 | if (_packetBuffer.Length > 0) 48 | { 49 | if (PlatformConfig.SizePrefix) 50 | { 51 | pipeStream.Write(BitConverter.GetBytes(_packetBuffer.Length), 0, 4); 52 | } 53 | _packetBuffer.WriteTo(pipeStream); 54 | } 55 | 56 | foreach (var payload in _trailingPayloads) 57 | { 58 | pipeStream.Write(payload, 0, payload.Length); 59 | } 60 | } 61 | } 62 | 63 | public void MergeFrom(WriteTransaction other) 64 | { 65 | other._packetBuffer.WriteTo(_packetBuffer); 66 | _trailingPayloads.AddRange(other._trailingPayloads); 67 | } 68 | 69 | private WriteTransaction AddMessage(TransportMessage message) 70 | { 71 | // message.WriteDelimitedTo is a helper method that does this for us. But it uses a default buffer size of 4096 72 | // which is much bigger than we need - most messages are only a few bytes. A small buffer size ends up being 73 | // significantly faster on the ServerStreamingManyMessagesPerformance test. 74 | CodedOutputStream codedOutput = new CodedOutputStream(_packetBuffer, 16); 75 | codedOutput.WriteLength(message.CalculateSize()); 76 | message.WriteTo(codedOutput); 77 | codedOutput.Flush(); 78 | return this; 79 | } 80 | 81 | public WriteTransaction RequestInit(string methodFullName, DateTime? deadline) 82 | { 83 | _logger.Log($"Sending for '{methodFullName}'"); 84 | return AddMessage(new TransportMessage 85 | { 86 | RequestInit = new RequestInit 87 | { 88 | MethodFullName = methodFullName, 89 | Deadline = deadline != null ? Timestamp.FromDateTime(deadline.Value) : null, 90 | ConnectionId = _logger.ConnectionId 91 | } 92 | }); 93 | } 94 | 95 | private void ToTransportMetadata(Metadata metadata, RepeatedField transportMetadata) 96 | { 97 | foreach (var entry in metadata ?? new Metadata()) 98 | { 99 | var transportEntry = new MetadataEntry 100 | { 101 | Name = entry.Key 102 | }; 103 | if (entry.IsBinary) 104 | { 105 | transportEntry.ValueBytes = ByteString.CopyFrom(entry.ValueBytes); 106 | } 107 | else 108 | { 109 | transportEntry.ValueString = entry.Value; 110 | } 111 | 112 | transportMetadata.Add(transportEntry); 113 | } 114 | } 115 | 116 | public WriteTransaction Headers(Metadata headers) 117 | { 118 | _logger.Log($"Sending "); 119 | var transportHeaders = new Headers(); 120 | ToTransportMetadata(headers, transportHeaders.Metadata); 121 | return AddMessage(new TransportMessage 122 | { 123 | Headers = transportHeaders 124 | }); 125 | } 126 | 127 | public WriteTransaction Trailers(StatusCode statusCode, string statusDetail, Metadata trailers) 128 | { 129 | _logger.Log($"Sending with status '{statusCode}'"); 130 | var transportTrailers = new Trailers 131 | { 132 | StatusCode = (int) statusCode, 133 | StatusDetail = statusDetail 134 | }; 135 | ToTransportMetadata(trailers, transportTrailers.Metadata); 136 | return AddMessage(new TransportMessage 137 | { 138 | Trailers = transportTrailers 139 | }); 140 | } 141 | 142 | public WriteTransaction Cancel() 143 | { 144 | _logger.Log("Sending "); 145 | return AddMessage(new TransportMessage 146 | { 147 | RequestControl = RequestControl.Cancel 148 | }); 149 | } 150 | 151 | public WriteTransaction RequestStreamEnd() 152 | { 153 | _logger.Log("Sending "); 154 | return AddMessage(new TransportMessage 155 | { 156 | RequestControl = RequestControl.StreamEnd 157 | }); 158 | } 159 | 160 | public WriteTransaction Payload(byte[] payload) 161 | { 162 | _logger.Log($"Sending with {payload.Length} bytes"); 163 | // TODO: Why doesn't this work on Unix? 164 | if (payload.Length > PayloadInSeparatePacketThreshold && 165 | Environment.OSVersion.Platform == PlatformID.Win32NT) 166 | { 167 | // For large payloads, writing the payload outside the packet saves extra copying. 168 | AddMessage(new TransportMessage 169 | { 170 | PayloadInfo = new PayloadInfo 171 | { 172 | Size = payload.Length, 173 | InSamePacket = false 174 | } 175 | }); 176 | _trailingPayloads.Add(payload); 177 | } 178 | else 179 | { 180 | // For small payloads, including the payload in the packet reduces the number of reads. 181 | AddMessage(new TransportMessage 182 | { 183 | PayloadInfo = new PayloadInfo 184 | { 185 | Size = payload.Length, 186 | InSamePacket = true 187 | } 188 | }); 189 | _packetBuffer.Write(payload, 0, payload.Length); 190 | } 191 | 192 | return this; 193 | } 194 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Protocol/WriteTransactionQueue.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal.Protocol; 18 | 19 | internal class WriteTransactionQueue 20 | { 21 | private readonly PipeStream _pipeStream; 22 | private List _queue = new(); 23 | private Task _dequeueTask; 24 | 25 | public WriteTransactionQueue(PipeStream pipeStream) 26 | { 27 | _pipeStream = pipeStream; 28 | } 29 | 30 | public void Add(WriteTransaction tx) 31 | { 32 | lock (this) 33 | { 34 | if (!_pipeStream.IsConnected) 35 | { 36 | throw new RpcException(new Status(StatusCode.Unavailable, "connection was unexpectedly terminated")); 37 | } 38 | _queue.Add(tx); 39 | _dequeueTask ??= Task.Run(Dequeue); 40 | } 41 | } 42 | 43 | private void Dequeue() 44 | { 45 | while (true) 46 | { 47 | List transactionsToWrite; 48 | lock (this) 49 | { 50 | transactionsToWrite = _queue; 51 | _queue = new List(); 52 | } 53 | // Merge transactions together if multiple are queued 54 | var mergedTx = new WriteTransaction(this, null); 55 | foreach (var tx in transactionsToWrite) 56 | { 57 | mergedTx.MergeFrom(tx); 58 | } 59 | try 60 | { 61 | mergedTx.WriteTo(_pipeStream); 62 | } 63 | catch (Exception) 64 | { 65 | // Not a lot we can do to recover here 66 | } 67 | lock (this) 68 | { 69 | if (_queue.Count == 0) 70 | { 71 | _dequeueTask = null; 72 | break; 73 | } 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/Protocol/transport.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | syntax = "proto3"; 18 | 19 | import "google/protobuf/timestamp.proto"; 20 | 21 | package GrpcDotNetNamedPipes.Generated; 22 | 23 | message TransportMessage { 24 | oneof data { 25 | RequestInit request_init = 1; 26 | Headers headers = 2; 27 | PayloadInfo payload_info = 3; 28 | RequestControl request_control = 4; 29 | Trailers trailers = 5; 30 | } 31 | } 32 | 33 | enum RequestControl { 34 | none = 0; 35 | cancel = 1; 36 | stream_end = 2; 37 | } 38 | 39 | message RequestInit { 40 | string methodFullName = 1; 41 | google.protobuf.Timestamp deadline = 2; 42 | int32 connectionId = 3; 43 | } 44 | 45 | message Headers { 46 | repeated MetadataEntry metadata = 1; 47 | } 48 | 49 | message Trailers { 50 | repeated MetadataEntry metadata = 1; 51 | int32 status_code = 2; 52 | string status_detail = 3; 53 | } 54 | 55 | message PayloadInfo { 56 | int32 size = 1; 57 | // For small packets, the payload bytes immediately follow the TransportMessage proto in the same packet. 58 | // For large packets, the payload bytes are in a separate packet to avoid extra copying. 59 | bool in_same_packet = 2; 60 | } 61 | 62 | message MetadataEntry { 63 | string name = 1; 64 | oneof value { 65 | string valueString = 2; 66 | bytes valueBytes = 3; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/RequestStreamWriterImpl.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class RequestStreamWriterImpl : StreamWriterImpl, IClientStreamWriter 20 | { 21 | private readonly Task _initTask; 22 | private bool _isInitialized; 23 | private bool _isCompleted; 24 | 25 | public RequestStreamWriterImpl(NamedPipeTransport stream, CancellationToken cancellationToken, 26 | Marshaller marshaller, Task initTask) 27 | : base(stream, cancellationToken, marshaller) 28 | { 29 | _initTask = initTask; 30 | } 31 | 32 | public override Task WriteAsync(T message) 33 | { 34 | if (_isCompleted) 35 | { 36 | throw new InvalidOperationException($"Request stream has already been completed."); 37 | } 38 | return WriteAsyncCore(message); 39 | } 40 | 41 | private async Task WriteAsyncCore(T message) 42 | { 43 | if (!_isInitialized) 44 | { 45 | await _initTask.ConfigureAwait(false); 46 | _isInitialized = true; 47 | } 48 | await base.WriteAsync(message).ConfigureAwait(false); 49 | } 50 | 51 | public async Task CompleteAsync() 52 | { 53 | if (!_isInitialized) 54 | { 55 | await _initTask.ConfigureAwait(false); 56 | _isInitialized = true; 57 | } 58 | if (CancelToken.IsCancellationRequested) 59 | { 60 | throw new TaskCanceledException(); 61 | } 62 | Stream.Write().RequestStreamEnd().Commit(); 63 | _isCompleted = true; 64 | } 65 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/ResponseStreamWriterImpl.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class ResponseStreamWriterImpl : StreamWriterImpl, IServerStreamWriter 20 | { 21 | private readonly Func _isCompleted; 22 | 23 | public ResponseStreamWriterImpl(NamedPipeTransport stream, CancellationToken cancellationToken, 24 | Marshaller marshaller, Func isCompleted) 25 | : base(stream, cancellationToken, marshaller) 26 | { 27 | _isCompleted = isCompleted; 28 | } 29 | 30 | public override Task WriteAsync(T message) 31 | { 32 | if (_isCompleted()) 33 | { 34 | throw new InvalidOperationException($"Response stream has already been completed."); 35 | } 36 | return base.WriteAsync(message); 37 | } 38 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/ServerConnectionContext.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class ServerConnectionContext : TransportMessageHandler, IDisposable 20 | { 21 | private readonly ConnectionLogger _logger; 22 | private readonly Dictionary> _methodHandlers; 23 | private readonly PayloadQueue _payloadQueue; 24 | private readonly CancellationTokenSource _requestInitTimeoutCts = new(); 25 | 26 | public ServerConnectionContext(NamedPipeServerStream pipeStream, ConnectionLogger logger, 27 | Dictionary> methodHandlers) 28 | { 29 | CallContext = new NamedPipeCallContext(this); 30 | PipeStream = pipeStream; 31 | Transport = new NamedPipeTransport(pipeStream, logger); 32 | _logger = logger; 33 | _methodHandlers = methodHandlers; 34 | _payloadQueue = new PayloadQueue(); 35 | CancellationTokenSource = new CancellationTokenSource(); 36 | 37 | // We're supposed to receive a RequestInit message immediately after the pipe connects. 10s is chosen as a very 38 | // conservative timeout. If this expires without receiving RequestInit, we can assume the client is not using 39 | // the right protocol and we should terminate the connection rather than potentially leave it open forever. 40 | Task.Delay(10_000, _requestInitTimeoutCts.Token) 41 | .ContinueWith(_ => RequestInitTimeout(), TaskContinuationOptions.OnlyOnRanToCompletion); 42 | } 43 | 44 | public NamedPipeServerStream PipeStream { get; } 45 | 46 | public NamedPipeTransport Transport { get; } 47 | 48 | public CancellationTokenSource CancellationTokenSource { get; } 49 | 50 | public Deadline Deadline { get; private set; } 51 | 52 | public Metadata RequestHeaders { get; private set; } 53 | 54 | public ServerCallContext CallContext { get; } 55 | 56 | public bool IsCompleted { get; private set; } 57 | 58 | public MessageReader GetMessageReader(Marshaller requestMarshaller) 59 | { 60 | return new MessageReader(_payloadQueue, requestMarshaller, CancellationToken.None, Deadline); 61 | } 62 | 63 | public IServerStreamWriter CreateResponseStream(Marshaller responseMarshaller) 64 | { 65 | return new ResponseStreamWriterImpl(Transport, CancellationToken.None, responseMarshaller, 66 | () => IsCompleted); 67 | } 68 | 69 | public override void HandleRequestInit(string methodFullName, DateTime? deadline) 70 | { 71 | _requestInitTimeoutCts.Cancel(); 72 | if (!_methodHandlers.ContainsKey(methodFullName)) 73 | { 74 | _logger.Log("Unsupported method"); 75 | try 76 | { 77 | WriteTrailers(StatusCode.Unimplemented, ""); 78 | PipeStream.Disconnect(); 79 | } 80 | catch (Exception) 81 | { 82 | // Ignore 83 | } 84 | return; 85 | } 86 | Deadline = new Deadline(deadline); 87 | Task.Run(async () => await _methodHandlers[methodFullName](this).ConfigureAwait(false)); 88 | } 89 | 90 | private void RequestInitTimeout() 91 | { 92 | _logger.Log("Timed out waiting for RequestInit"); 93 | try 94 | { 95 | PipeStream.Disconnect(); 96 | } 97 | catch (Exception) 98 | { 99 | // Ignore 100 | } 101 | } 102 | 103 | public override void HandleHeaders(Metadata headers) => RequestHeaders = headers; 104 | 105 | public override void HandleCancel() => CancellationTokenSource.Cancel(); 106 | 107 | public override void HandleStreamEnd() => _payloadQueue.SetCompleted(); 108 | 109 | public override void HandlePayload(byte[] payload) => _payloadQueue.AppendPayload(payload); 110 | 111 | public void Error(Exception ex) 112 | { 113 | _logger.Log("RPC error"); 114 | IsCompleted = true; 115 | if (Deadline != null && Deadline.IsExpired) 116 | { 117 | WriteTrailers(StatusCode.DeadlineExceeded, ""); 118 | } 119 | else if (CancellationTokenSource.IsCancellationRequested) 120 | { 121 | WriteTrailers(StatusCode.Cancelled, ""); 122 | } 123 | else if (ex is RpcException rpcException) 124 | { 125 | WriteTrailers(rpcException.StatusCode, rpcException.Status.Detail); 126 | } 127 | else 128 | { 129 | WriteTrailers(StatusCode.Unknown, "Exception was thrown by handler."); 130 | } 131 | } 132 | 133 | public void Success(byte[] responsePayload = null) 134 | { 135 | _logger.Log("RPC successful"); 136 | IsCompleted = true; 137 | if (CallContext.Status.StatusCode != StatusCode.OK) 138 | { 139 | WriteTrailers(CallContext.Status.StatusCode, CallContext.Status.Detail); 140 | } 141 | else if (responsePayload != null) 142 | { 143 | Transport.Write() 144 | .Payload(responsePayload) 145 | .Trailers(StatusCode.OK, "", CallContext.ResponseTrailers) 146 | .Commit(); 147 | } 148 | else 149 | { 150 | WriteTrailers(StatusCode.OK, ""); 151 | } 152 | } 153 | 154 | private void WriteTrailers(StatusCode statusCode, string statusDetail) 155 | { 156 | Transport.Write().Trailers(statusCode, statusDetail, CallContext.ResponseTrailers).Commit(); 157 | } 158 | 159 | public void Dispose() 160 | { 161 | _logger.Log("Disposing server context"); 162 | if (!IsCompleted) 163 | { 164 | CancellationTokenSource.Cancel(); 165 | } 166 | _payloadQueue.Dispose(); 167 | PipeStream?.Dispose(); 168 | } 169 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/ServerStreamPool.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class ServerStreamPool : IDisposable 20 | { 21 | private const int PoolSize = 4; 22 | private const int FallbackMin = 100; 23 | private const int FallbackMax = 10_000; 24 | 25 | private readonly CancellationTokenSource _cts = new(); 26 | private readonly string _pipeName; 27 | private readonly NamedPipeServerOptions _options; 28 | private readonly Func _handleConnection; 29 | private readonly Action _invokeError; 30 | private bool _started; 31 | private bool _stopped; 32 | 33 | public ServerStreamPool(string pipeName, NamedPipeServerOptions options, 34 | Func handleConnection, Action invokeError) 35 | { 36 | _pipeName = pipeName; 37 | _options = options; 38 | _handleConnection = handleConnection; 39 | _invokeError = invokeError; 40 | } 41 | 42 | private NamedPipeServerStream CreatePipeServer() 43 | { 44 | var pipeOptions = PipeOptions.Asynchronous; 45 | #if NETFRAMEWORK 46 | return new NamedPipeServerStream(_pipeName, 47 | PipeDirection.InOut, 48 | NamedPipeServerStream.MaxAllowedServerInstances, 49 | PlatformConfig.TransmissionMode, 50 | pipeOptions, 51 | 0, 52 | 0, 53 | _options.PipeSecurity); 54 | #else 55 | #if NET6_0_OR_GREATER 56 | if (_options.CurrentUserOnly) 57 | { 58 | pipeOptions |= PipeOptions.CurrentUserOnly; 59 | } 60 | if (OperatingSystem.IsWindows()) 61 | { 62 | return NamedPipeServerStreamAcl.Create(_pipeName, 63 | PipeDirection.InOut, 64 | NamedPipeServerStream.MaxAllowedServerInstances, 65 | PlatformConfig.TransmissionMode, 66 | pipeOptions, 67 | 0, 68 | 0, 69 | _options.PipeSecurity); 70 | } 71 | #endif 72 | return new NamedPipeServerStream(_pipeName, 73 | PipeDirection.InOut, 74 | NamedPipeServerStream.MaxAllowedServerInstances, 75 | PlatformConfig.TransmissionMode, 76 | pipeOptions); 77 | #endif 78 | } 79 | 80 | public void Start() 81 | { 82 | if (_stopped) 83 | { 84 | throw new InvalidOperationException( 85 | "The server has been killed and can't be restarted. Create a new server if needed."); 86 | } 87 | if (_started) 88 | { 89 | return; 90 | } 91 | 92 | for (int i = 0; i < PoolSize; i++) 93 | { 94 | StartListenThread(); 95 | } 96 | 97 | _started = true; 98 | } 99 | 100 | private void StartListenThread() 101 | { 102 | var thread = new Thread(ConnectionLoop); 103 | thread.Start(); 104 | } 105 | 106 | private void ConnectionLoop() 107 | { 108 | int fallback = FallbackMin; 109 | while (true) 110 | { 111 | try 112 | { 113 | ListenForConnection(); 114 | fallback = FallbackMin; 115 | } 116 | catch (Exception error) 117 | { 118 | if (_cts.IsCancellationRequested) 119 | { 120 | break; 121 | } 122 | _invokeError(error); 123 | Thread.Sleep(fallback); 124 | fallback = Math.Min(fallback * 2, FallbackMax); 125 | } 126 | } 127 | } 128 | 129 | private void ListenForConnection() 130 | { 131 | var pipeServer = CreatePipeServer(); 132 | WaitForConnection(pipeServer); 133 | RunHandleConnection(pipeServer); 134 | } 135 | 136 | private void WaitForConnection(NamedPipeServerStream pipeServer) 137 | { 138 | try 139 | { 140 | pipeServer.WaitForConnectionAsync(_cts.Token).Wait(); 141 | } 142 | catch (Exception) 143 | { 144 | try 145 | { 146 | pipeServer.Disconnect(); 147 | } 148 | catch (Exception) 149 | { 150 | // Ignore disconnection errors 151 | } 152 | pipeServer.Dispose(); 153 | throw; 154 | } 155 | } 156 | 157 | private void RunHandleConnection(NamedPipeServerStream pipeServer) 158 | { 159 | Task.Run(async () => 160 | { 161 | try 162 | { 163 | await _handleConnection(pipeServer); 164 | if (pipeServer.IsConnected) 165 | pipeServer.Disconnect(); 166 | } 167 | catch (Exception error) 168 | { 169 | _invokeError(error); 170 | } 171 | finally 172 | { 173 | pipeServer.Dispose(); 174 | } 175 | }); 176 | } 177 | 178 | public void Dispose() 179 | { 180 | _stopped = true; 181 | try 182 | { 183 | _cts.Cancel(); 184 | } 185 | catch (Exception error) 186 | { 187 | _invokeError(error); 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/SimpleAsyncLock.cs: -------------------------------------------------------------------------------- 1 | namespace GrpcDotNetNamedPipes.Internal; 2 | 3 | internal class SimpleAsyncLock 4 | { 5 | private readonly Queue> _listeners = new(); 6 | private bool _isTaken; 7 | 8 | public Task Take() 9 | { 10 | lock (this) 11 | { 12 | if (!_isTaken) 13 | { 14 | _isTaken = true; 15 | return Task.CompletedTask; 16 | } 17 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 18 | _listeners.Enqueue(tcs); 19 | return tcs.Task; 20 | } 21 | } 22 | 23 | public void Release() 24 | { 25 | lock (this) 26 | { 27 | if (_listeners.Count > 0) 28 | { 29 | _listeners.Dequeue().SetResult(true); 30 | } 31 | else 32 | { 33 | _isTaken = false; 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/StreamWriterImpl.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal class StreamWriterImpl : IAsyncStreamWriter 20 | { 21 | private readonly Marshaller _marshaller; 22 | 23 | public StreamWriterImpl(NamedPipeTransport stream, CancellationToken cancelToken, Marshaller marshaller) 24 | { 25 | Stream = stream; 26 | CancelToken = cancelToken; 27 | _marshaller = marshaller; 28 | } 29 | 30 | public WriteOptions WriteOptions { get; set; } 31 | 32 | protected CancellationToken CancelToken { get; } 33 | 34 | protected NamedPipeTransport Stream { get; } 35 | 36 | public virtual Task WriteAsync(T message) 37 | { 38 | if (CancelToken.IsCancellationRequested) 39 | { 40 | return Task.FromCanceled(CancelToken); 41 | } 42 | 43 | var payload = SerializationHelpers.Serialize(_marshaller, message); 44 | // TODO: Potential for 4x streaming message throughput by queueing up messages and sending multiple at a time 45 | Stream.Write().Payload(payload).Commit(); 46 | return Task.CompletedTask; 47 | } 48 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/Internal/TransportMessageHandler.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes.Internal; 18 | 19 | internal abstract class TransportMessageHandler 20 | { 21 | public virtual void HandleRequestInit(string methodFullName, DateTime? deadline) => 22 | throw new InvalidOperationException(); 23 | 24 | public virtual void HandleHeaders(Metadata headers) => throw new InvalidOperationException(); 25 | public virtual void HandlePayload(byte[] payload) => throw new InvalidOperationException(); 26 | public virtual void HandleCancel() => throw new InvalidOperationException(); 27 | public virtual void HandleStreamEnd() => throw new InvalidOperationException(); 28 | public virtual void HandleTrailers(Metadata trailers, Status status) => throw new InvalidOperationException(); 29 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/NamedPipeCallContext.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes; 18 | 19 | /// 20 | /// A subclass of ServerCallContext for calls to a NamedPipeServer. 21 | /// 22 | /// You only need to use this class in order to call RunAsClient for impersonation. 23 | /// The client needs to set NamedPipeChannelOptions.ImpersonationLevel for this to work. 24 | /// 25 | /// 26 | /// 27 | /// public override Task<HelloResponse> SayHello(HelloRequest request, ServerCallContext context) 28 | /// { 29 | /// var namedPipeCallContext = (NamedPipeCallContext) context; 30 | /// namedPipeCallContext.RunAsClient(DoSomething); 31 | /// return new HelloResponse(); 32 | /// } 33 | /// 34 | /// 35 | public class NamedPipeCallContext : ServerCallContext 36 | { 37 | private readonly ServerConnectionContext _ctx; 38 | 39 | internal NamedPipeCallContext(ServerConnectionContext ctx) 40 | { 41 | _ctx = ctx; 42 | } 43 | 44 | /// 45 | /// Calls a delegate while impersonating the client. 46 | /// 47 | /// The client needs to set NamedPipeChannelOptions.ImpersonationLevel for this to work. 48 | /// 49 | public void RunAsClient(PipeStreamImpersonationWorker impersonationWorker) 50 | { 51 | _ctx.PipeStream.RunAsClient(impersonationWorker); 52 | } 53 | 54 | internal void DisconnectPipeStream() 55 | { 56 | _ctx.PipeStream.Disconnect(); 57 | } 58 | 59 | protected override CancellationToken CancellationTokenCore => 60 | _ctx.CancellationTokenSource.Token; 61 | 62 | protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) 63 | { 64 | _ctx.Transport.Write().Headers(responseHeaders).Commit(); 65 | return Task.CompletedTask; 66 | } 67 | 68 | protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions options) => 69 | throw new NotSupportedException(); 70 | 71 | protected override string MethodCore => throw new NotSupportedException(); 72 | 73 | protected override string HostCore => throw new NotSupportedException(); 74 | 75 | /// 76 | /// Returns a string in the form "net.pipe://localhost/pid/12345" that gives you the machine name (or "localhost") 77 | /// and the process ID of the caller for the current RPC. 78 | /// 79 | /// Only supported on Windows. 80 | protected override string PeerCore 81 | { 82 | get 83 | { 84 | if (Environment.OSVersion.Platform != PlatformID.Win32NT) throw new NotSupportedException(); 85 | var pipeHandle = _ctx.PipeStream.SafePipeHandle.DangerousGetHandle(); 86 | var computerName = PipeInterop.GetClientComputerName(pipeHandle); 87 | var processId = PipeInterop.GetClientProcessId(pipeHandle); 88 | return $"net.pipe://{computerName}/pid/{processId}"; 89 | } 90 | } 91 | 92 | protected override DateTime DeadlineCore => _ctx.Deadline.Value; 93 | 94 | protected override Metadata RequestHeadersCore => _ctx.RequestHeaders; 95 | 96 | protected override Metadata ResponseTrailersCore { get; } = new(); 97 | 98 | protected override Status StatusCore { get; set; } 99 | 100 | protected override WriteOptions WriteOptionsCore 101 | { 102 | get => throw new NotSupportedException(); 103 | set => throw new NotSupportedException(); 104 | } 105 | 106 | protected override AuthContext AuthContextCore => throw new NotSupportedException(); 107 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/NamedPipeChannel.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes; 18 | 19 | public class NamedPipeChannel : CallInvoker 20 | { 21 | private readonly string _serverName; 22 | private readonly string _pipeName; 23 | private readonly NamedPipeChannelOptions _options; 24 | private readonly Action _log; 25 | private readonly SimpleAsyncLock _connectLock = new(); 26 | 27 | public NamedPipeChannel(string serverName, string pipeName) 28 | : this(serverName, pipeName, new NamedPipeChannelOptions()) 29 | { 30 | } 31 | 32 | public NamedPipeChannel(string serverName, string pipeName, NamedPipeChannelOptions options) 33 | : this(serverName, pipeName, options, null) 34 | { 35 | } 36 | 37 | internal NamedPipeChannel(string serverName, string pipeName, NamedPipeChannelOptions options, Action log) 38 | { 39 | _serverName = serverName; 40 | _pipeName = pipeName; 41 | _options = options; 42 | _log = log; 43 | } 44 | 45 | internal Action PipeCallback { get; set; } 46 | 47 | private ClientConnectionContext CreateConnectionContext( 48 | Method method, CallOptions callOptions, TRequest request) 49 | where TRequest : class where TResponse : class 50 | { 51 | var pipeOptions = PipeOptions.Asynchronous; 52 | #if NETCOREAPP || NETSTANDARD2_1 53 | if (_options.CurrentUserOnly) 54 | { 55 | pipeOptions |= PipeOptions.CurrentUserOnly; 56 | } 57 | #endif 58 | 59 | var stream = new NamedPipeClientStream(_serverName, _pipeName, PipeDirection.InOut, 60 | pipeOptions, _options.ImpersonationLevel, HandleInheritability.None); 61 | PipeCallback?.Invoke(stream); 62 | 63 | bool isServerUnary = method.Type == MethodType.Unary || method.Type == MethodType.ClientStreaming; 64 | var logger = ConnectionLogger.Client(_log); 65 | var ctx = new ClientConnectionContext(stream, callOptions, isServerUnary, _options.ConnectionTimeout, 66 | _connectLock, logger); 67 | ctx.InitCall(method, request); 68 | Task.Run(async () => 69 | { 70 | await ctx.InitTask.ConfigureAwait(false); 71 | await new PipeReader(stream, ctx, logger, ctx.Dispose).ReadLoop().ConfigureAwait(false); 72 | }); 73 | return ctx; 74 | } 75 | 76 | public override TResponse BlockingUnaryCall(Method method, 77 | string host, CallOptions callOptions, TRequest request) 78 | { 79 | try 80 | { 81 | var ctx = CreateConnectionContext(method, callOptions, request); 82 | return ctx.GetMessageReader(method.ResponseMarshaller).ReadNextMessage(callOptions.CancellationToken) 83 | .Result; 84 | } 85 | catch (AggregateException ex) 86 | { 87 | // Calling .Result will wrap the original exception inside an AggregateException 88 | throw ex.InnerException!; 89 | } 90 | } 91 | 92 | public override AsyncUnaryCall AsyncUnaryCall( 93 | Method method, string host, CallOptions callOptions, TRequest request) 94 | { 95 | var ctx = CreateConnectionContext(method, callOptions, request); 96 | return new AsyncUnaryCall( 97 | ctx.GetMessageReader(method.ResponseMarshaller).ReadNextMessage(callOptions.CancellationToken), 98 | ctx.ResponseHeadersAsync, 99 | ctx.GetStatus, 100 | ctx.GetTrailers, 101 | ctx.DisposeCall); 102 | } 103 | 104 | public override AsyncServerStreamingCall AsyncServerStreamingCall( 105 | Method method, string host, CallOptions callOptions, 106 | TRequest request) 107 | { 108 | var ctx = CreateConnectionContext(method, callOptions, request); 109 | return new AsyncServerStreamingCall( 110 | ctx.GetMessageReader(method.ResponseMarshaller), 111 | ctx.ResponseHeadersAsync, 112 | ctx.GetStatus, 113 | ctx.GetTrailers, 114 | ctx.DisposeCall); 115 | } 116 | 117 | public override AsyncClientStreamingCall AsyncClientStreamingCall( 118 | Method method, string host, CallOptions callOptions) 119 | { 120 | var ctx = CreateConnectionContext(method, callOptions, null); 121 | return new AsyncClientStreamingCall( 122 | ctx.CreateRequestStream(method.RequestMarshaller), 123 | ctx.GetMessageReader(method.ResponseMarshaller).ReadNextMessage(callOptions.CancellationToken), 124 | ctx.ResponseHeadersAsync, 125 | ctx.GetStatus, 126 | ctx.GetTrailers, 127 | ctx.DisposeCall); 128 | } 129 | 130 | public override AsyncDuplexStreamingCall AsyncDuplexStreamingCall( 131 | Method method, string host, CallOptions callOptions) 132 | { 133 | var ctx = CreateConnectionContext(method, callOptions, null); 134 | return new AsyncDuplexStreamingCall( 135 | ctx.CreateRequestStream(method.RequestMarshaller), 136 | ctx.GetMessageReader(method.ResponseMarshaller), 137 | ctx.ResponseHeadersAsync, 138 | ctx.GetStatus, 139 | ctx.GetTrailers, 140 | ctx.DisposeCall); 141 | } 142 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/NamedPipeChannelOptions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | using System.Security.Principal; 18 | 19 | namespace GrpcDotNetNamedPipes; 20 | 21 | public class NamedPipeChannelOptions 22 | { 23 | #if NET6_0_OR_GREATER 24 | /// 25 | /// Gets or sets a value indicating whether the client pipe can only connect to a server created by the same 26 | /// user. 27 | /// 28 | public bool CurrentUserOnly { get; set; } 29 | #endif 30 | 31 | /// 32 | /// Gets or sets a value indicating the security impersonation level. 33 | /// 34 | public TokenImpersonationLevel ImpersonationLevel { get; set; } 35 | 36 | /// 37 | /// Gets or sets a value indicating the number of milliseconds to wait for the server to respond before the connection times out. 38 | /// 39 | public int ConnectionTimeout { get; set; } = 30 * 1000; 40 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/NamedPipeErrorEventArgs.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes; 18 | 19 | public class NamedPipeErrorEventArgs : EventArgs 20 | { 21 | public Exception Error { get; } 22 | 23 | public NamedPipeErrorEventArgs(Exception error) 24 | { 25 | Error = error; 26 | } 27 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/NamedPipeServer.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes; 18 | 19 | public class NamedPipeServer : IDisposable 20 | { 21 | private readonly ServerStreamPool _pool; 22 | private readonly Action _log; 23 | private readonly Dictionary> _methodHandlers = new(); 24 | 25 | public NamedPipeServer(string pipeName) 26 | : this(pipeName, new NamedPipeServerOptions()) 27 | { 28 | } 29 | 30 | public NamedPipeServer(string pipeName, NamedPipeServerOptions options) 31 | : this(pipeName, options, null) 32 | { 33 | } 34 | 35 | internal NamedPipeServer(string pipeName, NamedPipeServerOptions options, Action log) 36 | { 37 | _pool = new ServerStreamPool(pipeName, options, HandleConnection, InvokeError); 38 | _log = log; 39 | ServiceBinder = new ServiceBinderImpl(this); 40 | } 41 | 42 | public ServiceBinderBase ServiceBinder { get; } 43 | 44 | public event EventHandler Error; 45 | 46 | private void InvokeError(Exception error) 47 | { 48 | Error?.Invoke(this, new NamedPipeErrorEventArgs(error)); 49 | } 50 | 51 | public void Start() 52 | { 53 | _pool.Start(); 54 | } 55 | 56 | public void Kill() 57 | { 58 | _pool.Dispose(); 59 | } 60 | 61 | public void Dispose() 62 | { 63 | _pool.Dispose(); 64 | } 65 | 66 | private async Task HandleConnection(NamedPipeServerStream pipeStream) 67 | { 68 | var logger = ConnectionLogger.Server(_log); 69 | var ctx = new ServerConnectionContext(pipeStream, logger, _methodHandlers); 70 | await Task.Run(new PipeReader(pipeStream, ctx, logger, ctx.Dispose, InvokeError).ReadLoop); 71 | } 72 | 73 | private class ServiceBinderImpl : ServiceBinderBase 74 | { 75 | private readonly NamedPipeServer _server; 76 | 77 | public ServiceBinderImpl(NamedPipeServer server) 78 | { 79 | _server = server; 80 | } 81 | 82 | public override void AddMethod(Method method, 83 | UnaryServerMethod handler) 84 | { 85 | _server._methodHandlers.Add(method.FullName, async ctx => 86 | { 87 | try 88 | { 89 | var request = await ctx.GetMessageReader(method.RequestMarshaller).ReadNextMessage() 90 | .ConfigureAwait(false); 91 | var response = await handler(request, ctx.CallContext).ConfigureAwait(false); 92 | ctx.Success(SerializationHelpers.Serialize(method.ResponseMarshaller, response)); 93 | } 94 | catch (Exception ex) 95 | { 96 | ctx.Error(ex); 97 | } 98 | }); 99 | } 100 | 101 | public override void AddMethod(Method method, 102 | ClientStreamingServerMethod handler) 103 | { 104 | _server._methodHandlers.Add(method.FullName, async ctx => 105 | { 106 | try 107 | { 108 | var response = await handler( 109 | ctx.GetMessageReader(method.RequestMarshaller), 110 | ctx.CallContext).ConfigureAwait(false); 111 | ctx.Success(SerializationHelpers.Serialize(method.ResponseMarshaller, response)); 112 | } 113 | catch (Exception ex) 114 | { 115 | ctx.Error(ex); 116 | } 117 | }); 118 | } 119 | 120 | public override void AddMethod(Method method, 121 | ServerStreamingServerMethod handler) 122 | { 123 | _server._methodHandlers.Add(method.FullName, async ctx => 124 | { 125 | try 126 | { 127 | var request = await ctx.GetMessageReader(method.RequestMarshaller).ReadNextMessage() 128 | .ConfigureAwait(false); 129 | await handler( 130 | request, 131 | ctx.CreateResponseStream(method.ResponseMarshaller), 132 | ctx.CallContext).ConfigureAwait(false); 133 | ctx.Success(); 134 | } 135 | catch (Exception ex) 136 | { 137 | ctx.Error(ex); 138 | } 139 | }); 140 | } 141 | 142 | public override void AddMethod(Method method, 143 | DuplexStreamingServerMethod handler) 144 | { 145 | _server._methodHandlers.Add(method.FullName, async ctx => 146 | { 147 | try 148 | { 149 | await handler( 150 | ctx.GetMessageReader(method.RequestMarshaller), 151 | ctx.CreateResponseStream(method.ResponseMarshaller), 152 | ctx.CallContext).ConfigureAwait(false); 153 | ctx.Success(); 154 | } 155 | catch (Exception ex) 156 | { 157 | ctx.Error(ex); 158 | } 159 | }); 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/NamedPipeServerOptions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | namespace GrpcDotNetNamedPipes; 18 | 19 | public class NamedPipeServerOptions 20 | { 21 | #if NET6_0_OR_GREATER 22 | /// 23 | /// Gets or sets a value indicating whether the server pipe can only be connected to a client created by the 24 | /// same user. 25 | /// 26 | public bool CurrentUserOnly { get; set; } 27 | #endif 28 | #if NETFRAMEWORK || NET6_0_OR_GREATER 29 | /// 30 | /// Gets or sets a value indicating the access control to be used for the pipe. 31 | /// 32 | public PipeSecurity PipeSecurity { get; set; } 33 | #endif 34 | } -------------------------------------------------------------------------------- /GrpcDotNetNamedPipes/public_signing_key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cyanfish/grpc-dotnet-namedpipes/da307b551a9d56b62f5780e91a2d95a699f32430/GrpcDotNetNamedPipes/public_signing_key.snk -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrpcDotNetNamedPipes 2 | 3 | [![NuGet](https://img.shields.io/nuget/v/GrpcDotNetNamedPipes)](https://www.nuget.org/packages/GrpcDotNetNamedPipes/) 4 | 5 | Named pipe transport for [gRPC](https://grpc.io/) in C#/.NET. 6 | 7 | **This is not an official Google product.** 8 | 9 | ## Supported platforms 10 | 11 | - .NET Framework 4.6.2+ (Windows) 12 | - .NET 6+ (Windows, macOS, Linux) 13 | 14 | ## Usage 15 | 16 | Suppose you have a Greeter service as described in 17 | the [gRPC on .NET Core](https://docs.microsoft.com/en-us/aspnet/core/grpc/) intro. 18 | 19 | Server: 20 | 21 | ``` 22 | var server = new NamedPipeServer("MY_PIPE_NAME"); 23 | Greeter.BindService(server.ServiceBinder, new GreeterService()); 24 | server.Start(); 25 | ``` 26 | 27 | Client: 28 | 29 | ``` 30 | var channel = new NamedPipeChannel(".", "MY_PIPE_NAME"); 31 | var client = new Greeter.GreeterClient(channel); 32 | 33 | var response = await client.SayHelloAsync( 34 | new HelloRequest { Name = "World" }); 35 | 36 | Console.WriteLine(response.Message); 37 | ``` 38 | 39 | ## Why named pipes? 40 | 41 | Named pipes are suitable for inter-process communication (IPC). 42 | 43 | Since the introduction of this project, ASP.NET Core has added support for gRPC 44 | over [Unix Domain Sockets](https://learn.microsoft.com/en-us/aspnet/core/grpc/interprocess-uds?view=aspnetcore-8.0) and 45 | over [Named Pipes](https://learn.microsoft.com/en-us/aspnet/core/grpc/interprocess-namedpipes?view=aspnetcore-8.0). Here 46 | is a handy matrix to help you decide what's right for you: 47 | 48 | | | GrpcDotNetNamedPipes | ASP.NET UDS | ASP.NET Named Pipes | ASP.NET HTTP | 49 | |----------------------------|--------------------------------|----------------------------|------------------------------------|---------------------------| 50 | | .NET Platform | .NET Framework 4.6.2
.NET 5 | .NET 5 | .NET 8 (server)
.NET 5 (client) | .NET 5 | 51 | | OS | Windows 7
Mac
Linux | Windows 10
Mac
Linux | Windows 7 | Windows 7
Mac
Linux | 52 | | No firewall warnings | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | 53 | | No network adapter | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :x: | 54 | | Access controls | :heavy_check_mark: | :x: | :heavy_check_mark: | :x: | 55 | | Binary size (trimmed) | **\~ 300 KB** | ~ 7 MB | **\~ 7 MB** | ~ 7 MB | 56 | | Startup time | < 25ms | < 25ms | < 25ms | ~ 250ms | 57 | | Large message throughput | **\~ 500MB/s** | ~ 400MB/s | **\~ 100MB/s** | ~ 100MB/s | 58 | | Streaming messages | ~ 400k/s | ~ 500k/s | ~ 500k/s | ~ 400k/s | 59 | | Method calls | ~ 8000/s | ~ 4000/s | ~ 5000/s | ~ 2500/s | 60 | | Compatible with gRPC-Go | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 61 | | Official Microsoft support | :x: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | 62 | 63 | Performance numbers are based 64 | on [tests](https://github.com/cyanfish/grpc-dotnet-namedpipes/blob/master/GrpcDotNetNamedPipes.PerfTests/GrpcPerformanceTests.cs) 65 | running on Windows 11 with .NET 8. 66 | 67 | ## Caveats 68 | 69 | This implementation currently uses a custom wire protocol so it won't be compatible with other gRPC named pipe 70 | implementations. 71 | 72 | Linux and macOS support is provided for universal compatibility but may not be as optimized as Windows. -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------