Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support named pipe fallback for IPC in local mode on unsupported platforms #15046

Merged
merged 1 commit into from
Sep 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading