Skip to content

Commit

Permalink
Support named pipe fallback for IPC in local mode on unsupported plat…
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-c-martin authored Sep 11, 2024
1 parent 2f0e78d commit c5e58ed
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@ namespace Bicep.Local.Deploy.IntegrationTests;
[TestClass]
public class KestrelProviderExtension : ProviderExtension
{
protected override async Task RunServer(string socketPath, ResourceDispatcher dispatcher, CancellationToken cancellationToken)
protected override async Task RunServer(ConnectionOptions connectionOptions, ResourceDispatcher dispatcher, CancellationToken cancellationToken)
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenUnixSocket(socketPath, listenOptions =>
switch (connectionOptions)
{
listenOptions.Protocols = HttpProtocols.Http2;
});
case { Socket: {}, Pipe: null }:
options.ListenUnixSocket(connectionOptions.Socket, listenOptions => listenOptions.Protocols = HttpProtocols.Http2);
break;
case { Socket: null, Pipe: {} }:
options.ListenNamedPipe(connectionOptions.Pipe, listenOptions => listenOptions.Protocols = HttpProtocols.Http2);
break;
default:
throw new InvalidOperationException("Either socketPath or pipeName must be specified.");
}
});

builder.Services.AddGrpc();
Expand Down
49 changes: 43 additions & 6 deletions src/Bicep.Local.Deploy.IntegrationTests/ProviderExtensionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

using System.Diagnostics;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Nodes;
using Azure.Deployments.Expression.Expressions;
using Bicep.Core.UnitTests;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Mock;
Expand All @@ -13,6 +15,7 @@
using Bicep.Local.Extension.Rpc;
using FluentAssertions;
using Grpc.Core;
using Grpc.Net.Client;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.WindowsAzure.ResourceStack.Common.Json;
using Moq;
Expand All @@ -27,10 +30,14 @@ namespace Bicep.Local.Deploy.IntegrationTests;
[TestClass]
public class ProviderExtensionTests : TestBase
{
private async Task RunExtensionTest(Action<ResourceDispatcherBuilder> registerHandlers, Func<BicepExtension.BicepExtensionClient, CancellationToken, Task> testFunc)
public enum ChannelMode
{
var socketPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.tmp");
UnixDomainSocket,
NamedPipe,
}

private async Task RunExtensionTest(string[] processArgs, Func<GrpcChannel> channelBuilder, Action<ResourceDispatcherBuilder> registerHandlers, Func<BicepExtension.BicepExtensionClient, CancellationToken, Task> testFunc)
{
var testTimeout = TimeSpan.FromMinutes(1);
var cts = new CancellationTokenSource(testTimeout);

Expand All @@ -39,14 +46,13 @@ await Task.WhenAll(
{
var extension = new KestrelProviderExtension();

await extension.RunAsync(["--socket", socketPath], registerHandlers, cts.Token);
await extension.RunAsync(processArgs, registerHandlers, cts.Token);
}),
Task.Run(async () =>
{
try
{
var channel = GrpcChannelHelper.CreateChannel(socketPath);
var client = new BicepExtension.BicepExtensionClient(channel);
var client = new BicepExtension.BicepExtensionClient(channelBuilder());

await GrpcChannelHelper.WaitForConnectionAsync(client, cts.Token);

Expand All @@ -59,9 +65,38 @@ await Task.WhenAll(
}, cts.Token));
}

private static IEnumerable<object[]> GetDataSets()
{
yield return new object[] { ChannelMode.UnixDomainSocket };
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Kestrel only supports named pipes on Windows
yield return new object[] { ChannelMode.NamedPipe };
}
}

[TestMethod]
public async Task Save_request_works_as_expected()
[DynamicData(nameof(GetDataSets), DynamicDataSourceType.Method)]
public async Task Save_request_works_as_expected(ChannelMode mode)
{
string[] processArgs;
Func<GrpcChannel> channelBuilder;
switch (mode)
{
case ChannelMode.UnixDomainSocket:
var socketPath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.tmp");
processArgs = ["--socket", socketPath];
channelBuilder = () => GrpcChannelHelper.CreateDomainSocketChannel(socketPath);
break;
case ChannelMode.NamedPipe:
var pipeName = $"{Guid.NewGuid()}.tmp";
processArgs = ["--pipe", pipeName];
channelBuilder = () => GrpcChannelHelper.CreateNamedPipeChannel(pipeName);
break;
default:
throw new NotImplementedException();
}

JsonObject identifiers = new()
{
{ "name", "someName" },
Expand All @@ -78,6 +113,8 @@ public async Task Save_request_works_as_expected()
null)));

await RunExtensionTest(
processArgs,
channelBuilder,
builder => builder.AddHandler(handlerMock.Object),
async (client, token) =>
{
Expand Down
31 changes: 24 additions & 7 deletions src/Bicep.Local.Deploy/Extensibility/GrpcBuiltInLocalExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// Licensed under the MIT License.

using System.Diagnostics;
using System.Net.Sockets;
using System.Text.Json.Nodes;
using Azure.Deployments.Extensibility.Core.V2.Json;
using Bicep.Local.Extension.Rpc;
using Google.Protobuf.Collections;
using Grpc.Net.Client;
using Json.Pointer;
using Microsoft.WindowsAzure.ResourceStack.Common.Json;
using Newtonsoft.Json.Linq;
Expand All @@ -27,20 +29,36 @@ private GrpcBuiltInLocalExtension(BicepExtension.BicepExtensionClient client, Pr

public static async Task<LocalExtensibilityHost> Start(Uri pathToBinary)
{
var socketName = $"{Guid.NewGuid()}.tmp";
var socketPath = Path.Combine(Path.GetTempPath(), socketName);
string processArgs;
Func<GrpcChannel> channelBuilder;

if (File.Exists(socketPath))
if (Socket.OSSupportsUnixDomainSockets)
{
File.Delete(socketPath);
var socketName = $"{Guid.NewGuid()}.tmp";
var socketPath = Path.Combine(Path.GetTempPath(), socketName);

if (File.Exists(socketPath))
{
File.Delete(socketPath);
}

processArgs = $"--socket {socketPath}";
channelBuilder = () => GrpcChannelHelper.CreateDomainSocketChannel(socketPath);
}
else
{
var pipeName = $"{Guid.NewGuid()}.tmp";

processArgs = $"--pipe {pipeName}";
channelBuilder = () => GrpcChannelHelper.CreateNamedPipeChannel(pipeName);
}

var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = pathToBinary.LocalPath,
Arguments = $"--socket {socketPath}",
Arguments = processArgs,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
Expand All @@ -62,8 +80,7 @@ public static async Task<LocalExtensibilityHost> Start(Uri pathToBinary)
process.BeginErrorReadLine();
process.BeginOutputReadLine();

var channel = GrpcChannelHelper.CreateChannel(socketPath);
var client = new BicepExtension.BicepExtensionClient(channel);
var client = new BicepExtension.BicepExtensionClient(channelBuilder());

await GrpcChannelHelper.WaitForConnectionAsync(client, cts.Token);

Expand Down
15 changes: 11 additions & 4 deletions src/Bicep.Local.Extension.Mock/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,22 @@ public static void RegisterHandlers(ResourceDispatcherBuilder builder) => builde

public class KestrelProviderExtension : ProviderExtension
{
protected override async Task RunServer(string socketPath, ResourceDispatcher dispatcher, CancellationToken cancellationToken)
protected override async Task RunServer(ConnectionOptions connectionOptions, ResourceDispatcher dispatcher, CancellationToken cancellationToken)
{
var builder = WebApplication.CreateBuilder();
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenUnixSocket(socketPath, listenOptions =>
switch (connectionOptions)
{
listenOptions.Protocols = HttpProtocols.Http2;
});
case { Socket: {}, Pipe: null }:
options.ListenUnixSocket(connectionOptions.Socket, listenOptions => listenOptions.Protocols = HttpProtocols.Http2);
break;
case { Socket: null, Pipe: {} }:
options.ListenNamedPipe(connectionOptions.Pipe, listenOptions => listenOptions.Protocols = HttpProtocols.Http2);
break;
default:
throw new InvalidOperationException("Either socketPath or pipeName must be specified.");
}
});

builder.Services.AddGrpc();
Expand Down
18 changes: 10 additions & 8 deletions src/Bicep.Local.Extension/ProviderExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,18 @@ namespace Bicep.Local.Extension;

public abstract class ProviderExtension
{
public record ConnectionOptions(
string? Socket,
string? Pipe);

internal class CommandLineOptions
{
[Option("socket", Required = false, HelpText = "The path to the domain socket to connect on")]
public string? Socket { get; set; }

[Option("pipe", Required = false, HelpText = "The named pipe to connect on")]
public string? Pipe { get; set; }

[Option("wait-for-debugger", Required = false, HelpText = "If set, wait for a dotnet debugger to be attached before starting the server")]
public bool WaitForDebugger { get; set; }
}
Expand Down Expand Up @@ -41,7 +48,7 @@ await parser.ParseArguments<CommandLineOptions>(args)
.WithParsedAsync(async options => await RunServer(registerHandlers, options, cancellationToken));
}

protected abstract Task RunServer(string socketPath, ResourceDispatcher dispatcher, CancellationToken cancellationToken);
protected abstract Task RunServer(ConnectionOptions options, ResourceDispatcher dispatcher, CancellationToken cancellationToken);

private async Task RunServer(Action<ResourceDispatcherBuilder> registerHandlers, CommandLineOptions options, CancellationToken cancellationToken)
{
Expand All @@ -64,13 +71,8 @@ private async Task RunServer(Action<ResourceDispatcherBuilder> registerHandlers,
registerHandlers(handlerBuilder);
var dispatcher = handlerBuilder.Build();

if (options.Socket is { } socketPath)
{
await Task.WhenAny(RunServer(socketPath, dispatcher, cancellationToken), WaitForCancellation(cancellationToken));
return;
}

throw new NotImplementedException();
ConnectionOptions connectionOptions = new(options.Socket, options.Pipe);
await Task.WhenAny(RunServer(connectionOptions, dispatcher, cancellationToken), WaitForCancellation(cancellationToken));
}

private static async Task WaitForCancellation(CancellationToken cancellationToken)
Expand Down
40 changes: 39 additions & 1 deletion src/Bicep.Local.Extension/Rpc/GrpcChannelHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
// Licensed under the MIT License.

using System.Collections.Immutable;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.AccessControl;
using System.Security.Principal;
using Grpc.Core;
using Grpc.Net.Client;

Expand Down Expand Up @@ -38,7 +42,7 @@ public async ValueTask<Stream> ConnectAsync(SocketsHttpConnectionContext _,
}
}

public static GrpcChannel CreateChannel(string socketPath)
public static GrpcChannel CreateDomainSocketChannel(string socketPath)
{
var udsEndPoint = new UnixDomainSocketEndPoint(socketPath);
var connectionFactory = new UnixDomainSocketsConnectionFactory(udsEndPoint);
Expand All @@ -54,6 +58,40 @@ public static GrpcChannel CreateChannel(string socketPath)
});
}

public static GrpcChannel CreateNamedPipeChannel(string pipeName)
{
static async ValueTask<Stream> connectPipe(string pipeName, CancellationToken cancellationToken)
{
var clientStream = new NamedPipeClientStream(
serverName: ".",
pipeName: pipeName,
direction: PipeDirection.InOut,
options: PipeOptions.WriteThrough | PipeOptions.Asynchronous,
impersonationLevel: TokenImpersonationLevel.Anonymous);

try
{
await clientStream.ConnectAsync(cancellationToken).ConfigureAwait(false);
return clientStream;
}
catch
{
await clientStream.DisposeAsync();
throw;
}
}

var socketsHttpHandler = new SocketsHttpHandler
{
ConnectCallback = (context, cancellationToken) => connectPipe(pipeName, cancellationToken),
};

return GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
{
HttpHandler = socketsHttpHandler
});
}

public static async Task WaitForConnectionAsync(BicepExtension.BicepExtensionClient client, CancellationToken cancellationToken)
{
var connected = false;
Expand Down

0 comments on commit c5e58ed

Please sign in to comment.