Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a7a9e4d11 |
@@ -1,14 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MxGateway.Client\MxGateway.Client.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
using MxGateway.Client;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
public static class MxGatewayClientCli
|
|
||||||
{
|
|
||||||
public static int Run(
|
|
||||||
string[] args,
|
|
||||||
TextWriter standardOutput,
|
|
||||||
TextWriter standardError)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(args);
|
|
||||||
ArgumentNullException.ThrowIfNull(standardOutput);
|
|
||||||
ArgumentNullException.ThrowIfNull(standardError);
|
|
||||||
|
|
||||||
if (args.Length is 0 || IsHelp(args[0]))
|
|
||||||
{
|
|
||||||
WriteUsage(standardOutput);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.Equals(args[0], "version", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
standardOutput.WriteLine(
|
|
||||||
$"gateway-protocol={MxGatewayClientContractInfo.GatewayProtocolVersion}");
|
|
||||||
standardOutput.WriteLine(
|
|
||||||
$"worker-protocol={MxGatewayClientContractInfo.WorkerProtocolVersion}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
standardError.WriteLine($"Unknown command '{args[0]}'.");
|
|
||||||
WriteUsage(standardError);
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsHelp(string value)
|
|
||||||
{
|
|
||||||
return string.Equals(value, "-h", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(value, "--help", StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals(value, "help", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteUsage(TextWriter writer)
|
|
||||||
{
|
|
||||||
writer.WriteLine("mxgw-dotnet version");
|
|
||||||
writer.WriteLine("mxgw-dotnet --help");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
using MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
return MxGatewayClientCli.Run(args, Console.Out, Console.Error);
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
|
|
||||||
{
|
|
||||||
private readonly Queue<MxCommandReply> _invokeReplies = new();
|
|
||||||
private readonly List<MxEvent> _events = [];
|
|
||||||
|
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
|
||||||
|
|
||||||
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
|
|
||||||
|
|
||||||
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
|
|
||||||
|
|
||||||
public List<(CloseSessionRequest Request, CallOptions CallOptions)> CloseSessionCalls { get; } = [];
|
|
||||||
|
|
||||||
public List<(MxCommandRequest Request, CallOptions CallOptions)> InvokeCalls { get; } = [];
|
|
||||||
|
|
||||||
public List<(StreamEventsRequest Request, CallOptions CallOptions)> StreamEventsCalls { get; } = [];
|
|
||||||
|
|
||||||
public OpenSessionReply OpenSessionReply { get; set; } = new()
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
BackendName = "mxaccess-worker",
|
|
||||||
GatewayProtocolVersion = 1,
|
|
||||||
WorkerProtocolVersion = 1,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
};
|
|
||||||
|
|
||||||
public CloseSessionReply CloseSessionReply { get; set; } = new()
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
FinalState = SessionState.Closed,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
};
|
|
||||||
|
|
||||||
public Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
OpenSessionCalls.Add((request, callOptions));
|
|
||||||
return Task.FromResult(OpenSessionReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
CloseSessionCalls.Add((request, callOptions));
|
|
||||||
return Task.FromResult(CloseSessionReply);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
InvokeCalls.Add((request, callOptions));
|
|
||||||
return Task.FromResult(_invokeReplies.Dequeue());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
StreamEventsCalls.Add((request, callOptions));
|
|
||||||
|
|
||||||
foreach (MxEvent gatewayEvent in _events)
|
|
||||||
{
|
|
||||||
callOptions.CancellationToken.ThrowIfCancellationRequested();
|
|
||||||
await Task.Yield();
|
|
||||||
yield return gatewayEvent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddInvokeReply(MxCommandReply reply)
|
|
||||||
{
|
|
||||||
_invokeReplies.Enqueue(reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddEvent(MxEvent gatewayEvent)
|
|
||||||
{
|
|
||||||
_events.Add(gatewayEvent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Using Include="Xunit" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\MxGateway.Client\MxGateway.Client.csproj" />
|
|
||||||
<ProjectReference Include="..\MxGateway.Client.Cli\MxGateway.Client.Cli.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
public sealed class MxGatewayClientCliTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Run_Version_PrintsCompiledProtocolVersions()
|
|
||||||
{
|
|
||||||
using var output = new StringWriter();
|
|
||||||
using var error = new StringWriter();
|
|
||||||
|
|
||||||
var exitCode = MxGatewayClientCli.Run(["version"], output, error);
|
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
|
||||||
Assert.Contains("gateway-protocol=1", output.ToString());
|
|
||||||
Assert.Contains("worker-protocol=1", output.ToString());
|
|
||||||
Assert.Equal(string.Empty, error.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using MxGateway.Contracts;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
public sealed class MxGatewayClientContractInfoTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void GatewayProtocolVersion_MatchesSharedContract()
|
|
||||||
{
|
|
||||||
Assert.Equal(
|
|
||||||
GatewayContractInfo.GatewayProtocolVersion,
|
|
||||||
MxGatewayClientContractInfo.GatewayProtocolVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void WorkerProtocolVersion_MatchesSharedContract()
|
|
||||||
{
|
|
||||||
Assert.Equal(
|
|
||||||
GatewayContractInfo.WorkerProtocolVersion,
|
|
||||||
MxGatewayClientContractInfo.WorkerProtocolVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
namespace MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
public sealed class MxGatewayClientOptionsTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Validate_WithAbsoluteEndpointAndApiKey_Succeeds()
|
|
||||||
{
|
|
||||||
var options = new MxGatewayClientOptions
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("http://localhost:5000"),
|
|
||||||
ApiKey = "test-api-key",
|
|
||||||
};
|
|
||||||
|
|
||||||
options.Validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Validate_WithEmptyApiKey_Throws()
|
|
||||||
{
|
|
||||||
var options = new MxGatewayClientOptions
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("http://localhost:5000"),
|
|
||||||
ApiKey = "",
|
|
||||||
};
|
|
||||||
|
|
||||||
Assert.Throws<ArgumentException>(options.Validate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
public sealed class MxGatewayClientSessionTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
|
|
||||||
{
|
|
||||||
using CancellationTokenSource cancellation = new();
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
await using MxGatewayClient client = CreateClient(transport);
|
|
||||||
|
|
||||||
await client.OpenSessionRawAsync(new OpenSessionRequest(), cancellation.Token);
|
|
||||||
|
|
||||||
var call = Assert.Single(transport.OpenSessionCalls);
|
|
||||||
Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
|
|
||||||
Assert.Equal(cancellation.Token, call.CallOptions.CancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
|
|
||||||
{
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
transport.OpenSessionReply.WorkerProcessId = 1234;
|
|
||||||
await using MxGatewayClient client = CreateClient(transport);
|
|
||||||
|
|
||||||
MxGatewaySession session = await client.OpenSessionAsync();
|
|
||||||
|
|
||||||
Assert.Equal("session-fixture", session.SessionId);
|
|
||||||
Assert.Same(transport.OpenSessionReply, session.OpenSessionReply);
|
|
||||||
Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
|
|
||||||
{
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
transport.AddInvokeReply(new MxCommandReply
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Kind = MxCommandKind.Register,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
Register = new RegisterReply { ServerHandle = 12 },
|
|
||||||
});
|
|
||||||
await using MxGatewayClient client = CreateClient(transport);
|
|
||||||
MxGatewaySession session = await client.OpenSessionAsync();
|
|
||||||
|
|
||||||
int serverHandle = await session.RegisterAsync("fixture-client");
|
|
||||||
|
|
||||||
Assert.Equal(12, serverHandle);
|
|
||||||
var call = Assert.Single(transport.InvokeCalls);
|
|
||||||
Assert.Equal("session-fixture", call.Request.SessionId);
|
|
||||||
Assert.False(string.IsNullOrWhiteSpace(call.Request.ClientCorrelationId));
|
|
||||||
Assert.Equal(MxCommandKind.Register, call.Request.Command.Kind);
|
|
||||||
Assert.Equal("fixture-client", call.Request.Command.Register.ClientName);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
|
|
||||||
{
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
transport.AddInvokeReply(new MxCommandReply
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Kind = MxCommandKind.AddItem2,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
AddItem2 = new AddItem2Reply { ItemHandle = 34 },
|
|
||||||
});
|
|
||||||
await using MxGatewayClient client = CreateClient(transport);
|
|
||||||
MxGatewaySession session = await client.OpenSessionAsync();
|
|
||||||
|
|
||||||
int itemHandle = await session.AddItem2Async(12, "Area001.Pump001.Speed", "runtime");
|
|
||||||
|
|
||||||
Assert.Equal(34, itemHandle);
|
|
||||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
|
||||||
Assert.Equal(MxCommandKind.AddItem2, request.Command.Kind);
|
|
||||||
Assert.Equal(12, request.Command.AddItem2.ServerHandle);
|
|
||||||
Assert.Equal("Area001.Pump001.Speed", request.Command.AddItem2.ItemDefinition);
|
|
||||||
Assert.Equal("runtime", request.Command.AddItem2.ItemContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
|
|
||||||
{
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
transport.AddInvokeReply(new MxCommandReply
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Kind = MxCommandKind.Write,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
});
|
|
||||||
await using MxGatewayClient client = CreateClient(transport);
|
|
||||||
MxGatewaySession session = await client.OpenSessionAsync();
|
|
||||||
MxValue value = new()
|
|
||||||
{
|
|
||||||
DataType = MxDataType.Integer,
|
|
||||||
VariantType = "VT_I4",
|
|
||||||
Int32Value = 123,
|
|
||||||
};
|
|
||||||
|
|
||||||
MxCommandReply reply = await session.WriteRawAsync(12, 34, value, 56);
|
|
||||||
|
|
||||||
Assert.Equal(MxCommandKind.Write, reply.Kind);
|
|
||||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
|
||||||
Assert.Equal(MxCommandKind.Write, request.Command.Kind);
|
|
||||||
Assert.Equal(12, request.Command.Write.ServerHandle);
|
|
||||||
Assert.Equal(34, request.Command.Write.ItemHandle);
|
|
||||||
Assert.Same(value, request.Command.Write.Value);
|
|
||||||
Assert.Equal(56, request.Command.Write.UserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
|
|
||||||
{
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
transport.AddEvent(new MxEvent
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Family = MxEventFamily.OnDataChange,
|
|
||||||
WorkerSequence = 1,
|
|
||||||
});
|
|
||||||
transport.AddEvent(new MxEvent
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Family = MxEventFamily.OnWriteComplete,
|
|
||||||
WorkerSequence = 2,
|
|
||||||
});
|
|
||||||
await using MxGatewayClient client = CreateClient(transport);
|
|
||||||
MxGatewaySession session = await client.OpenSessionAsync();
|
|
||||||
List<ulong> sequences = [];
|
|
||||||
|
|
||||||
await foreach (MxEvent gatewayEvent in session.StreamEventsAsync(afterWorkerSequence: 0))
|
|
||||||
{
|
|
||||||
sequences.Add(gatewayEvent.WorkerSequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
Assert.Equal([1UL, 2UL], sequences);
|
|
||||||
StreamEventsRequest request = Assert.Single(transport.StreamEventsCalls).Request;
|
|
||||||
Assert.Equal("session-fixture", request.SessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task CloseAsync_IsExplicitAndIdempotent()
|
|
||||||
{
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
await using MxGatewayClient client = CreateClient(transport);
|
|
||||||
MxGatewaySession session = await client.OpenSessionAsync();
|
|
||||||
|
|
||||||
CloseSessionReply first = await session.CloseAsync();
|
|
||||||
CloseSessionReply second = await session.CloseAsync();
|
|
||||||
|
|
||||||
Assert.Same(first, second);
|
|
||||||
var call = Assert.Single(transport.CloseSessionCalls);
|
|
||||||
Assert.Equal("session-fixture", call.Request.SessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task InvokeHelpers_PassCancellationTokenToTransport()
|
|
||||||
{
|
|
||||||
using CancellationTokenSource cancellation = new();
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
transport.AddInvokeReply(new MxCommandReply
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Kind = MxCommandKind.Advise,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
});
|
|
||||||
await using MxGatewayClient client = CreateClient(transport);
|
|
||||||
MxGatewaySession session = await client.OpenSessionAsync();
|
|
||||||
|
|
||||||
await session.AdviseAsync(12, 34, cancellation.Token);
|
|
||||||
|
|
||||||
Assert.Equal(cancellation.Token, Assert.Single(transport.InvokeCalls).CallOptions.CancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxGatewayClient CreateClient(FakeGatewayTransport transport)
|
|
||||||
{
|
|
||||||
return new MxGatewayClient(transport.Options, transport);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static FakeGatewayTransport CreateTransport()
|
|
||||||
{
|
|
||||||
return new FakeGatewayTransport(new MxGatewayClientOptions
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("http://localhost:5000"),
|
|
||||||
ApiKey = "test-api-key",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
namespace MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
public sealed class MxGatewayGeneratedContractTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
|
|
||||||
{
|
|
||||||
var options = new MxGatewayClientOptions
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("http://localhost:5000"),
|
|
||||||
ApiKey = "test-api-key",
|
|
||||||
};
|
|
||||||
|
|
||||||
await using var client = MxGatewayClient.Create(options);
|
|
||||||
|
|
||||||
Assert.NotNull(client.RawClient);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.0.31903.59
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Client", "MxGateway.Client\MxGateway.Client.csproj", "{7CF9ED88-1F32-4040-BEB1-D0902E304C70}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Contracts", "..\..\src\MxGateway.Contracts\MxGateway.Contracts.csproj", "{9AB807A8-0469-40F7-A000-D240F36B6E5D}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Client.Cli", "MxGateway.Client.Cli\MxGateway.Client.Cli.csproj", "{EB061E77-2475-4322-9257-3F2456DD141C}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MxGateway.Client.Tests", "MxGateway.Client.Tests\MxGateway.Client.Tests.csproj", "{B77B5A8E-0C53-4419-9BCD-227C9753A074}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Debug|x64 = Debug|x64
|
|
||||||
Debug|x86 = Debug|x86
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
Release|x64 = Release|x64
|
|
||||||
Release|x86 = Release|x86
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{7CF9ED88-1F32-4040-BEB1-D0902E304C70}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{9AB807A8-0469-40F7-A000-D240F36B6E5D}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{EB061E77-2475-4322-9257-3F2456DD141C}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x64.ActiveCfg = Debug|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x64.Build.0 = Debug|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x86.ActiveCfg = Debug|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Debug|x86.Build.0 = Debug|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x64.ActiveCfg = Release|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x64.Build.0 = Release|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x86.ActiveCfg = Release|Any CPU
|
|
||||||
{B77B5A8E-0C53-4419-9BCD-227C9753A074}.Release|x86.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
internal sealed class GrpcMxGatewayClientTransport(
|
|
||||||
MxGatewayClientOptions options,
|
|
||||||
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
|
|
||||||
{
|
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
|
||||||
|
|
||||||
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
|
|
||||||
|
|
||||||
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
|
|
||||||
|
|
||||||
public async Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
return await RawClient.OpenSessionAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
return await RawClient.CloseSessionAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
return await RawClient.InvokeAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CallOptions callOptions,
|
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
CancellationToken effectiveCancellationToken = cancellationToken.CanBeCanceled
|
|
||||||
? cancellationToken
|
|
||||||
: callOptions.CancellationToken;
|
|
||||||
|
|
||||||
using AsyncServerStreamingCall<MxEvent> call = RawClient.StreamEvents(request, callOptions);
|
|
||||||
|
|
||||||
await foreach (MxEvent gatewayEvent in call.ResponseStream
|
|
||||||
.ReadAllAsync(effectiveCancellationToken)
|
|
||||||
.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
yield return gatewayEvent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
return StreamEventsAsync(request, callOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
internal interface IMxGatewayClientTransport
|
|
||||||
{
|
|
||||||
MxGatewayClientOptions Options { get; }
|
|
||||||
|
|
||||||
MxAccessGateway.MxAccessGatewayClient? RawClient { get; }
|
|
||||||
|
|
||||||
Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
|
|
||||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CallOptions callOptions);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\..\src\MxGateway.Contracts\MxGateway.Contracts.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.7" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net10.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
using Grpc.Core;
|
|
||||||
using Grpc.Net.Client;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provides the .NET client entry point for the public MXAccess Gateway gRPC API.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class MxGatewayClient : IAsyncDisposable
|
|
||||||
{
|
|
||||||
private readonly GrpcChannel _channel;
|
|
||||||
private readonly IMxGatewayClientTransport _transport;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
internal MxGatewayClient(
|
|
||||||
MxGatewayClientOptions options,
|
|
||||||
IMxGatewayClientTransport transport)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
|
||||||
options.Validate();
|
|
||||||
|
|
||||||
Options = options;
|
|
||||||
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
|
||||||
_channel = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private MxGatewayClient(
|
|
||||||
GrpcChannel channel,
|
|
||||||
IMxGatewayClientTransport transport)
|
|
||||||
{
|
|
||||||
_channel = channel;
|
|
||||||
_transport = transport;
|
|
||||||
Options = transport.Options;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxGatewayClientOptions Options { get; }
|
|
||||||
|
|
||||||
public MxAccessGateway.MxAccessGatewayClient RawClient =>
|
|
||||||
_transport.RawClient
|
|
||||||
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
|
|
||||||
|
|
||||||
public static MxGatewayClient Create(MxGatewayClientOptions options)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
|
||||||
options.Validate();
|
|
||||||
|
|
||||||
var channel = GrpcChannel.ForAddress(
|
|
||||||
options.Endpoint,
|
|
||||||
new GrpcChannelOptions
|
|
||||||
{
|
|
||||||
LoggerFactory = options.LoggerFactory,
|
|
||||||
});
|
|
||||||
|
|
||||||
return new MxGatewayClient(
|
|
||||||
channel,
|
|
||||||
new GrpcMxGatewayClientTransport(
|
|
||||||
options,
|
|
||||||
new MxAccessGateway.MxAccessGatewayClient(channel)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MxGatewaySession> OpenSessionAsync(
|
|
||||||
OpenSessionRequest? request = null,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
OpenSessionReply reply = await OpenSessionRawAsync(
|
|
||||||
request ?? new OpenSessionRequest(),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
return new MxGatewaySession(this, reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<OpenSessionReply> OpenSessionRawAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
return _transport.OpenSessionAsync(request, CreateCallOptions(cancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<CloseSessionReply> CloseSessionRawAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
return _transport.CloseSessionAsync(request, CreateCallOptions(cancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
return _transport.InvokeAsync(request, CreateCallOptions(cancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
|
||||||
ThrowIfDisposed();
|
|
||||||
|
|
||||||
return _transport.StreamEventsAsync(request, CreateCallOptions(cancellationToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return ValueTask.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
_channel?.Dispose();
|
|
||||||
return ValueTask.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Metadata headers = new()
|
|
||||||
{
|
|
||||||
{ "authorization", $"Bearer {Options.ApiKey}" },
|
|
||||||
};
|
|
||||||
|
|
||||||
return new CallOptions(
|
|
||||||
headers,
|
|
||||||
DateTime.UtcNow.Add(Options.DefaultCallTimeout),
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ThrowIfDisposed()
|
|
||||||
{
|
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using MxGateway.Contracts;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Exposes the protocol versions compiled into this client package.
|
|
||||||
/// </summary>
|
|
||||||
public static class MxGatewayClientContractInfo
|
|
||||||
{
|
|
||||||
public const uint GatewayProtocolVersion =
|
|
||||||
GatewayContractInfo.GatewayProtocolVersion;
|
|
||||||
|
|
||||||
public const uint WorkerProtocolVersion =
|
|
||||||
GatewayContractInfo.WorkerProtocolVersion;
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configures the gRPC channel used by the .NET MXAccess Gateway client.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class MxGatewayClientOptions
|
|
||||||
{
|
|
||||||
public required Uri Endpoint { get; init; }
|
|
||||||
|
|
||||||
public required string ApiKey { get; init; }
|
|
||||||
|
|
||||||
public bool UseTls { get; init; }
|
|
||||||
|
|
||||||
public string? CaCertificatePath { get; init; }
|
|
||||||
|
|
||||||
public string? ServerNameOverride { get; init; }
|
|
||||||
|
|
||||||
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
|
|
||||||
|
|
||||||
public ILoggerFactory? LoggerFactory { get; init; }
|
|
||||||
|
|
||||||
public void Validate()
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(Endpoint);
|
|
||||||
|
|
||||||
if (!Endpoint.IsAbsoluteUri)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
"The gateway endpoint must be an absolute URI.",
|
|
||||||
nameof(Endpoint));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(ApiKey))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
"The gateway API key must not be empty.",
|
|
||||||
nameof(ApiKey));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ConnectTimeout <= TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(ConnectTimeout),
|
|
||||||
"The connect timeout must be greater than zero.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DefaultCallTimeout <= TimeSpan.Zero)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(DefaultCallTimeout),
|
|
||||||
"The default call timeout must be greater than zero.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents one gateway-backed MXAccess session.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class MxGatewaySession : IAsyncDisposable
|
|
||||||
{
|
|
||||||
private readonly MxGatewayClient _client;
|
|
||||||
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
|
||||||
private CloseSessionReply? _closeReply;
|
|
||||||
|
|
||||||
internal MxGatewaySession(
|
|
||||||
MxGatewayClient client,
|
|
||||||
OpenSessionReply openSessionReply)
|
|
||||||
{
|
|
||||||
_client = client ?? throw new ArgumentNullException(nameof(client));
|
|
||||||
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string SessionId => OpenSessionReply.SessionId;
|
|
||||||
|
|
||||||
public OpenSessionReply OpenSessionReply { get; }
|
|
||||||
|
|
||||||
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
if (_closeReply is not null)
|
|
||||||
{
|
|
||||||
return _closeReply;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _closeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (_closeReply is not null)
|
|
||||||
{
|
|
||||||
return _closeReply;
|
|
||||||
}
|
|
||||||
|
|
||||||
_closeReply = await _client.CloseSessionRawAsync(
|
|
||||||
new CloseSessionRequest { SessionId = SessionId },
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return _closeReply;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_closeLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> RegisterAsync(
|
|
||||||
string clientName,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await RegisterRawAsync(clientName, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> RegisterRawAsync(
|
|
||||||
string clientName,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(clientName);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Register,
|
|
||||||
Register = new RegisterCommand { ClientName = clientName },
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> AddItemAsync(
|
|
||||||
int serverHandle,
|
|
||||||
string itemDefinition,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await AddItemRawAsync(
|
|
||||||
serverHandle,
|
|
||||||
itemDefinition,
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> AddItemRawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
string itemDefinition,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.AddItem,
|
|
||||||
AddItem = new AddItemCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemDefinition = itemDefinition,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<int> AddItem2Async(
|
|
||||||
int serverHandle,
|
|
||||||
string itemDefinition,
|
|
||||||
string itemContext,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await AddItem2RawAsync(
|
|
||||||
serverHandle,
|
|
||||||
itemDefinition,
|
|
||||||
itemContext,
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> AddItem2RawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
string itemDefinition,
|
|
||||||
string itemContext,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(itemDefinition);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.AddItem2,
|
|
||||||
AddItem2 = new AddItem2Command
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemDefinition = itemDefinition,
|
|
||||||
ItemContext = itemContext ?? string.Empty,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task AdviseAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
await AdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> AdviseRawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Advise,
|
|
||||||
Advise = new AdviseCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WriteAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
MxValue value,
|
|
||||||
int userId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
await WriteRawAsync(serverHandle, itemHandle, value, userId, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> WriteRawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
MxValue value,
|
|
||||||
int userId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Write,
|
|
||||||
Write = new WriteCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
Value = value,
|
|
||||||
UserId = userId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
|
||||||
return _client.InvokeAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
ulong afterWorkerSequence = 0,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return _client.StreamEventsAsync(
|
|
||||||
new StreamEventsRequest
|
|
||||||
{
|
|
||||||
SessionId = SessionId,
|
|
||||||
AfterWorkerSequence = afterWorkerSequence,
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
await CloseAsync().ConfigureAwait(false);
|
|
||||||
_closeLock.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<MxCommandReply> InvokeCommandAsync(
|
|
||||||
MxCommand command,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return _client.InvokeAsync(
|
|
||||||
new MxCommandRequest
|
|
||||||
{
|
|
||||||
SessionId = SessionId,
|
|
||||||
ClientCorrelationId = Guid.NewGuid().ToString("N"),
|
|
||||||
Command = command,
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("MxGateway.Client.Tests")]
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# .NET Client Projects
|
|
||||||
|
|
||||||
The .NET client workspace contains the MXAccess Gateway client library, test
|
|
||||||
CLI, and unit tests.
|
|
||||||
|
|
||||||
## Projects
|
|
||||||
|
|
||||||
| Project | Purpose |
|
|
||||||
|---------|---------|
|
|
||||||
| `MxGateway.Client` | .NET 10 library entry point, raw gRPC calls, and session helpers. |
|
|
||||||
| `MxGateway.Client.Cli` | Test CLI for smoke and diagnostic commands. |
|
|
||||||
| `MxGateway.Client.Tests` | Unit tests for client options, generated contract wiring, auth metadata, session helpers, cancellation, and event streaming. |
|
|
||||||
|
|
||||||
The projects reference `src/MxGateway.Contracts/MxGateway.Contracts.csproj` so
|
|
||||||
the client compiles against the same generated protobuf and gRPC types as the
|
|
||||||
gateway. `clients/dotnet/generated` remains reserved for generator output if a
|
|
||||||
future client build switches to client-local `Grpc.Tools` generation.
|
|
||||||
|
|
||||||
## Build And Test
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet build clients/dotnet/MxGateway.Client.sln
|
|
||||||
dotnet test clients/dotnet/MxGateway.Client.sln --no-build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Client Usage
|
|
||||||
|
|
||||||
`MxGatewayClient` opens a gRPC channel to the gateway and attaches the API key
|
|
||||||
to every unary and streaming call as `authorization: Bearer <api-key>`.
|
|
||||||
Cancellation tokens passed to the public methods flow to the generated gRPC
|
|
||||||
call. Client-side cancellation stops waiting for the gateway response; it does
|
|
||||||
not abort an MXAccess COM call that is already executing inside a worker.
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
await using MxGatewayClient client = MxGatewayClient.Create(
|
|
||||||
new MxGatewayClientOptions
|
|
||||||
{
|
|
||||||
Endpoint = new Uri("http://localhost:5000"),
|
|
||||||
ApiKey = apiKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
MxGatewaySession session = await client.OpenSessionAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
int serverHandle = await session.RegisterAsync("sample-client");
|
|
||||||
int itemHandle = await session.AddItemAsync(
|
|
||||||
serverHandle,
|
|
||||||
"Area001.Pump001.Speed");
|
|
||||||
|
|
||||||
await session.AdviseAsync(serverHandle, itemHandle);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
await session.CloseAsync();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `OpenSessionRawAsync`, `CloseSessionRawAsync`, `InvokeAsync`, and
|
|
||||||
`StreamEventsAsync` when tests or parity tools need direct generated protobuf
|
|
||||||
messages. `MxGatewaySession.OpenSessionReply` keeps the raw session-open reply
|
|
||||||
available, and command helpers have `*RawAsync` variants when callers need the
|
|
||||||
complete `MxCommandReply`.
|
|
||||||
|
|
||||||
`MxGatewaySession.CloseAsync` is explicit and idempotent. Repeated calls return
|
|
||||||
the first `CloseSessionReply` instead of sending another close request.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# Go Client
|
|
||||||
|
|
||||||
The Go client module contains the generated MXAccess Gateway protobuf bindings,
|
|
||||||
a small handwritten `mxgateway` package, and the `mxgw-go` test CLI scaffold.
|
|
||||||
The module uses the shared proto inputs documented in
|
|
||||||
`../../docs/client-proto-generation.md` so gateway and client contracts stay in
|
|
||||||
sync.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```text
|
|
||||||
clients/go/
|
|
||||||
go.mod
|
|
||||||
generate-proto.ps1
|
|
||||||
internal/generated/
|
|
||||||
mxgateway/
|
|
||||||
cmd/mxgw-go/
|
|
||||||
```
|
|
||||||
|
|
||||||
`internal/generated` contains code produced by `protoc`, `protoc-gen-go`, and
|
|
||||||
`protoc-gen-go-grpc`. Do not edit generated files by hand.
|
|
||||||
|
|
||||||
## Regenerating Protobuf Bindings
|
|
||||||
|
|
||||||
Run generation after the shared `.proto` files or the Go output path changes:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
./generate-proto.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
The script uses the tool paths recorded in `../../docs/toolchain-links.md`.
|
|
||||||
|
|
||||||
## Build And Test
|
|
||||||
|
|
||||||
Run the Go module checks from `clients/go`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
go test ./...
|
|
||||||
go build ./...
|
|
||||||
go vet ./...
|
|
||||||
```
|
|
||||||
|
|
||||||
The tests parse the shared JSON fixtures, exercise value and status conversion,
|
|
||||||
use `bufconn` for fake gateway auth and streaming behavior, and cover CLI JSON
|
|
||||||
redaction.
|
|
||||||
|
|
||||||
## Client API
|
|
||||||
|
|
||||||
Use `mxgateway.Dial` with `mxgateway.Options` to configure plaintext or TLS
|
|
||||||
transport, API-key metadata, dial timeout, and per-call timeout:
|
|
||||||
|
|
||||||
```go
|
|
||||||
client, err := mxgateway.Dial(ctx, mxgateway.Options{
|
|
||||||
Endpoint: "localhost:5000",
|
|
||||||
APIKey: os.Getenv("MXGATEWAY_API_KEY"),
|
|
||||||
Plaintext: true,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
`Client.OpenSession` returns a `Session` with helpers for `Register`,
|
|
||||||
`AddItem`, `AddItem2`, `Advise`, `Write`, `Events`, and `Close`. Raw protobuf
|
|
||||||
messages remain available through the `mxgateway` package aliases and the
|
|
||||||
`Raw` helper methods. Typed errors support `errors.As` for `GatewayError`,
|
|
||||||
`CommandError`, and `MxAccessError`; command errors preserve the raw reply.
|
|
||||||
|
|
||||||
## CLI
|
|
||||||
|
|
||||||
The `mxgw-go` CLI emits JSON with redacted API keys for commands that connect to
|
|
||||||
the gateway:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
go run ./cmd/mxgw-go version -json
|
|
||||||
go run ./cmd/mxgw-go open-session -endpoint localhost:5000 -plaintext -json
|
|
||||||
go run ./cmd/mxgw-go register -session-id <id> -client-name mxgw-go -plaintext -json
|
|
||||||
go run ./cmd/mxgw-go add-item -session-id <id> -server-handle 1 -item Area001.Tag.Value -plaintext -json
|
|
||||||
go run ./cmd/mxgw-go advise -session-id <id> -server-handle 1 -item-handle 1 -plaintext -json
|
|
||||||
go run ./cmd/mxgw-go write -session-id <id> -server-handle 1 -item-handle 1 -type int32 -value 123 -plaintext -json
|
|
||||||
go run ./cmd/mxgw-go stream-events -session-id <id> -plaintext -json
|
|
||||||
go run ./cmd/mxgw-go smoke -item Area001.Tag.Value -plaintext -json
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `-api-key-env MXGATEWAY_API_KEY` or `-api-key <key>` when authentication is
|
|
||||||
enabled. CLI output redacts the key value and never writes the raw secret.
|
|
||||||
@@ -1,530 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/mxgateway"
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
|
||||||
)
|
|
||||||
|
|
||||||
type versionOutput struct {
|
|
||||||
ClientVersion string `json:"clientVersion"`
|
|
||||||
GatewayProtocolVersion uint32 `json:"gatewayProtocolVersion"`
|
|
||||||
WorkerProtocolVersion uint32 `json:"workerProtocolVersion"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type commonOptions struct {
|
|
||||||
Endpoint string `json:"endpoint"`
|
|
||||||
APIKey string `json:"apiKey"`
|
|
||||||
APIKeyEnv string `json:"apiKeyEnv,omitempty"`
|
|
||||||
Plaintext bool `json:"plaintext"`
|
|
||||||
CACertFile string `json:"caCertFile,omitempty"`
|
|
||||||
ServerName string `json:"serverNameOverride,omitempty"`
|
|
||||||
CallTimeout string `json:"callTimeout,omitempty"`
|
|
||||||
|
|
||||||
apiKeyValue string
|
|
||||||
timeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type openSessionOutput struct {
|
|
||||||
Command string `json:"command"`
|
|
||||||
Options commonOptions `json:"options"`
|
|
||||||
Reply json.RawMessage `json:"reply"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type commandReplyOutput struct {
|
|
||||||
Command string `json:"command"`
|
|
||||||
Options commonOptions `json:"options"`
|
|
||||||
Reply json.RawMessage `json:"reply"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if err := runWithIO(context.Background(), os.Args[1:], os.Stdout, os.Stderr); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func run(args []string) error {
|
|
||||||
return runWithIO(context.Background(), args, os.Stdout, os.Stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWithIO(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
if len(args) == 0 {
|
|
||||||
writeUsage(stderr)
|
|
||||||
return errors.New("missing command")
|
|
||||||
}
|
|
||||||
|
|
||||||
switch args[0] {
|
|
||||||
case "version":
|
|
||||||
return runVersion(args[1:], stdout, stderr)
|
|
||||||
case "open-session":
|
|
||||||
return runOpenSession(ctx, args[1:], stdout, stderr)
|
|
||||||
case "close-session":
|
|
||||||
return runCloseSession(ctx, args[1:], stdout, stderr)
|
|
||||||
case "register":
|
|
||||||
return runRegister(ctx, args[1:], stdout, stderr)
|
|
||||||
case "add-item":
|
|
||||||
return runAddItem(ctx, args[1:], stdout, stderr)
|
|
||||||
case "advise":
|
|
||||||
return runAdvise(ctx, args[1:], stdout, stderr)
|
|
||||||
case "write":
|
|
||||||
return runWrite(ctx, args[1:], stdout, stderr)
|
|
||||||
case "stream-events":
|
|
||||||
return runStreamEvents(ctx, args[1:], stdout, stderr)
|
|
||||||
case "smoke":
|
|
||||||
return runSmoke(ctx, args[1:], stdout, stderr)
|
|
||||||
default:
|
|
||||||
writeUsage(stderr)
|
|
||||||
return fmt.Errorf("unknown command %q", args[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runVersion(args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("version", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
output := versionOutput{
|
|
||||||
ClientVersion: mxgateway.ClientVersion,
|
|
||||||
GatewayProtocolVersion: mxgateway.GatewayProtocolVersion,
|
|
||||||
WorkerProtocolVersion: mxgateway.WorkerProtocolVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
if *jsonOutput {
|
|
||||||
return writeJSON(stdout, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(stdout, "mxgw-go %s\n", output.ClientVersion)
|
|
||||||
fmt.Fprintf(stdout, "gateway protocol %d\n", output.GatewayProtocolVersion)
|
|
||||||
fmt.Fprintf(stdout, "worker protocol %d\n", output.WorkerProtocolVersion)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runOpenSession(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("open-session", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
clientName := flags.String("client-session-name", "", "client session name")
|
|
||||||
backend := flags.String("backend", "", "requested backend")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
reply, err := client.OpenSessionRaw(ctx, (&mxgateway.OpenSessionOptions{
|
|
||||||
RequestedBackend: *backend,
|
|
||||||
ClientSessionName: *clientName,
|
|
||||||
}).Request())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if *jsonOutput {
|
|
||||||
return writeJSON(stdout, openSessionOutput{
|
|
||||||
Command: "open-session",
|
|
||||||
Options: options,
|
|
||||||
Reply: mustMarshalProto(reply),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(stdout, reply.GetSessionId())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runCloseSession(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("close-session", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
sessionID := flags.String("session-id", "", "gateway session id")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *sessionID == "" {
|
|
||||||
return errors.New("session-id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
reply, err := client.CloseSessionRaw(ctx, &mxgateway.CloseSessionRequest{SessionId: *sessionID})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *jsonOutput {
|
|
||||||
return writeJSON(stdout, commandReplyOutput{
|
|
||||||
Command: "close-session",
|
|
||||||
Options: options,
|
|
||||||
Reply: mustMarshalProto(reply),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(stdout, reply.GetFinalState())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runRegister(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("register", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
sessionID := flags.String("session-id", "", "gateway session id")
|
|
||||||
clientName := flags.String("client-name", "", "MXAccess client name")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *sessionID == "" || *clientName == "" {
|
|
||||||
return errors.New("session-id and client-name are required")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
|
||||||
reply, err := session.RegisterRaw(ctx, *clientName)
|
|
||||||
return writeCommandOutput(stdout, *jsonOutput, "register", options, reply, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runAddItem(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("add-item", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
sessionID := flags.String("session-id", "", "gateway session id")
|
|
||||||
serverHandle := flags.Int("server-handle", 0, "MXAccess server handle")
|
|
||||||
item := flags.String("item", "", "item definition")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *sessionID == "" || *item == "" {
|
|
||||||
return errors.New("session-id and item are required")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
|
||||||
reply, err := session.AddItemRaw(ctx, int32(*serverHandle), *item)
|
|
||||||
return writeCommandOutput(stdout, *jsonOutput, "add-item", options, reply, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runAdvise(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("advise", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
sessionID := flags.String("session-id", "", "gateway session id")
|
|
||||||
serverHandle := flags.Int("server-handle", 0, "MXAccess server handle")
|
|
||||||
itemHandle := flags.Int("item-handle", 0, "MXAccess item handle")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *sessionID == "" {
|
|
||||||
return errors.New("session-id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
|
||||||
reply, err := session.AdviseRaw(ctx, int32(*serverHandle), int32(*itemHandle))
|
|
||||||
return writeCommandOutput(stdout, *jsonOutput, "advise", options, reply, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runWrite(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("write", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
sessionID := flags.String("session-id", "", "gateway session id")
|
|
||||||
serverHandle := flags.Int("server-handle", 0, "MXAccess server handle")
|
|
||||||
itemHandle := flags.Int("item-handle", 0, "MXAccess item handle")
|
|
||||||
valueType := flags.String("type", "string", "value type: bool, int32, int64, float, double, string")
|
|
||||||
valueText := flags.String("value", "", "value text")
|
|
||||||
userID := flags.Int("user-id", 0, "MXAccess user id")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *sessionID == "" {
|
|
||||||
return errors.New("session-id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := parseValue(*valueType, *valueText)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
|
||||||
reply, err := session.WriteRaw(ctx, int32(*serverHandle), int32(*itemHandle), value, int32(*userID))
|
|
||||||
return writeCommandOutput(stdout, *jsonOutput, "write", options, reply, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runStreamEvents(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("stream-events", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
sessionID := flags.String("session-id", "", "gateway session id")
|
|
||||||
after := flags.Uint64("after-worker-sequence", 0, "first worker sequence to read after")
|
|
||||||
limit := flags.Int("limit", 0, "maximum events to read; 0 means unbounded")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *sessionID == "" {
|
|
||||||
return errors.New("session-id is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, _, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
session := mxgateway.NewSessionForID(client, *sessionID)
|
|
||||||
streamCtx, cancelStream := context.WithCancel(ctx)
|
|
||||||
defer cancelStream()
|
|
||||||
events, err := session.EventsAfter(streamCtx, *after)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
count := 0
|
|
||||||
for result := range events {
|
|
||||||
if result.Err != nil {
|
|
||||||
return result.Err
|
|
||||||
}
|
|
||||||
if *jsonOutput {
|
|
||||||
fmt.Fprintln(stdout, string(mustMarshalProto(result.Event)))
|
|
||||||
} else {
|
|
||||||
fmt.Fprintf(stdout, "%d %s\n", result.Event.GetWorkerSequence(), result.Event.GetFamily())
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
if *limit > 0 && count >= *limit {
|
|
||||||
cancelStream()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSmoke(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
|
||||||
flags := flag.NewFlagSet("smoke", flag.ContinueOnError)
|
|
||||||
flags.SetOutput(stderr)
|
|
||||||
common := bindCommonFlags(flags)
|
|
||||||
jsonOutput := flags.Bool("json", false, "write JSON output")
|
|
||||||
clientName := flags.String("client-name", "mxgw-go-smoke", "MXAccess client name")
|
|
||||||
item := flags.String("item", "", "item definition")
|
|
||||||
|
|
||||||
if err := flags.Parse(args); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if *item == "" {
|
|
||||||
return errors.New("item is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, options, err := dialForCommand(ctx, common)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
session, err := client.OpenSession(ctx, mxgateway.OpenSessionOptions{ClientSessionName: *clientName})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer session.Close(context.Background())
|
|
||||||
|
|
||||||
serverHandle, err := session.Register(ctx, *clientName)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
itemHandle, err := session.AddItem(ctx, serverHandle, *item)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := session.Advise(ctx, serverHandle, itemHandle); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
output := map[string]any{
|
|
||||||
"command": "smoke",
|
|
||||||
"options": options,
|
|
||||||
"sessionId": session.ID(),
|
|
||||||
"serverHandle": serverHandle,
|
|
||||||
"itemHandle": itemHandle,
|
|
||||||
}
|
|
||||||
if *jsonOutput {
|
|
||||||
return writeJSON(stdout, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(stdout, "session=%s server=%d item=%d\n", session.ID(), serverHandle, itemHandle)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func bindCommonFlags(flags *flag.FlagSet) *commonOptions {
|
|
||||||
common := &commonOptions{}
|
|
||||||
flags.StringVar(&common.Endpoint, "endpoint", "localhost:5000", "gateway endpoint")
|
|
||||||
flags.StringVar(&common.APIKey, "api-key", "", "gateway API key")
|
|
||||||
flags.StringVar(&common.APIKeyEnv, "api-key-env", "MXGATEWAY_API_KEY", "environment variable containing the API key")
|
|
||||||
flags.BoolVar(&common.Plaintext, "plaintext", false, "use plaintext transport")
|
|
||||||
flags.StringVar(&common.CACertFile, "ca-cert", "", "CA certificate file")
|
|
||||||
flags.StringVar(&common.ServerName, "server-name-override", "", "TLS server name override")
|
|
||||||
flags.StringVar(&common.CallTimeout, "call-timeout", "30s", "per-call timeout")
|
|
||||||
return common
|
|
||||||
}
|
|
||||||
|
|
||||||
func dialForCommand(ctx context.Context, common *commonOptions) (*mxgateway.Client, commonOptions, error) {
|
|
||||||
options, err := common.resolved()
|
|
||||||
if err != nil {
|
|
||||||
return nil, options, err
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := mxgateway.Dial(ctx, mxgateway.Options{
|
|
||||||
Endpoint: options.Endpoint,
|
|
||||||
APIKey: options.apiKeyValue,
|
|
||||||
Plaintext: options.Plaintext,
|
|
||||||
CACertFile: options.CACertFile,
|
|
||||||
ServerNameOverride: options.ServerName,
|
|
||||||
CallTimeout: options.timeout,
|
|
||||||
})
|
|
||||||
return client, options, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *commonOptions) resolved() (commonOptions, error) {
|
|
||||||
resolved := *o
|
|
||||||
if resolved.APIKey == "" && resolved.APIKeyEnv != "" {
|
|
||||||
resolved.apiKeyValue = os.Getenv(resolved.APIKeyEnv)
|
|
||||||
} else {
|
|
||||||
resolved.apiKeyValue = resolved.APIKey
|
|
||||||
}
|
|
||||||
resolved.APIKey = mxgateway.RedactAPIKey(resolved.apiKeyValue)
|
|
||||||
if resolved.CallTimeout != "" {
|
|
||||||
timeout, err := time.ParseDuration(resolved.CallTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return resolved, err
|
|
||||||
}
|
|
||||||
resolved.timeout = timeout
|
|
||||||
}
|
|
||||||
return resolved, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseValue(valueType, valueText string) (*mxgateway.MxValue, error) {
|
|
||||||
switch valueType {
|
|
||||||
case "bool":
|
|
||||||
value, err := strconv.ParseBool(valueText)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return mxgateway.BoolValue(value), nil
|
|
||||||
case "int32":
|
|
||||||
value, err := strconv.ParseInt(valueText, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return mxgateway.Int32Value(int32(value)), nil
|
|
||||||
case "int64":
|
|
||||||
value, err := strconv.ParseInt(valueText, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return mxgateway.Int64Value(value), nil
|
|
||||||
case "float":
|
|
||||||
value, err := strconv.ParseFloat(valueText, 32)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return mxgateway.FloatValue(float32(value)), nil
|
|
||||||
case "double":
|
|
||||||
value, err := strconv.ParseFloat(valueText, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return mxgateway.DoubleValue(value), nil
|
|
||||||
case "string":
|
|
||||||
return mxgateway.StringValue(valueText), nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported value type %q", valueType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeCommandOutput(stdout io.Writer, jsonOutput bool, command string, options commonOptions, reply *mxgateway.MxCommandReply, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if jsonOutput {
|
|
||||||
return writeJSON(stdout, commandReplyOutput{
|
|
||||||
Command: command,
|
|
||||||
Options: options,
|
|
||||||
Reply: mustMarshalProto(reply),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
fmt.Fprintln(stdout, reply.GetKind())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeJSON(writer io.Writer, value any) error {
|
|
||||||
encoder := json.NewEncoder(writer)
|
|
||||||
encoder.SetIndent("", " ")
|
|
||||||
return encoder.Encode(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustMarshalProto(message protojsonMessage) json.RawMessage {
|
|
||||||
data, err := protojson.MarshalOptions{UseProtoNames: false}.Marshal(message)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
type protojsonMessage interface {
|
|
||||||
ProtoReflect() protoreflect.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeUsage(writer io.Writer) {
|
|
||||||
fmt.Fprintln(writer, "usage: mxgw-go <version|open-session|close-session|register|add-item|advise|write|stream-events|smoke>")
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRunVersionJSON(t *testing.T) {
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
|
||||||
|
|
||||||
if err := runWithIO(t.Context(), []string{"version", "-json"}, &stdout, &stderr); err != nil {
|
|
||||||
t.Fatalf("runWithIO() error = %v; stderr = %s", err, stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
var output versionOutput
|
|
||||||
if err := json.Unmarshal(stdout.Bytes(), &output); err != nil {
|
|
||||||
t.Fatalf("parse JSON: %v", err)
|
|
||||||
}
|
|
||||||
if output.GatewayProtocolVersion == 0 || output.WorkerProtocolVersion == 0 {
|
|
||||||
t.Fatalf("protocol versions were not populated: %+v", output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCommonOptionsRedactsAPIKey(t *testing.T) {
|
|
||||||
options, err := (&commonOptions{
|
|
||||||
Endpoint: "localhost:5000",
|
|
||||||
APIKey: "mxgw_super_secret",
|
|
||||||
Plaintext: true,
|
|
||||||
CallTimeout: "2s",
|
|
||||||
}).resolved()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolved() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(options)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal options: %v", err)
|
|
||||||
}
|
|
||||||
if strings.Contains(string(data), "super_secret") {
|
|
||||||
t.Fatalf("redacted JSON leaked API key: %s", data)
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(data), "mxgw") {
|
|
||||||
t.Fatalf("redacted JSON did not preserve key shape: %s", data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseValueBuildsTypedValue(t *testing.T) {
|
|
||||||
value, err := parseValue("int32", "123")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("parseValue() error = %v", err)
|
|
||||||
}
|
|
||||||
if got := value.GetInt32Value(); got != 123 {
|
|
||||||
t.Fatalf("int32 value = %d, want 123", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
Set-StrictMode -Version Latest
|
|
||||||
$ErrorActionPreference = 'Stop'
|
|
||||||
|
|
||||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
|
||||||
$protoRoot = Join-Path $repoRoot 'src\MxGateway.Contracts\Protos'
|
|
||||||
$outputRoot = Join-Path $PSScriptRoot 'internal\generated'
|
|
||||||
$modulePath = 'gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated'
|
|
||||||
$protoc = 'C:\Users\dohertj2\AppData\Local\Microsoft\WinGet\Packages\Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe\bin\protoc.exe'
|
|
||||||
$goPluginPath = 'C:\Users\dohertj2\go\bin'
|
|
||||||
|
|
||||||
if (-not (Test-Path $protoc)) {
|
|
||||||
throw "protoc was not found at $protoc. See docs/toolchain-links.md."
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($pluginName in @('protoc-gen-go.exe', 'protoc-gen-go-grpc.exe')) {
|
|
||||||
$pluginPath = Join-Path $goPluginPath $pluginName
|
|
||||||
if (-not (Test-Path $pluginPath)) {
|
|
||||||
throw "$pluginName was not found at $pluginPath. See docs/toolchain-links.md."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
New-Item -ItemType Directory -Path $outputRoot -Force | Out-Null
|
|
||||||
Get-ChildItem -Path $outputRoot -Filter '*.pb.go' -File | Remove-Item
|
|
||||||
|
|
||||||
$env:Path = "$goPluginPath;$env:Path"
|
|
||||||
|
|
||||||
& $protoc `
|
|
||||||
--proto_path=$protoRoot `
|
|
||||||
--go_out=$outputRoot `
|
|
||||||
--go_opt=paths=source_relative `
|
|
||||||
"--go_opt=Mmxaccess_gateway.proto=$modulePath;generated" `
|
|
||||||
"--go_opt=Mmxaccess_worker.proto=$modulePath;generated" `
|
|
||||||
mxaccess_gateway.proto `
|
|
||||||
mxaccess_worker.proto
|
|
||||||
|
|
||||||
& $protoc `
|
|
||||||
--proto_path=$protoRoot `
|
|
||||||
--go-grpc_out=$outputRoot `
|
|
||||||
--go-grpc_opt=paths=source_relative `
|
|
||||||
"--go-grpc_opt=Mmxaccess_gateway.proto=$modulePath;generated" `
|
|
||||||
mxaccess_gateway.proto
|
|
||||||
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
module gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go
|
|
||||||
|
|
||||||
go 1.26
|
|
||||||
|
|
||||||
require (
|
|
||||||
google.golang.org/grpc v1.80.0
|
|
||||||
google.golang.org/protobuf v1.36.11
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
golang.org/x/net v0.49.0 // indirect
|
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
|
||||||
golang.org/x/text v0.33.0 // indirect
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
|
||||||
)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
|
||||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
|
||||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
|
||||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
|
||||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
|
||||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
|
||||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
|
||||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
|
||||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
|
||||||
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
|
|
||||||
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
|
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,243 +0,0 @@
|
|||||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
|
||||||
// versions:
|
|
||||||
// - protoc-gen-go-grpc v1.6.1
|
|
||||||
// - protoc v7.34.1
|
|
||||||
// source: mxaccess_gateway.proto
|
|
||||||
|
|
||||||
package generated
|
|
||||||
|
|
||||||
import (
|
|
||||||
context "context"
|
|
||||||
grpc "google.golang.org/grpc"
|
|
||||||
codes "google.golang.org/grpc/codes"
|
|
||||||
status "google.golang.org/grpc/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
// This is a compile-time assertion to ensure that this generated file
|
|
||||||
// is compatible with the grpc package it is being compiled against.
|
|
||||||
// Requires gRPC-Go v1.64.0 or later.
|
|
||||||
const _ = grpc.SupportPackageIsVersion9
|
|
||||||
|
|
||||||
const (
|
|
||||||
MxAccessGateway_OpenSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/OpenSession"
|
|
||||||
MxAccessGateway_CloseSession_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/CloseSession"
|
|
||||||
MxAccessGateway_Invoke_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/Invoke"
|
|
||||||
MxAccessGateway_StreamEvents_FullMethodName = "/mxaccess_gateway.v1.MxAccessGateway/StreamEvents"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MxAccessGatewayClient is the client API for MxAccessGateway service.
|
|
||||||
//
|
|
||||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
|
||||||
//
|
|
||||||
// Public client API for MXAccess sessions hosted by the gateway.
|
|
||||||
type MxAccessGatewayClient interface {
|
|
||||||
OpenSession(ctx context.Context, in *OpenSessionRequest, opts ...grpc.CallOption) (*OpenSessionReply, error)
|
|
||||||
CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionReply, error)
|
|
||||||
Invoke(ctx context.Context, in *MxCommandRequest, opts ...grpc.CallOption) (*MxCommandReply, error)
|
|
||||||
StreamEvents(ctx context.Context, in *StreamEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MxEvent], error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type mxAccessGatewayClient struct {
|
|
||||||
cc grpc.ClientConnInterface
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMxAccessGatewayClient(cc grpc.ClientConnInterface) MxAccessGatewayClient {
|
|
||||||
return &mxAccessGatewayClient{cc}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mxAccessGatewayClient) OpenSession(ctx context.Context, in *OpenSessionRequest, opts ...grpc.CallOption) (*OpenSessionReply, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(OpenSessionReply)
|
|
||||||
err := c.cc.Invoke(ctx, MxAccessGateway_OpenSession_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mxAccessGatewayClient) CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*CloseSessionReply, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(CloseSessionReply)
|
|
||||||
err := c.cc.Invoke(ctx, MxAccessGateway_CloseSession_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mxAccessGatewayClient) Invoke(ctx context.Context, in *MxCommandRequest, opts ...grpc.CallOption) (*MxCommandReply, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(MxCommandReply)
|
|
||||||
err := c.cc.Invoke(ctx, MxAccessGateway_Invoke_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *mxAccessGatewayClient) StreamEvents(ctx context.Context, in *StreamEventsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[MxEvent], error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
stream, err := c.cc.NewStream(ctx, &MxAccessGateway_ServiceDesc.Streams[0], MxAccessGateway_StreamEvents_FullMethodName, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
x := &grpc.GenericClientStream[StreamEventsRequest, MxEvent]{ClientStream: stream}
|
|
||||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := x.ClientStream.CloseSend(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return x, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type MxAccessGateway_StreamEventsClient = grpc.ServerStreamingClient[MxEvent]
|
|
||||||
|
|
||||||
// MxAccessGatewayServer is the server API for MxAccessGateway service.
|
|
||||||
// All implementations must embed UnimplementedMxAccessGatewayServer
|
|
||||||
// for forward compatibility.
|
|
||||||
//
|
|
||||||
// Public client API for MXAccess sessions hosted by the gateway.
|
|
||||||
type MxAccessGatewayServer interface {
|
|
||||||
OpenSession(context.Context, *OpenSessionRequest) (*OpenSessionReply, error)
|
|
||||||
CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionReply, error)
|
|
||||||
Invoke(context.Context, *MxCommandRequest) (*MxCommandReply, error)
|
|
||||||
StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error
|
|
||||||
mustEmbedUnimplementedMxAccessGatewayServer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnimplementedMxAccessGatewayServer must be embedded to have
|
|
||||||
// forward compatible implementations.
|
|
||||||
//
|
|
||||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
|
||||||
// pointer dereference when methods are called.
|
|
||||||
type UnimplementedMxAccessGatewayServer struct{}
|
|
||||||
|
|
||||||
func (UnimplementedMxAccessGatewayServer) OpenSession(context.Context, *OpenSessionRequest) (*OpenSessionReply, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method OpenSession not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedMxAccessGatewayServer) CloseSession(context.Context, *CloseSessionRequest) (*CloseSessionReply, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method CloseSession not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedMxAccessGatewayServer) Invoke(context.Context, *MxCommandRequest) (*MxCommandReply, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method Invoke not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedMxAccessGatewayServer) StreamEvents(*StreamEventsRequest, grpc.ServerStreamingServer[MxEvent]) error {
|
|
||||||
return status.Error(codes.Unimplemented, "method StreamEvents not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedMxAccessGatewayServer) mustEmbedUnimplementedMxAccessGatewayServer() {}
|
|
||||||
func (UnimplementedMxAccessGatewayServer) testEmbeddedByValue() {}
|
|
||||||
|
|
||||||
// UnsafeMxAccessGatewayServer may be embedded to opt out of forward compatibility for this service.
|
|
||||||
// Use of this interface is not recommended, as added methods to MxAccessGatewayServer will
|
|
||||||
// result in compilation errors.
|
|
||||||
type UnsafeMxAccessGatewayServer interface {
|
|
||||||
mustEmbedUnimplementedMxAccessGatewayServer()
|
|
||||||
}
|
|
||||||
|
|
||||||
func RegisterMxAccessGatewayServer(s grpc.ServiceRegistrar, srv MxAccessGatewayServer) {
|
|
||||||
// If the following call panics, it indicates UnimplementedMxAccessGatewayServer was
|
|
||||||
// embedded by pointer and is nil. This will cause panics if an
|
|
||||||
// unimplemented method is ever invoked, so we test this at initialization
|
|
||||||
// time to prevent it from happening at runtime later due to I/O.
|
|
||||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
|
||||||
t.testEmbeddedByValue()
|
|
||||||
}
|
|
||||||
s.RegisterService(&MxAccessGateway_ServiceDesc, srv)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _MxAccessGateway_OpenSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(OpenSessionRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(MxAccessGatewayServer).OpenSession(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: MxAccessGateway_OpenSession_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(MxAccessGatewayServer).OpenSession(ctx, req.(*OpenSessionRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _MxAccessGateway_CloseSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(CloseSessionRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(MxAccessGatewayServer).CloseSession(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: MxAccessGateway_CloseSession_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(MxAccessGatewayServer).CloseSession(ctx, req.(*CloseSessionRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _MxAccessGateway_Invoke_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(MxCommandRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(MxAccessGatewayServer).Invoke(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: MxAccessGateway_Invoke_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(MxAccessGatewayServer).Invoke(ctx, req.(*MxCommandRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _MxAccessGateway_StreamEvents_Handler(srv interface{}, stream grpc.ServerStream) error {
|
|
||||||
m := new(StreamEventsRequest)
|
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return srv.(MxAccessGatewayServer).StreamEvents(m, &grpc.GenericServerStream[StreamEventsRequest, MxEvent]{ServerStream: stream})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type MxAccessGateway_StreamEventsServer = grpc.ServerStreamingServer[MxEvent]
|
|
||||||
|
|
||||||
// MxAccessGateway_ServiceDesc is the grpc.ServiceDesc for MxAccessGateway service.
|
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
|
||||||
// and not to be introspected or modified (even as a copy)
|
|
||||||
var MxAccessGateway_ServiceDesc = grpc.ServiceDesc{
|
|
||||||
ServiceName: "mxaccess_gateway.v1.MxAccessGateway",
|
|
||||||
HandlerType: (*MxAccessGatewayServer)(nil),
|
|
||||||
Methods: []grpc.MethodDesc{
|
|
||||||
{
|
|
||||||
MethodName: "OpenSession",
|
|
||||||
Handler: _MxAccessGateway_OpenSession_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "CloseSession",
|
|
||||||
Handler: _MxAccessGateway_CloseSession_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "Invoke",
|
|
||||||
Handler: _MxAccessGateway_Invoke_Handler,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Streams: []grpc.StreamDesc{
|
|
||||||
{
|
|
||||||
StreamName: "StreamEvents",
|
|
||||||
Handler: _MxAccessGateway_StreamEvents_Handler,
|
|
||||||
ServerStreams: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Metadata: "mxaccess_gateway.proto",
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
const authorizationHeader = "authorization"
|
|
||||||
|
|
||||||
func unaryAuthInterceptor(apiKey string) grpc.UnaryClientInterceptor {
|
|
||||||
return func(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
|
||||||
return invoker(authContext(ctx, apiKey), method, req, reply, cc, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func streamAuthInterceptor(apiKey string) grpc.StreamClientInterceptor {
|
|
||||||
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
|
||||||
return streamer(authContext(ctx, apiKey), desc, cc, method, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func authContext(ctx context.Context, apiKey string) context.Context {
|
|
||||||
if apiKey == "" {
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata.AppendToOutgoingContext(ctx, authorizationHeader, "Bearer "+apiKey)
|
|
||||||
}
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials"
|
|
||||||
"google.golang.org/grpc/credentials/insecure"
|
|
||||||
"google.golang.org/protobuf/types/known/durationpb"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultDialTimeout = 10 * time.Second
|
|
||||||
defaultCallTimeout = 30 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
// Client owns a gateway gRPC connection and exposes session-oriented helpers.
|
|
||||||
type Client struct {
|
|
||||||
conn *grpc.ClientConn
|
|
||||||
raw pb.MxAccessGatewayClient
|
|
||||||
opts Options
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dial opens a gRPC connection to the gateway and configures auth metadata,
|
|
||||||
// transport security, and blocking dial cancellation from ctx.
|
|
||||||
func Dial(ctx context.Context, opts Options) (*Client, error) {
|
|
||||||
if opts.Endpoint == "" {
|
|
||||||
return nil, errors.New("mxgateway: endpoint is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
dialCtx := ctx
|
|
||||||
cancel := func() {}
|
|
||||||
if opts.DialTimeout > 0 {
|
|
||||||
dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
|
|
||||||
} else if _, ok := ctx.Deadline(); !ok {
|
|
||||||
dialCtx, cancel = context.WithTimeout(ctx, defaultDialTimeout)
|
|
||||||
}
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
transportCredentials, err := resolveTransportCredentials(opts)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
dialOptions := []grpc.DialOption{
|
|
||||||
grpc.WithTransportCredentials(transportCredentials),
|
|
||||||
grpc.WithUnaryInterceptor(unaryAuthInterceptor(opts.APIKey)),
|
|
||||||
grpc.WithStreamInterceptor(streamAuthInterceptor(opts.APIKey)),
|
|
||||||
grpc.WithBlock(),
|
|
||||||
}
|
|
||||||
dialOptions = append(dialOptions, opts.DialOptions...)
|
|
||||||
|
|
||||||
conn, err := grpc.DialContext(dialCtx, opts.Endpoint, dialOptions...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &GatewayError{Op: "dial", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewClient(conn, opts), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient wraps an existing gRPC connection. The caller owns closing conn
|
|
||||||
// unless it calls Close on the returned Client.
|
|
||||||
func NewClient(conn *grpc.ClientConn, opts Options) *Client {
|
|
||||||
return &Client{
|
|
||||||
conn: conn,
|
|
||||||
raw: pb.NewMxAccessGatewayClient(conn),
|
|
||||||
opts: opts,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RawClient returns the generated gRPC client for command-specific parity tests.
|
|
||||||
func (c *Client) RawClient() RawGatewayClient {
|
|
||||||
return c.raw
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenSession creates a gateway-backed MXAccess session.
|
|
||||||
func (c *Client) OpenSession(ctx context.Context, opts OpenSessionOptions) (*Session, error) {
|
|
||||||
reply, err := c.OpenSessionRaw(ctx, opts.Request())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSession(c, reply), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenSessionRaw sends a raw OpenSession request and validates protocol status.
|
|
||||||
func (c *Client) OpenSessionRaw(ctx context.Context, req *OpenSessionRequest) (*OpenSessionReply, error) {
|
|
||||||
if req == nil {
|
|
||||||
return nil, errors.New("mxgateway: open session request is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
callCtx, cancel := c.callContext(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
reply, err := c.raw.OpenSession(callCtx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &GatewayError{Op: "open session", Err: err}
|
|
||||||
}
|
|
||||||
if err := EnsureProtocolSuccess("open session", reply.GetProtocolStatus(), nil); err != nil {
|
|
||||||
return reply, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invoke sends a raw MXAccess command request and validates protocol and
|
|
||||||
// MXAccess status fields while preserving the raw reply on typed errors.
|
|
||||||
func (c *Client) Invoke(ctx context.Context, req *MxCommandRequest) (*MxCommandReply, error) {
|
|
||||||
if req == nil {
|
|
||||||
return nil, errors.New("mxgateway: command request is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
callCtx, cancel := c.callContext(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
reply, err := c.raw.Invoke(callCtx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &GatewayError{Op: "invoke", Err: err}
|
|
||||||
}
|
|
||||||
if err := EnsureProtocolSuccess("invoke", reply.GetProtocolStatus(), reply); err != nil {
|
|
||||||
return reply, err
|
|
||||||
}
|
|
||||||
if err := EnsureMxAccessSuccess("invoke", reply); err != nil {
|
|
||||||
return reply, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseSessionRaw sends a raw CloseSession request and validates protocol
|
|
||||||
// status.
|
|
||||||
func (c *Client) CloseSessionRaw(ctx context.Context, req *CloseSessionRequest) (*CloseSessionReply, error) {
|
|
||||||
if req == nil {
|
|
||||||
return nil, errors.New("mxgateway: close session request is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
callCtx, cancel := c.callContext(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
reply, err := c.raw.CloseSession(callCtx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &GatewayError{Op: "close session", Err: err}
|
|
||||||
}
|
|
||||||
if err := EnsureProtocolSuccess("close session", reply.GetProtocolStatus(), nil); err != nil {
|
|
||||||
return reply, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// StreamEventsRaw starts the generated event stream for callers that need direct
|
|
||||||
// control over Recv.
|
|
||||||
func (c *Client) StreamEventsRaw(ctx context.Context, req *StreamEventsRequest) (RawEventStream, error) {
|
|
||||||
if req == nil {
|
|
||||||
return nil, errors.New("mxgateway: stream events request is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
stream, err := c.raw.StreamEvents(ctx, req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &GatewayError{Op: "stream events", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
return stream, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the underlying gRPC connection.
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
if c == nil || c.conn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) callContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
|
||||||
timeout := c.opts.CallTimeout
|
|
||||||
if timeout == 0 {
|
|
||||||
timeout = defaultCallTimeout
|
|
||||||
}
|
|
||||||
if timeout < 0 {
|
|
||||||
return ctx, func() {}
|
|
||||||
}
|
|
||||||
if _, ok := ctx.Deadline(); ok {
|
|
||||||
return ctx, func() {}
|
|
||||||
}
|
|
||||||
return context.WithTimeout(ctx, timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveTransportCredentials(opts Options) (credentials.TransportCredentials, error) {
|
|
||||||
if opts.TransportCredentials != nil {
|
|
||||||
return opts.TransportCredentials, nil
|
|
||||||
}
|
|
||||||
if opts.Plaintext {
|
|
||||||
return insecure.NewCredentials(), nil
|
|
||||||
}
|
|
||||||
if opts.CACertFile != "" {
|
|
||||||
return credentials.NewClientTLSFromFile(opts.CACertFile, opts.ServerNameOverride)
|
|
||||||
}
|
|
||||||
if opts.TLSConfig != nil {
|
|
||||||
cfg := opts.TLSConfig.Clone()
|
|
||||||
if opts.ServerNameOverride != "" {
|
|
||||||
cfg.ServerName = opts.ServerNameOverride
|
|
||||||
}
|
|
||||||
return credentials.NewTLS(cfg), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return credentials.NewTLS(&tls.Config{
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
ServerName: opts.ServerNameOverride,
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenSessionOptions describes fields used to create an OpenSessionRequest.
|
|
||||||
type OpenSessionOptions struct {
|
|
||||||
RequestedBackend string
|
|
||||||
ClientSessionName string
|
|
||||||
ClientCorrelationID string
|
|
||||||
CommandTimeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request returns the raw protobuf OpenSessionRequest for these options.
|
|
||||||
func (o OpenSessionOptions) Request() *OpenSessionRequest {
|
|
||||||
req := &OpenSessionRequest{
|
|
||||||
RequestedBackend: o.RequestedBackend,
|
|
||||||
ClientSessionName: o.ClientSessionName,
|
|
||||||
ClientCorrelationId: o.ClientCorrelationID,
|
|
||||||
}
|
|
||||||
if o.CommandTimeout > 0 {
|
|
||||||
req.CommandTimeout = durationpb.New(o.CommandTimeout)
|
|
||||||
}
|
|
||||||
return req
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
"google.golang.org/grpc/test/bufconn"
|
|
||||||
)
|
|
||||||
|
|
||||||
const bufSize = 1024 * 1024
|
|
||||||
|
|
||||||
func TestDialAttachesAuthMetadataToUnaryCalls(t *testing.T) {
|
|
||||||
fake := &fakeGatewayServer{
|
|
||||||
openReply: &pb.OpenSessionReply{
|
|
||||||
SessionId: "session-1",
|
|
||||||
GatewayProtocolVersion: GatewayProtocolVersion,
|
|
||||||
WorkerProtocolVersion: WorkerProtocolVersion,
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{
|
|
||||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
_, err := client.OpenSession(context.Background(), OpenSessionOptions{ClientSessionName: "fixture"})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("OpenSession() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := fake.openAuth; got != "Bearer test-api-key" {
|
|
||||||
t.Fatalf("authorization metadata = %q, want %q", got, "Bearer test-api-key")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStreamEventsAttachesAuthMetadataAndClosesOnCancellation(t *testing.T) {
|
|
||||||
fake := &fakeGatewayServer{
|
|
||||||
streamStarted: make(chan struct{}),
|
|
||||||
}
|
|
||||||
client, cleanup := newBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
session := NewSessionForID(client, "session-1")
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
|
|
||||||
events, err := session.Events(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Events() error = %v", err)
|
|
||||||
}
|
|
||||||
<-fake.streamStarted
|
|
||||||
|
|
||||||
first := <-events
|
|
||||||
if first.Err != nil {
|
|
||||||
t.Fatalf("first event error = %v", first.Err)
|
|
||||||
}
|
|
||||||
if first.Event.GetWorkerSequence() != 1 {
|
|
||||||
t.Fatalf("worker sequence = %d, want 1", first.Event.GetWorkerSequence())
|
|
||||||
}
|
|
||||||
if got := fake.streamAuth; got != "Bearer test-api-key" {
|
|
||||||
t.Fatalf("stream authorization metadata = %q, want %q", got, "Bearer test-api-key")
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
select {
|
|
||||||
case _, ok := <-events:
|
|
||||||
if ok {
|
|
||||||
t.Fatal("events channel produced an extra item after cancellation")
|
|
||||||
}
|
|
||||||
case <-time.After(2 * time.Second):
|
|
||||||
t.Fatal("events channel did not close after cancellation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSessionHelpersBuildCommandsAndExposeRawReply(t *testing.T) {
|
|
||||||
fake := &fakeGatewayServer{
|
|
||||||
invokeReply: &pb.MxCommandReply{
|
|
||||||
SessionId: "session-1",
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2,
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{
|
|
||||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
|
||||||
},
|
|
||||||
Payload: &pb.MxCommandReply_AddItem2{
|
|
||||||
AddItem2: &pb.AddItem2Reply{ItemHandle: 42},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
session := NewSessionForID(client, "session-1")
|
|
||||||
|
|
||||||
itemHandle, err := session.AddItem2(context.Background(), 12, "Area001.Pump001.Speed", "runtime")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("AddItem2() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if itemHandle != 42 {
|
|
||||||
t.Fatalf("item handle = %d, want 42", itemHandle)
|
|
||||||
}
|
|
||||||
req := fake.invokeRequest
|
|
||||||
if req.GetSessionId() != "session-1" {
|
|
||||||
t.Fatalf("session id = %q, want session-1", req.GetSessionId())
|
|
||||||
}
|
|
||||||
if req.GetClientCorrelationId() == "" {
|
|
||||||
t.Fatal("client correlation id is empty")
|
|
||||||
}
|
|
||||||
if req.GetCommand().GetKind() != pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2 {
|
|
||||||
t.Fatalf("command kind = %s", req.GetCommand().GetKind())
|
|
||||||
}
|
|
||||||
if req.GetCommand().GetAddItem2().GetItemContext() != "runtime" {
|
|
||||||
t.Fatalf("item context = %q, want runtime", req.GetCommand().GetAddItem2().GetItemContext())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvokeReturnsTypedMxAccessErrorWithRawReply(t *testing.T) {
|
|
||||||
hresult := int32(-2147467259)
|
|
||||||
fake := &fakeGatewayServer{
|
|
||||||
invokeReply: &pb.MxCommandReply{
|
|
||||||
SessionId: "session-1",
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADVISE,
|
|
||||||
Hresult: &hresult,
|
|
||||||
DiagnosticMessage: "native failure",
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE, Message: "MXAccess failed"},
|
|
||||||
Statuses: []*pb.MxStatusProxy{{Success: 0, DiagnosticText: "failed"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client, cleanup := newBufconnClient(t, fake)
|
|
||||||
defer cleanup()
|
|
||||||
session := NewSessionForID(client, "session-1")
|
|
||||||
|
|
||||||
err := session.Advise(context.Background(), 12, 34)
|
|
||||||
|
|
||||||
var mxErr *MxAccessError
|
|
||||||
if !errors.As(err, &mxErr) {
|
|
||||||
t.Fatalf("error %T does not support errors.As(*MxAccessError)", err)
|
|
||||||
}
|
|
||||||
if mxErr.Reply.GetHresult() != hresult {
|
|
||||||
t.Fatalf("raw reply HRESULT = %d, want %d", mxErr.Reply.GetHresult(), hresult)
|
|
||||||
}
|
|
||||||
var commandErr *CommandError
|
|
||||||
if !errors.As(err, &commandErr) {
|
|
||||||
t.Fatalf("error %T does not support errors.As(*CommandError)", err)
|
|
||||||
}
|
|
||||||
if commandErr.Reply.GetDiagnosticMessage() != "native failure" {
|
|
||||||
t.Fatalf("raw diagnostic = %q", commandErr.Reply.GetDiagnosticMessage())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBufconnClient(t *testing.T, fake *fakeGatewayServer) (*Client, func()) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
listener := bufconn.Listen(bufSize)
|
|
||||||
server := grpc.NewServer()
|
|
||||||
pb.RegisterMxAccessGatewayServer(server, fake)
|
|
||||||
go func() {
|
|
||||||
if err := server.Serve(listener); err != nil && !errors.Is(err, grpc.ErrServerStopped) {
|
|
||||||
t.Errorf("bufconn server failed: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
dialer := func(ctx context.Context, _ string) (net.Conn, error) {
|
|
||||||
return listener.DialContext(ctx)
|
|
||||||
}
|
|
||||||
client, err := Dial(context.Background(), Options{
|
|
||||||
Endpoint: "bufnet",
|
|
||||||
APIKey: "test-api-key",
|
|
||||||
Plaintext: true,
|
|
||||||
DialOptions: []grpc.DialOption{
|
|
||||||
grpc.WithContextDialer(dialer),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Dial() error = %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, func() {
|
|
||||||
client.Close()
|
|
||||||
server.Stop()
|
|
||||||
listener.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeGatewayServer struct {
|
|
||||||
pb.UnimplementedMxAccessGatewayServer
|
|
||||||
|
|
||||||
openReply *pb.OpenSessionReply
|
|
||||||
openAuth string
|
|
||||||
streamAuth string
|
|
||||||
streamStarted chan struct{}
|
|
||||||
invokeReply *pb.MxCommandReply
|
|
||||||
invokeRequest *pb.MxCommandRequest
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fakeGatewayServer) OpenSession(ctx context.Context, req *pb.OpenSessionRequest) (*pb.OpenSessionReply, error) {
|
|
||||||
s.openAuth = authorizationFromContext(ctx)
|
|
||||||
if s.openReply != nil {
|
|
||||||
return s.openReply, nil
|
|
||||||
}
|
|
||||||
return &pb.OpenSessionReply{
|
|
||||||
SessionId: "session-1",
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{
|
|
||||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fakeGatewayServer) CloseSession(ctx context.Context, req *pb.CloseSessionRequest) (*pb.CloseSessionReply, error) {
|
|
||||||
return &pb.CloseSessionReply{
|
|
||||||
SessionId: req.GetSessionId(),
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{
|
|
||||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fakeGatewayServer) Invoke(ctx context.Context, req *pb.MxCommandRequest) (*pb.MxCommandReply, error) {
|
|
||||||
s.invokeRequest = req
|
|
||||||
if s.invokeReply != nil {
|
|
||||||
return s.invokeReply, nil
|
|
||||||
}
|
|
||||||
return &pb.MxCommandReply{
|
|
||||||
SessionId: req.GetSessionId(),
|
|
||||||
Kind: req.GetCommand().GetKind(),
|
|
||||||
ProtocolStatus: &pb.ProtocolStatus{
|
|
||||||
Code: pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *fakeGatewayServer) StreamEvents(req *pb.StreamEventsRequest, stream grpc.ServerStreamingServer[pb.MxEvent]) error {
|
|
||||||
s.streamAuth = authorizationFromContext(stream.Context())
|
|
||||||
if s.streamStarted != nil {
|
|
||||||
close(s.streamStarted)
|
|
||||||
}
|
|
||||||
if err := stream.Send(&pb.MxEvent{
|
|
||||||
SessionId: req.GetSessionId(),
|
|
||||||
Family: pb.MxEventFamily_MX_EVENT_FAMILY_ON_DATA_CHANGE,
|
|
||||||
WorkerSequence: 1,
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
<-stream.Context().Done()
|
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
func authorizationFromContext(ctx context.Context) string {
|
|
||||||
md, ok := metadata.FromIncomingContext(ctx)
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
values := md.Get(authorizationHeader)
|
|
||||||
if len(values) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return values[0]
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValueConversionFixtures(t *testing.T) {
|
|
||||||
data, err := os.ReadFile(filepath.Join("..", "..", "proto", "fixtures", "behavior", "values", "value-conversion-cases.json"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read fixture: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var fixture struct {
|
|
||||||
Cases []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
ExpectedKind string `json:"expectedKind"`
|
|
||||||
Value json.RawMessage `json:"value"`
|
|
||||||
} `json:"cases"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &fixture); err != nil {
|
|
||||||
t.Fatalf("parse fixture manifest: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range fixture.Cases {
|
|
||||||
t.Run(tc.ID, func(t *testing.T) {
|
|
||||||
var value pb.MxValue
|
|
||||||
if err := protojson.Unmarshal(tc.Value, &value); err != nil {
|
|
||||||
t.Fatalf("parse value: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := NativeValue(&value); err != nil {
|
|
||||||
t.Fatalf("NativeValue() error = %v", err)
|
|
||||||
}
|
|
||||||
if got := value.ProtoReflect().WhichOneof(value.ProtoReflect().Descriptor().Oneofs().ByName("kind")).JSONName(); got != tc.ExpectedKind {
|
|
||||||
t.Fatalf("kind = %q, want %q", got, tc.ExpectedKind)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatusConversionFixtures(t *testing.T) {
|
|
||||||
data, err := os.ReadFile(filepath.Join("..", "..", "proto", "fixtures", "behavior", "statuses", "status-conversion-cases.json"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read fixture: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var fixture struct {
|
|
||||||
Cases []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Status json.RawMessage `json:"status"`
|
|
||||||
} `json:"cases"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &fixture); err != nil {
|
|
||||||
t.Fatalf("parse fixture manifest: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range fixture.Cases {
|
|
||||||
t.Run(tc.ID, func(t *testing.T) {
|
|
||||||
var status pb.MxStatusProxy
|
|
||||||
if err := protojson.Unmarshal(tc.Status, &status); err != nil {
|
|
||||||
t.Fatalf("parse status: %v", err)
|
|
||||||
}
|
|
||||||
if got, want := StatusSucceeded(&status), status.GetSuccess() != 0; got != want {
|
|
||||||
t.Fatalf("StatusSucceeded() = %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GatewayError wraps transport-level gRPC failures.
|
|
||||||
type GatewayError struct {
|
|
||||||
Op string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *GatewayError) Error() string {
|
|
||||||
if e == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if e.Op == "" {
|
|
||||||
return fmt.Sprintf("mxgateway: %v", e.Err)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("mxgateway: %s failed: %v", e.Op, e.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *GatewayError) Unwrap() error {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommandError reports a non-OK gateway protocol status and keeps the raw
|
|
||||||
// command reply when one exists.
|
|
||||||
type CommandError struct {
|
|
||||||
Op string
|
|
||||||
Status *ProtocolStatus
|
|
||||||
Reply *MxCommandReply
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *CommandError) Error() string {
|
|
||||||
if e == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
status := e.Status
|
|
||||||
if status == nil {
|
|
||||||
return fmt.Sprintf("mxgateway: %s failed with missing protocol status", e.Op)
|
|
||||||
}
|
|
||||||
if status.GetMessage() == "" {
|
|
||||||
return fmt.Sprintf("mxgateway: %s failed with protocol status %s", e.Op, status.GetCode())
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("mxgateway: %s failed with protocol status %s: %s", e.Op, status.GetCode(), status.GetMessage())
|
|
||||||
}
|
|
||||||
|
|
||||||
// MxAccessError reports HRESULT or MXSTATUS_PROXY failures returned by MXAccess.
|
|
||||||
type MxAccessError struct {
|
|
||||||
Command *CommandError
|
|
||||||
Reply *MxCommandReply
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *MxAccessError) Error() string {
|
|
||||||
if e == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if e.Command != nil && e.Command.Status != nil && e.Command.Status.GetMessage() != "" {
|
|
||||||
return e.Command.Error()
|
|
||||||
}
|
|
||||||
if e.Reply != nil && e.Reply.GetDiagnosticMessage() != "" {
|
|
||||||
return fmt.Sprintf("mxgateway: MXAccess command %s failed: %s", e.Reply.GetKind(), e.Reply.GetDiagnosticMessage())
|
|
||||||
}
|
|
||||||
if e.Reply != nil && e.Reply.Hresult != nil {
|
|
||||||
return fmt.Sprintf("mxgateway: MXAccess command %s failed with HRESULT 0x%08X", e.Reply.GetKind(), uint32(e.Reply.GetHresult()))
|
|
||||||
}
|
|
||||||
return "mxgateway: MXAccess command failed"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *MxAccessError) Unwrap() error {
|
|
||||||
if e == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return e.Command
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnsureProtocolSuccess returns a typed CommandError when status is non-OK.
|
|
||||||
func EnsureProtocolSuccess(op string, status *ProtocolStatus, reply *MxCommandReply) error {
|
|
||||||
if status == nil || status.GetCode() == pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
commandError := &CommandError{
|
|
||||||
Op: op,
|
|
||||||
Status: status,
|
|
||||||
Reply: reply,
|
|
||||||
}
|
|
||||||
if status.GetCode() == pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE {
|
|
||||||
return &MxAccessError{
|
|
||||||
Command: commandError,
|
|
||||||
Reply: reply,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return commandError
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnsureMxAccessSuccess returns a typed MxAccessError for failing HRESULTs or
|
|
||||||
// MXSTATUS_PROXY entries.
|
|
||||||
func EnsureMxAccessSuccess(op string, reply *MxCommandReply) error {
|
|
||||||
if reply == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if reply.Hresult != nil && reply.GetHresult() != 0 {
|
|
||||||
return &MxAccessError{Reply: reply}
|
|
||||||
}
|
|
||||||
for _, status := range reply.GetStatuses() {
|
|
||||||
if !StatusSucceeded(status) {
|
|
||||||
return &MxAccessError{Reply: reply}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Options configures gateway connections.
|
|
||||||
type Options struct {
|
|
||||||
Endpoint string
|
|
||||||
APIKey string
|
|
||||||
Plaintext bool
|
|
||||||
CACertFile string
|
|
||||||
ServerNameOverride string
|
|
||||||
DialTimeout time.Duration
|
|
||||||
CallTimeout time.Duration
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
TransportCredentials credentials.TransportCredentials
|
|
||||||
DialOptions []grpc.DialOption
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedactedAPIKey returns a display-safe representation of the configured API
|
|
||||||
// key for diagnostics and CLI output.
|
|
||||||
func (o Options) RedactedAPIKey() string {
|
|
||||||
return RedactAPIKey(o.APIKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedactAPIKey hides credential material while keeping enough shape for
|
|
||||||
// troubleshooting whether a key was supplied.
|
|
||||||
func RedactAPIKey(apiKey string) string {
|
|
||||||
if apiKey == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(apiKey) <= 8 {
|
|
||||||
return "<redacted>"
|
|
||||||
}
|
|
||||||
|
|
||||||
prefix, suffix := apiKey[:4], apiKey[len(apiKey)-4:]
|
|
||||||
return prefix + strings.Repeat("*", len(apiKey)-8) + suffix
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestRedactAPIKey(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
apiKey string
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{name: "empty", apiKey: "", want: ""},
|
|
||||||
{name: "short", apiKey: "mxgw_1", want: "<redacted>"},
|
|
||||||
{name: "long", apiKey: "mxgw_key_secret", want: "mxgw*******cret"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := RedactAPIKey(tt.apiKey); got != tt.want {
|
|
||||||
t.Fatalf("RedactAPIKey() = %q, want %q", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGeneratedGoldenFixturesParse(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
msg proto.Message
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "open session reply",
|
|
||||||
path: filepath.Join("..", "..", "proto", "fixtures", "golden", "open-session-reply.ok.json"),
|
|
||||||
msg: &pb.OpenSessionReply{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "register command request",
|
|
||||||
path: filepath.Join("..", "..", "proto", "fixtures", "golden", "register-command-request.json"),
|
|
||||||
msg: &pb.MxCommandRequest{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "on data change event",
|
|
||||||
path: filepath.Join("..", "..", "proto", "fixtures", "golden", "on-data-change-event.json"),
|
|
||||||
msg: &pb.MxEvent{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
unmarshal := protojson.UnmarshalOptions{DiscardUnknown: false}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
data, err := os.ReadFile(tt.path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read fixture: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := unmarshal.Unmarshal(data, tt.msg); err != nil {
|
|
||||||
t.Fatalf("parse fixture: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOpenSessionFixtureProtocolVersions(t *testing.T) {
|
|
||||||
data, err := os.ReadFile(filepath.Join("..", "..", "proto", "fixtures", "golden", "open-session-reply.ok.json"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read fixture: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var reply pb.OpenSessionReply
|
|
||||||
if err := protojson.Unmarshal(data, &reply); err != nil {
|
|
||||||
t.Fatalf("parse fixture: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if reply.GetGatewayProtocolVersion() != GatewayProtocolVersion {
|
|
||||||
t.Fatalf("gateway protocol = %d, want %d", reply.GetGatewayProtocolVersion(), GatewayProtocolVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
if reply.GetWorkerProtocolVersion() != WorkerProtocolVersion {
|
|
||||||
t.Fatalf("worker protocol = %d, want %d", reply.GetWorkerProtocolVersion(), WorkerProtocolVersion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
)
|
|
||||||
|
|
||||||
// EventResult carries either the next ordered event or a terminal stream error.
|
|
||||||
type EventResult struct {
|
|
||||||
Event *MxEvent
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Session represents one gateway-backed MXAccess session.
|
|
||||||
type Session struct {
|
|
||||||
client *Client
|
|
||||||
openReply *OpenSessionReply
|
|
||||||
closeMu sync.Mutex
|
|
||||||
closeReply *CloseSessionReply
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSession(client *Client, openReply *OpenSessionReply) *Session {
|
|
||||||
return &Session{
|
|
||||||
client: client,
|
|
||||||
openReply: openReply,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSessionForID creates a session wrapper for commands against an existing
|
|
||||||
// gateway session id.
|
|
||||||
func NewSessionForID(client *Client, sessionID string) *Session {
|
|
||||||
return newSession(client, &pb.OpenSessionReply{SessionId: sessionID})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns the gateway session identifier.
|
|
||||||
func (s *Session) ID() string {
|
|
||||||
return s.openReply.GetSessionId()
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenReply returns the raw OpenSession reply.
|
|
||||||
func (s *Session) OpenReply() *OpenSessionReply {
|
|
||||||
return s.openReply
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the gateway session once and returns the raw close reply.
|
|
||||||
func (s *Session) Close(ctx context.Context) (*CloseSessionReply, error) {
|
|
||||||
s.closeMu.Lock()
|
|
||||||
defer s.closeMu.Unlock()
|
|
||||||
|
|
||||||
if s.closeReply != nil {
|
|
||||||
return s.closeReply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reply, err := s.client.CloseSessionRaw(ctx, &pb.CloseSessionRequest{SessionId: s.ID()})
|
|
||||||
if err != nil {
|
|
||||||
return reply, err
|
|
||||||
}
|
|
||||||
s.closeReply = reply
|
|
||||||
return reply, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register invokes MXAccess Register and returns the server handle.
|
|
||||||
func (s *Session) Register(ctx context.Context, clientName string) (int32, error) {
|
|
||||||
reply, err := s.RegisterRaw(ctx, clientName)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if reply.GetRegister() != nil {
|
|
||||||
return reply.GetRegister().GetServerHandle(), nil
|
|
||||||
}
|
|
||||||
return reply.GetReturnValue().GetInt32Value(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterRaw invokes MXAccess Register and returns the raw reply.
|
|
||||||
func (s *Session) RegisterRaw(ctx context.Context, clientName string) (*MxCommandReply, error) {
|
|
||||||
if clientName == "" {
|
|
||||||
return nil, errors.New("mxgateway: client name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_REGISTER,
|
|
||||||
Payload: &pb.MxCommand_Register{
|
|
||||||
Register: &pb.RegisterCommand{ClientName: clientName},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unregister invokes MXAccess Unregister.
|
|
||||||
func (s *Session) Unregister(ctx context.Context, serverHandle int32) error {
|
|
||||||
_, err := s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER,
|
|
||||||
Payload: &pb.MxCommand_Unregister{
|
|
||||||
Unregister: &pb.UnregisterCommand{ServerHandle: serverHandle},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddItem invokes MXAccess AddItem and returns the item handle.
|
|
||||||
func (s *Session) AddItem(ctx context.Context, serverHandle int32, itemDefinition string) (int32, error) {
|
|
||||||
reply, err := s.AddItemRaw(ctx, serverHandle, itemDefinition)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if reply.GetAddItem() != nil {
|
|
||||||
return reply.GetAddItem().GetItemHandle(), nil
|
|
||||||
}
|
|
||||||
return reply.GetReturnValue().GetInt32Value(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddItemRaw invokes MXAccess AddItem and returns the raw reply.
|
|
||||||
func (s *Session) AddItemRaw(ctx context.Context, serverHandle int32, itemDefinition string) (*MxCommandReply, error) {
|
|
||||||
if itemDefinition == "" {
|
|
||||||
return nil, errors.New("mxgateway: item definition is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM,
|
|
||||||
Payload: &pb.MxCommand_AddItem{
|
|
||||||
AddItem: &pb.AddItemCommand{
|
|
||||||
ServerHandle: serverHandle,
|
|
||||||
ItemDefinition: itemDefinition,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddItem2 invokes MXAccess AddItem2 and returns the item handle.
|
|
||||||
func (s *Session) AddItem2(ctx context.Context, serverHandle int32, itemDefinition, itemContext string) (int32, error) {
|
|
||||||
reply, err := s.AddItem2Raw(ctx, serverHandle, itemDefinition, itemContext)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if reply.GetAddItem2() != nil {
|
|
||||||
return reply.GetAddItem2().GetItemHandle(), nil
|
|
||||||
}
|
|
||||||
return reply.GetReturnValue().GetInt32Value(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddItem2Raw invokes MXAccess AddItem2 and returns the raw reply.
|
|
||||||
func (s *Session) AddItem2Raw(ctx context.Context, serverHandle int32, itemDefinition, itemContext string) (*MxCommandReply, error) {
|
|
||||||
if itemDefinition == "" {
|
|
||||||
return nil, errors.New("mxgateway: item definition is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2,
|
|
||||||
Payload: &pb.MxCommand_AddItem2{
|
|
||||||
AddItem2: &pb.AddItem2Command{
|
|
||||||
ServerHandle: serverHandle,
|
|
||||||
ItemDefinition: itemDefinition,
|
|
||||||
ItemContext: itemContext,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advise invokes MXAccess Advise.
|
|
||||||
func (s *Session) Advise(ctx context.Context, serverHandle, itemHandle int32) error {
|
|
||||||
_, err := s.AdviseRaw(ctx, serverHandle, itemHandle)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AdviseRaw invokes MXAccess Advise and returns the raw reply.
|
|
||||||
func (s *Session) AdviseRaw(ctx context.Context, serverHandle, itemHandle int32) (*MxCommandReply, error) {
|
|
||||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_ADVISE,
|
|
||||||
Payload: &pb.MxCommand_Advise{
|
|
||||||
Advise: &pb.AdviseCommand{
|
|
||||||
ServerHandle: serverHandle,
|
|
||||||
ItemHandle: itemHandle,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write invokes MXAccess Write.
|
|
||||||
func (s *Session) Write(ctx context.Context, serverHandle, itemHandle int32, value *MxValue, userID int32) error {
|
|
||||||
_, err := s.WriteRaw(ctx, serverHandle, itemHandle, value, userID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteRaw invokes MXAccess Write and returns the raw reply.
|
|
||||||
func (s *Session) WriteRaw(ctx context.Context, serverHandle, itemHandle int32, value *MxValue, userID int32) (*MxCommandReply, error) {
|
|
||||||
if value == nil {
|
|
||||||
return nil, errors.New("mxgateway: write value is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.invokeCommand(ctx, &pb.MxCommand{
|
|
||||||
Kind: pb.MxCommandKind_MX_COMMAND_KIND_WRITE,
|
|
||||||
Payload: &pb.MxCommand_Write{
|
|
||||||
Write: &pb.WriteCommand{
|
|
||||||
ServerHandle: serverHandle,
|
|
||||||
ItemHandle: itemHandle,
|
|
||||||
Value: value,
|
|
||||||
UserId: userID,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Events streams ordered session events until the server ends the stream,
|
|
||||||
// context cancellation stops Recv, or a terminal error is sent.
|
|
||||||
func (s *Session) Events(ctx context.Context) (<-chan EventResult, error) {
|
|
||||||
return s.EventsAfter(ctx, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventsAfter streams ordered session events after the given worker sequence.
|
|
||||||
func (s *Session) EventsAfter(ctx context.Context, afterWorkerSequence uint64) (<-chan EventResult, error) {
|
|
||||||
stream, err := s.client.StreamEventsRaw(ctx, &pb.StreamEventsRequest{
|
|
||||||
SessionId: s.ID(),
|
|
||||||
AfterWorkerSequence: afterWorkerSequence,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
results := make(chan EventResult, 16)
|
|
||||||
go func() {
|
|
||||||
defer close(results)
|
|
||||||
for {
|
|
||||||
event, err := stream.Recv()
|
|
||||||
if err == nil {
|
|
||||||
results <- EventResult{Event: event}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err == io.EOF || status.Code(err) == codes.Canceled || ctx.Err() != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
results <- EventResult{Err: &GatewayError{Op: "stream events", Err: err}}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return results, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Session) invokeCommand(ctx context.Context, command *MxCommand) (*MxCommandReply, error) {
|
|
||||||
return s.client.Invoke(ctx, &pb.MxCommandRequest{
|
|
||||||
SessionId: s.ID(),
|
|
||||||
ClientCorrelationId: newCorrelationID(),
|
|
||||||
Command: command,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func newCorrelationID() string {
|
|
||||||
var buffer [16]byte
|
|
||||||
if _, err := rand.Read(buffer[:]); err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(buffer[:])
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
// StatusSucceeded reports whether an MXSTATUS_PROXY entry represents success.
|
|
||||||
func StatusSucceeded(status *MxStatusProxy) bool {
|
|
||||||
return status == nil || status.GetSuccess() != 0
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
|
|
||||||
// RawGatewayClient is the generated gRPC client interface exposed for callers
|
|
||||||
// that need direct contract access.
|
|
||||||
type RawGatewayClient = pb.MxAccessGatewayClient
|
|
||||||
|
|
||||||
// RawEventStream is the generated StreamEvents client stream.
|
|
||||||
type RawEventStream = pb.MxAccessGateway_StreamEventsClient
|
|
||||||
|
|
||||||
// Generated protobuf aliases keep raw contract access available from the public
|
|
||||||
// mxgateway package while generated code remains under internal/generated.
|
|
||||||
type (
|
|
||||||
OpenSessionRequest = pb.OpenSessionRequest
|
|
||||||
OpenSessionReply = pb.OpenSessionReply
|
|
||||||
CloseSessionRequest = pb.CloseSessionRequest
|
|
||||||
CloseSessionReply = pb.CloseSessionReply
|
|
||||||
StreamEventsRequest = pb.StreamEventsRequest
|
|
||||||
MxCommandRequest = pb.MxCommandRequest
|
|
||||||
MxCommandReply = pb.MxCommandReply
|
|
||||||
MxCommand = pb.MxCommand
|
|
||||||
MxEvent = pb.MxEvent
|
|
||||||
MxValue = pb.MxValue
|
|
||||||
Value = pb.MxValue
|
|
||||||
MxArray = pb.MxArray
|
|
||||||
MxStatusProxy = pb.MxStatusProxy
|
|
||||||
ProtocolStatus = pb.ProtocolStatus
|
|
||||||
RegisterCommand = pb.RegisterCommand
|
|
||||||
UnregisterCommand = pb.UnregisterCommand
|
|
||||||
AddItemCommand = pb.AddItemCommand
|
|
||||||
AddItem2Command = pb.AddItem2Command
|
|
||||||
AdviseCommand = pb.AdviseCommand
|
|
||||||
WriteCommand = pb.WriteCommand
|
|
||||||
Write2Command = pb.Write2Command
|
|
||||||
RegisterReply = pb.RegisterReply
|
|
||||||
AddItemReply = pb.AddItemReply
|
|
||||||
AddItem2Reply = pb.AddItem2Reply
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
MxCommandKind = pb.MxCommandKind
|
|
||||||
MxDataType = pb.MxDataType
|
|
||||||
MxEventFamily = pb.MxEventFamily
|
|
||||||
MxStatusCategory = pb.MxStatusCategory
|
|
||||||
MxStatusSource = pb.MxStatusSource
|
|
||||||
ProtocolStatusCode = pb.ProtocolStatusCode
|
|
||||||
SessionState = pb.SessionState
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
CommandKindRegister = pb.MxCommandKind_MX_COMMAND_KIND_REGISTER
|
|
||||||
CommandKindUnregister = pb.MxCommandKind_MX_COMMAND_KIND_UNREGISTER
|
|
||||||
CommandKindAddItem = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM
|
|
||||||
CommandKindAddItem2 = pb.MxCommandKind_MX_COMMAND_KIND_ADD_ITEM2
|
|
||||||
CommandKindAdvise = pb.MxCommandKind_MX_COMMAND_KIND_ADVISE
|
|
||||||
CommandKindWrite = pb.MxCommandKind_MX_COMMAND_KIND_WRITE
|
|
||||||
CommandKindWrite2 = pb.MxCommandKind_MX_COMMAND_KIND_WRITE2
|
|
||||||
|
|
||||||
DataTypeUnknown = pb.MxDataType_MX_DATA_TYPE_UNKNOWN
|
|
||||||
DataTypeBoolean = pb.MxDataType_MX_DATA_TYPE_BOOLEAN
|
|
||||||
DataTypeInteger = pb.MxDataType_MX_DATA_TYPE_INTEGER
|
|
||||||
DataTypeFloat = pb.MxDataType_MX_DATA_TYPE_FLOAT
|
|
||||||
DataTypeDouble = pb.MxDataType_MX_DATA_TYPE_DOUBLE
|
|
||||||
DataTypeString = pb.MxDataType_MX_DATA_TYPE_STRING
|
|
||||||
DataTypeTime = pb.MxDataType_MX_DATA_TYPE_TIME
|
|
||||||
|
|
||||||
ProtocolStatusOK = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_OK
|
|
||||||
ProtocolStatusMxAccessFailure = pb.ProtocolStatusCode_PROTOCOL_STATUS_CODE_MXACCESS_FAILURE
|
|
||||||
)
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated"
|
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BoolValue builds an MXAccess Boolean value.
|
|
||||||
func BoolValue(value bool) *MxValue {
|
|
||||||
return &pb.MxValue{
|
|
||||||
DataType: pb.MxDataType_MX_DATA_TYPE_BOOLEAN,
|
|
||||||
VariantType: "VT_BOOL",
|
|
||||||
Kind: &pb.MxValue_BoolValue{BoolValue: value},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int32Value builds an MXAccess Int32 value.
|
|
||||||
func Int32Value(value int32) *MxValue {
|
|
||||||
return &pb.MxValue{
|
|
||||||
DataType: pb.MxDataType_MX_DATA_TYPE_INTEGER,
|
|
||||||
VariantType: "VT_I4",
|
|
||||||
Kind: &pb.MxValue_Int32Value{Int32Value: value},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Int64Value builds an MXAccess Int64 value.
|
|
||||||
func Int64Value(value int64) *MxValue {
|
|
||||||
return &pb.MxValue{
|
|
||||||
DataType: pb.MxDataType_MX_DATA_TYPE_INTEGER,
|
|
||||||
VariantType: "VT_I8",
|
|
||||||
Kind: &pb.MxValue_Int64Value{Int64Value: value},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FloatValue builds an MXAccess Float value.
|
|
||||||
func FloatValue(value float32) *MxValue {
|
|
||||||
return &pb.MxValue{
|
|
||||||
DataType: pb.MxDataType_MX_DATA_TYPE_FLOAT,
|
|
||||||
VariantType: "VT_R4",
|
|
||||||
Kind: &pb.MxValue_FloatValue{FloatValue: value},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoubleValue builds an MXAccess Double value.
|
|
||||||
func DoubleValue(value float64) *MxValue {
|
|
||||||
return &pb.MxValue{
|
|
||||||
DataType: pb.MxDataType_MX_DATA_TYPE_DOUBLE,
|
|
||||||
VariantType: "VT_R8",
|
|
||||||
Kind: &pb.MxValue_DoubleValue{DoubleValue: value},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringValue builds an MXAccess String value.
|
|
||||||
func StringValue(value string) *MxValue {
|
|
||||||
return &pb.MxValue{
|
|
||||||
DataType: pb.MxDataType_MX_DATA_TYPE_STRING,
|
|
||||||
VariantType: "VT_BSTR",
|
|
||||||
Kind: &pb.MxValue_StringValue{StringValue: value},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TimestampValue builds an MXAccess timestamp value from a Go time.
|
|
||||||
func TimestampValue(value time.Time) *MxValue {
|
|
||||||
return &pb.MxValue{
|
|
||||||
DataType: pb.MxDataType_MX_DATA_TYPE_TIME,
|
|
||||||
VariantType: "VT_DATE",
|
|
||||||
Kind: &pb.MxValue_TimestampValue{TimestampValue: timestamppb.New(value)},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NativeValue converts a protobuf MxValue to the closest Go representation
|
|
||||||
// without discarding raw fallback data.
|
|
||||||
func NativeValue(value *MxValue) (any, error) {
|
|
||||||
if value == nil || value.GetIsNull() {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch kind := value.GetKind().(type) {
|
|
||||||
case *pb.MxValue_BoolValue:
|
|
||||||
return kind.BoolValue, nil
|
|
||||||
case *pb.MxValue_Int32Value:
|
|
||||||
return kind.Int32Value, nil
|
|
||||||
case *pb.MxValue_Int64Value:
|
|
||||||
return kind.Int64Value, nil
|
|
||||||
case *pb.MxValue_FloatValue:
|
|
||||||
return kind.FloatValue, nil
|
|
||||||
case *pb.MxValue_DoubleValue:
|
|
||||||
return kind.DoubleValue, nil
|
|
||||||
case *pb.MxValue_StringValue:
|
|
||||||
return kind.StringValue, nil
|
|
||||||
case *pb.MxValue_TimestampValue:
|
|
||||||
if kind.TimestampValue == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return kind.TimestampValue.AsTime(), nil
|
|
||||||
case *pb.MxValue_ArrayValue:
|
|
||||||
return NativeArray(kind.ArrayValue)
|
|
||||||
case *pb.MxValue_RawValue:
|
|
||||||
return append([]byte(nil), kind.RawValue...), nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("mxgateway: unsupported value kind %T", kind)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NativeArray converts a protobuf MxArray to the closest Go slice
|
|
||||||
// representation.
|
|
||||||
func NativeArray(array *MxArray) (any, error) {
|
|
||||||
if array == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch values := array.GetValues().(type) {
|
|
||||||
case *pb.MxArray_BoolValues:
|
|
||||||
return append([]bool(nil), values.BoolValues.GetValues()...), nil
|
|
||||||
case *pb.MxArray_Int32Values:
|
|
||||||
return append([]int32(nil), values.Int32Values.GetValues()...), nil
|
|
||||||
case *pb.MxArray_Int64Values:
|
|
||||||
return append([]int64(nil), values.Int64Values.GetValues()...), nil
|
|
||||||
case *pb.MxArray_FloatValues:
|
|
||||||
return append([]float32(nil), values.FloatValues.GetValues()...), nil
|
|
||||||
case *pb.MxArray_DoubleValues:
|
|
||||||
return append([]float64(nil), values.DoubleValues.GetValues()...), nil
|
|
||||||
case *pb.MxArray_StringValues:
|
|
||||||
return append([]string(nil), values.StringValues.GetValues()...), nil
|
|
||||||
case *pb.MxArray_TimestampValues:
|
|
||||||
result := make([]time.Time, 0, len(values.TimestampValues.GetValues()))
|
|
||||||
for _, value := range values.TimestampValues.GetValues() {
|
|
||||||
if value == nil {
|
|
||||||
result = append(result, time.Time{})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result = append(result, value.AsTime())
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
case *pb.MxArray_RawValues:
|
|
||||||
rawValues := values.RawValues.GetValues()
|
|
||||||
result := make([][]byte, 0, len(rawValues))
|
|
||||||
for _, value := range rawValues {
|
|
||||||
result = append(result, append([]byte(nil), value...))
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("mxgateway: unsupported array value kind %T", values)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package mxgateway
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ClientVersion identifies this Go client scaffold before package releases
|
|
||||||
// assign semantic versions.
|
|
||||||
ClientVersion = "0.1.0-dev"
|
|
||||||
|
|
||||||
// GatewayProtocolVersion matches GatewayContractInfo.GatewayProtocolVersion
|
|
||||||
// in the shared .NET contracts.
|
|
||||||
GatewayProtocolVersion uint32 = 1
|
|
||||||
|
|
||||||
// WorkerProtocolVersion matches GatewayContractInfo.WorkerProtocolVersion
|
|
||||||
// and is exposed for fake-worker and parity tests.
|
|
||||||
WorkerProtocolVersion uint32 = 1
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
Binary file not shown.
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"cases": [
|
|
||||||
{
|
|
||||||
"id": "missing-api-key",
|
|
||||||
"grpcStatusCode": "UNAUTHENTICATED",
|
|
||||||
"clientErrorCategory": "AuthenticationError",
|
|
||||||
"inputMetadata": {
|
|
||||||
"authorization": ""
|
|
||||||
},
|
|
||||||
"expectedRedactedOutput": "authentication failed: missing bearer token",
|
|
||||||
"retryableWithoutCredentialChange": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "invalid-api-key",
|
|
||||||
"grpcStatusCode": "UNAUTHENTICATED",
|
|
||||||
"clientErrorCategory": "AuthenticationError",
|
|
||||||
"inputMetadata": {
|
|
||||||
"authorization": "Bearer <redacted>"
|
|
||||||
},
|
|
||||||
"expectedRedactedOutput": "authentication failed: invalid API key <redacted>",
|
|
||||||
"retryableWithoutCredentialChange": false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "missing-write-scope",
|
|
||||||
"grpcStatusCode": "PERMISSION_DENIED",
|
|
||||||
"clientErrorCategory": "AuthorizationError",
|
|
||||||
"inputMetadata": {
|
|
||||||
"authorization": "Bearer <redacted>"
|
|
||||||
},
|
|
||||||
"requiredScope": "mxaccess.write",
|
|
||||||
"expectedRedactedOutput": "authorization failed: missing scope mxaccess.write",
|
|
||||||
"retryableWithoutCredentialChange": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"correlationId": "gateway-correlation-register-1",
|
|
||||||
"kind": "MX_COMMAND_KIND_REGISTER",
|
|
||||||
"protocolStatus": {
|
|
||||||
"code": "PROTOCOL_STATUS_CODE_OK",
|
|
||||||
"message": "Register completed."
|
|
||||||
},
|
|
||||||
"hresult": 0,
|
|
||||||
"returnValue": {
|
|
||||||
"dataType": "MX_DATA_TYPE_INTEGER",
|
|
||||||
"variantType": "VT_I4",
|
|
||||||
"int32Value": 12
|
|
||||||
},
|
|
||||||
"statuses": [
|
|
||||||
{
|
|
||||||
"success": 1,
|
|
||||||
"category": "MX_STATUS_CATEGORY_OK",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_RESPONDING_LMX",
|
|
||||||
"detail": 0,
|
|
||||||
"rawCategory": 0,
|
|
||||||
"rawDetectedBy": 0,
|
|
||||||
"diagnosticText": "OK"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"diagnosticMessage": "COM Register returned server handle 12.",
|
|
||||||
"register": {
|
|
||||||
"serverHandle": 12
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"correlationId": "gateway-correlation-write-1",
|
|
||||||
"kind": "MX_COMMAND_KIND_WRITE",
|
|
||||||
"protocolStatus": {
|
|
||||||
"code": "PROTOCOL_STATUS_CODE_MXACCESS_FAILURE",
|
|
||||||
"message": "MXAccess rejected the write."
|
|
||||||
},
|
|
||||||
"hresult": -2147220992,
|
|
||||||
"returnValue": {
|
|
||||||
"dataType": "MX_DATA_TYPE_NO_DATA",
|
|
||||||
"variantType": "VT_EMPTY",
|
|
||||||
"isNull": true,
|
|
||||||
"rawDiagnostic": "MXAccess returned no value for the failed write.",
|
|
||||||
"rawDataType": 2
|
|
||||||
},
|
|
||||||
"statuses": [
|
|
||||||
{
|
|
||||||
"success": 0,
|
|
||||||
"category": "MX_STATUS_CATEGORY_SECURITY_ERROR",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_RESPONDING_LMX",
|
|
||||||
"detail": 321,
|
|
||||||
"rawCategory": 8,
|
|
||||||
"rawDetectedBy": 3,
|
|
||||||
"diagnosticText": "Write denied by provider security."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"success": 0,
|
|
||||||
"category": "MX_STATUS_CATEGORY_OPERATIONAL_ERROR",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_RESPONDING_NMX",
|
|
||||||
"detail": 902,
|
|
||||||
"rawCategory": 7,
|
|
||||||
"rawDetectedBy": 5,
|
|
||||||
"diagnosticText": "Provider rejected the item state."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"diagnosticMessage": "Fixture preserves a data-bearing MXAccess failure reply with HRESULT and status array."
|
|
||||||
}
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"description": "Ordered event stream sample for one worker-backed session.",
|
|
||||||
"events": [
|
|
||||||
{
|
|
||||||
"family": "MX_EVENT_FAMILY_ON_DATA_CHANGE",
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"serverHandle": 12,
|
|
||||||
"itemHandle": 34,
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_INTEGER",
|
|
||||||
"variantType": "VT_I4",
|
|
||||||
"int32Value": 123
|
|
||||||
},
|
|
||||||
"quality": 192,
|
|
||||||
"sourceTimestamp": "2026-01-01T00:00:00Z",
|
|
||||||
"statuses": [
|
|
||||||
{
|
|
||||||
"success": 1,
|
|
||||||
"category": "MX_STATUS_CATEGORY_OK",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_RESPONDING_LMX",
|
|
||||||
"detail": 0,
|
|
||||||
"rawCategory": 0,
|
|
||||||
"rawDetectedBy": 0,
|
|
||||||
"diagnosticText": "OK"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"workerSequence": "1",
|
|
||||||
"workerTimestamp": "2026-01-01T00:00:00.010Z",
|
|
||||||
"gatewayReceiveTimestamp": "2026-01-01T00:00:00.015Z",
|
|
||||||
"onDataChange": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"family": "MX_EVENT_FAMILY_ON_WRITE_COMPLETE",
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"serverHandle": 12,
|
|
||||||
"itemHandle": 34,
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_DOUBLE",
|
|
||||||
"variantType": "VT_R8",
|
|
||||||
"doubleValue": 45.5
|
|
||||||
},
|
|
||||||
"quality": 192,
|
|
||||||
"sourceTimestamp": "2026-01-01T00:00:01Z",
|
|
||||||
"statuses": [
|
|
||||||
{
|
|
||||||
"success": 1,
|
|
||||||
"category": "MX_STATUS_CATEGORY_OK",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_RESPONDING_LMX",
|
|
||||||
"detail": 0,
|
|
||||||
"rawCategory": 0,
|
|
||||||
"rawDetectedBy": 0,
|
|
||||||
"diagnosticText": "Write complete."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"workerSequence": "2",
|
|
||||||
"workerTimestamp": "2026-01-01T00:00:01.010Z",
|
|
||||||
"gatewayReceiveTimestamp": "2026-01-01T00:00:01.015Z",
|
|
||||||
"hresult": 0,
|
|
||||||
"onWriteComplete": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"family": "MX_EVENT_FAMILY_OPERATION_COMPLETE",
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"serverHandle": 12,
|
|
||||||
"itemHandle": 34,
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_STRING",
|
|
||||||
"variantType": "VT_BSTR",
|
|
||||||
"stringValue": "operation-complete"
|
|
||||||
},
|
|
||||||
"quality": 192,
|
|
||||||
"sourceTimestamp": "2026-01-01T00:00:02Z",
|
|
||||||
"statuses": [
|
|
||||||
{
|
|
||||||
"success": 1,
|
|
||||||
"category": "MX_STATUS_CATEGORY_OK",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_RESPONDING_NMX",
|
|
||||||
"detail": 0,
|
|
||||||
"rawCategory": 0,
|
|
||||||
"rawDetectedBy": 0,
|
|
||||||
"diagnosticText": "Operation complete."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"workerSequence": "3",
|
|
||||||
"workerTimestamp": "2026-01-01T00:00:02.010Z",
|
|
||||||
"gatewayReceiveTimestamp": "2026-01-01T00:00:02.015Z",
|
|
||||||
"operationComplete": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"family": "MX_EVENT_FAMILY_ON_BUFFERED_DATA_CHANGE",
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"serverHandle": 12,
|
|
||||||
"itemHandle": 34,
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_FLOAT",
|
|
||||||
"arrayValue": {
|
|
||||||
"elementDataType": "MX_DATA_TYPE_FLOAT",
|
|
||||||
"variantType": "VT_ARRAY|VT_R4",
|
|
||||||
"dimensions": [
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"floatValues": {
|
|
||||||
"values": [
|
|
||||||
1.5,
|
|
||||||
2.5
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"quality": 192,
|
|
||||||
"sourceTimestamp": "2026-01-01T00:00:03Z",
|
|
||||||
"statuses": [
|
|
||||||
{
|
|
||||||
"success": 1,
|
|
||||||
"category": "MX_STATUS_CATEGORY_OK",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_RESPONDING_LMX",
|
|
||||||
"detail": 0,
|
|
||||||
"rawCategory": 0,
|
|
||||||
"rawDetectedBy": 0,
|
|
||||||
"diagnosticText": "Buffered data delivered."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"workerSequence": "4",
|
|
||||||
"workerTimestamp": "2026-01-01T00:00:03.010Z",
|
|
||||||
"gatewayReceiveTimestamp": "2026-01-01T00:00:03.015Z",
|
|
||||||
"onBufferedDataChange": {
|
|
||||||
"dataType": "MX_DATA_TYPE_FLOAT",
|
|
||||||
"qualityValues": {
|
|
||||||
"elementDataType": "MX_DATA_TYPE_INTEGER",
|
|
||||||
"variantType": "VT_ARRAY|VT_I4",
|
|
||||||
"dimensions": [
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"int32Values": {
|
|
||||||
"values": [
|
|
||||||
192,
|
|
||||||
192
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"timestampValues": {
|
|
||||||
"elementDataType": "MX_DATA_TYPE_TIME",
|
|
||||||
"variantType": "VT_ARRAY|VT_DATE",
|
|
||||||
"dimensions": [
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"timestampValues": {
|
|
||||||
"values": [
|
|
||||||
"2026-01-01T00:00:02Z",
|
|
||||||
"2026-01-01T00:00:03Z"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"rawDataType": 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"fixtureSet": "mxaccess-gateway-client-behavior",
|
|
||||||
"contractName": "mxaccess-gateway",
|
|
||||||
"gatewayProtocolVersion": 1,
|
|
||||||
"workerProtocolVersion": 1,
|
|
||||||
"protoInputManifest": "clients/proto/proto-inputs.json",
|
|
||||||
"fixtures": [
|
|
||||||
{
|
|
||||||
"id": "command-reply.register.ok",
|
|
||||||
"category": "command_replies",
|
|
||||||
"messageType": "mxaccess_gateway.v1.MxCommandReply",
|
|
||||||
"path": "command-replies/register.ok.reply.json",
|
|
||||||
"expectation": "Successful command replies preserve protocol status, HRESULT, return value, status arrays, and method-specific output."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "command-reply.write.mxaccess-failure",
|
|
||||||
"category": "command_replies",
|
|
||||||
"messageType": "mxaccess_gateway.v1.MxCommandReply",
|
|
||||||
"path": "command-replies/write.mxaccess-failure.reply.json",
|
|
||||||
"expectation": "MXAccess failures are data-bearing replies with HRESULT and status details, not transport failures."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "event-stream.session-ordered",
|
|
||||||
"category": "event_streams",
|
|
||||||
"messageType": "mxaccess_gateway.v1.MxEvent",
|
|
||||||
"path": "event-streams/session-event-stream.json",
|
|
||||||
"expectation": "Clients preserve per-session event order and event family bodies exactly as emitted."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "values.conversion-cases",
|
|
||||||
"category": "value_conversion",
|
|
||||||
"messageType": "mxaccess_gateway.v1.MxValue",
|
|
||||||
"path": "values/value-conversion-cases.json",
|
|
||||||
"expectation": "Clients expose typed projections and keep raw fallback metadata when conversion is incomplete."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "statuses.conversion-cases",
|
|
||||||
"category": "status_conversion",
|
|
||||||
"messageType": "mxaccess_gateway.v1.MxStatusProxy",
|
|
||||||
"path": "statuses/status-conversion-cases.json",
|
|
||||||
"expectation": "Clients preserve every MXSTATUS_PROXY field, including raw category/source values."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "auth.error-cases",
|
|
||||||
"category": "auth_errors",
|
|
||||||
"messageType": "client_behavior.v1.AuthErrorCase",
|
|
||||||
"path": "auth/auth-error-cases.json",
|
|
||||||
"expectation": "Clients map authentication and authorization failures distinctly and redact credentials."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "timeout-cancel.expected-behavior",
|
|
||||||
"category": "timeout_cancel",
|
|
||||||
"messageType": "client_behavior.v1.TimeoutCancelCase",
|
|
||||||
"path": "timeout-cancel/timeout-cancel-cases.json",
|
|
||||||
"expectation": "Client cancellation stops waiting locally but does not imply an in-flight MXAccess COM call was aborted."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"cases": [
|
|
||||||
{
|
|
||||||
"id": "ok.responding-lmx",
|
|
||||||
"status": {
|
|
||||||
"success": 1,
|
|
||||||
"category": "MX_STATUS_CATEGORY_OK",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_RESPONDING_LMX",
|
|
||||||
"detail": 0,
|
|
||||||
"rawCategory": 0,
|
|
||||||
"rawDetectedBy": 0,
|
|
||||||
"diagnosticText": "OK"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "security-error.requesting-lmx",
|
|
||||||
"status": {
|
|
||||||
"success": 0,
|
|
||||||
"category": "MX_STATUS_CATEGORY_SECURITY_ERROR",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_REQUESTING_LMX",
|
|
||||||
"detail": 401,
|
|
||||||
"rawCategory": 8,
|
|
||||||
"rawDetectedBy": 2,
|
|
||||||
"diagnosticText": "Requesting LMX denied the secured operation."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "raw-unknown-category",
|
|
||||||
"status": {
|
|
||||||
"success": 0,
|
|
||||||
"category": "MX_STATUS_CATEGORY_UNKNOWN",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_UNKNOWN",
|
|
||||||
"detail": 65535,
|
|
||||||
"rawCategory": 99,
|
|
||||||
"rawDetectedBy": 77,
|
|
||||||
"diagnosticText": "Unknown native MXSTATUS_PROXY fields are preserved."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"cases": [
|
|
||||||
{
|
|
||||||
"id": "unary-deadline-exceeded",
|
|
||||||
"operation": "Invoke",
|
|
||||||
"clientDeadline": "2s",
|
|
||||||
"grpcStatusCode": "DEADLINE_EXCEEDED",
|
|
||||||
"clientErrorCategory": "TimeoutError",
|
|
||||||
"gatewayWaitBehavior": "stops_waiting_for_reply",
|
|
||||||
"workerCommandBehavior": "continues_until_worker_reply_or_worker_fault",
|
|
||||||
"sessionExpectation": "session_state_is_unknown_until_follow_up_status_or_close",
|
|
||||||
"expectedClientAction": "issue GetSessionState or CloseSession before reusing handles"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "stream-cancel",
|
|
||||||
"operation": "StreamEvents",
|
|
||||||
"clientDeadline": "5s",
|
|
||||||
"grpcStatusCode": "CANCELLED",
|
|
||||||
"clientErrorCategory": "CancelledError",
|
|
||||||
"gatewayWaitBehavior": "stops_streaming_to_that_call",
|
|
||||||
"workerCommandBehavior": "does_not_cancel_worker_session",
|
|
||||||
"sessionExpectation": "session_remains_ready_if_worker_stays_healthy",
|
|
||||||
"expectedClientAction": "open a new StreamEvents call with the last observed worker sequence"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"cases": [
|
|
||||||
{
|
|
||||||
"id": "bool.true",
|
|
||||||
"expectedKind": "boolValue",
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_BOOLEAN",
|
|
||||||
"variantType": "VT_BOOL",
|
|
||||||
"boolValue": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "int64.large",
|
|
||||||
"expectedKind": "int64Value",
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_INTEGER",
|
|
||||||
"variantType": "VT_I8",
|
|
||||||
"int64Value": "9223372036854770000"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "timestamp.utc",
|
|
||||||
"expectedKind": "timestampValue",
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_TIME",
|
|
||||||
"variantType": "VT_DATE",
|
|
||||||
"timestampValue": "2026-01-01T00:00:04Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "string-array",
|
|
||||||
"expectedKind": "arrayValue",
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_STRING",
|
|
||||||
"arrayValue": {
|
|
||||||
"elementDataType": "MX_DATA_TYPE_STRING",
|
|
||||||
"variantType": "VT_ARRAY|VT_BSTR",
|
|
||||||
"dimensions": [
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"stringValues": {
|
|
||||||
"values": [
|
|
||||||
"alpha",
|
|
||||||
"beta"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "raw-fallback.variant",
|
|
||||||
"expectedKind": "rawValue",
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_UNKNOWN",
|
|
||||||
"variantType": "VT_RECORD",
|
|
||||||
"rawDiagnostic": "No lossless typed projection exists for this VARIANT.",
|
|
||||||
"rawDataType": 32767,
|
|
||||||
"rawValue": "AQIDBAU="
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "raw-array-fallback",
|
|
||||||
"expectedKind": "arrayValue",
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_UNKNOWN",
|
|
||||||
"arrayValue": {
|
|
||||||
"elementDataType": "MX_DATA_TYPE_UNKNOWN",
|
|
||||||
"variantType": "VT_ARRAY|VT_VARIANT",
|
|
||||||
"dimensions": [
|
|
||||||
2
|
|
||||||
],
|
|
||||||
"rawDiagnostic": "Array elements contain mixed VARIANT types.",
|
|
||||||
"rawElementDataType": 32767,
|
|
||||||
"rawValues": {
|
|
||||||
"values": [
|
|
||||||
"AAE=",
|
|
||||||
"AgM="
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"family": "MX_EVENT_FAMILY_ON_DATA_CHANGE",
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"serverHandle": 12,
|
|
||||||
"itemHandle": 34,
|
|
||||||
"value": {
|
|
||||||
"dataType": "MX_DATA_TYPE_INTEGER",
|
|
||||||
"variantType": "VT_I4",
|
|
||||||
"int32Value": 123
|
|
||||||
},
|
|
||||||
"quality": 192,
|
|
||||||
"sourceTimestamp": "2026-01-01T00:00:00Z",
|
|
||||||
"statuses": [
|
|
||||||
{
|
|
||||||
"success": 1,
|
|
||||||
"category": "MX_STATUS_CATEGORY_OK",
|
|
||||||
"detectedBy": "MX_STATUS_SOURCE_RESPONDING_LMX",
|
|
||||||
"detail": 0,
|
|
||||||
"rawCategory": 0,
|
|
||||||
"rawDetectedBy": 0,
|
|
||||||
"diagnosticText": "OK"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"workerSequence": "1",
|
|
||||||
"workerTimestamp": "2026-01-01T00:00:00.010Z",
|
|
||||||
"gatewayReceiveTimestamp": "2026-01-01T00:00:00.015Z",
|
|
||||||
"onDataChange": {}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"backendName": "mxaccess-worker",
|
|
||||||
"workerProcessId": 1234,
|
|
||||||
"workerProtocolVersion": 1,
|
|
||||||
"gatewayProtocolVersion": 1,
|
|
||||||
"capabilities": [
|
|
||||||
"unary-open-session",
|
|
||||||
"unary-close-session",
|
|
||||||
"unary-invoke",
|
|
||||||
"server-stream-events"
|
|
||||||
],
|
|
||||||
"defaultCommandTimeout": "30s",
|
|
||||||
"protocolStatus": {
|
|
||||||
"code": "PROTOCOL_STATUS_CODE_OK",
|
|
||||||
"message": "Session opened."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionId": "session-fixture",
|
|
||||||
"clientCorrelationId": "fixture-register-1",
|
|
||||||
"command": {
|
|
||||||
"kind": "MX_COMMAND_KIND_REGISTER",
|
|
||||||
"register": {
|
|
||||||
"clientName": "fixture-client"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"contractName": "mxaccess-gateway",
|
|
||||||
"gatewayProtocolVersion": 1,
|
|
||||||
"workerProtocolVersion": 1,
|
|
||||||
"protoRoot": "src/MxGateway.Contracts/Protos",
|
|
||||||
"sourceFiles": [
|
|
||||||
{
|
|
||||||
"path": "mxaccess_gateway.proto",
|
|
||||||
"role": "public_gateway"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"path": "mxaccess_worker.proto",
|
|
||||||
"role": "gateway_worker_ipc"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"descriptorSet": "clients/proto/descriptors/mxaccessgw-client-v1.protoset",
|
|
||||||
"fixtureRoot": "clients/proto/fixtures/golden",
|
|
||||||
"behaviorFixtureRoot": "clients/proto/fixtures/behavior",
|
|
||||||
"generatedOutputs": {
|
|
||||||
"dotnet": "clients/dotnet/generated",
|
|
||||||
"go": "clients/go/internal/generated",
|
|
||||||
"rust": "clients/rust/src/generated",
|
|
||||||
"python": "clients/python/src/mxgateway/generated",
|
|
||||||
"java": "clients/java/src/main/generated"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
Generated
-1308
File diff suppressed because it is too large
Load Diff
@@ -1,39 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "mxgateway-client"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
build = "build.rs"
|
|
||||||
|
|
||||||
[workspace]
|
|
||||||
members = ["crates/mxgw-cli"]
|
|
||||||
resolver = "2"
|
|
||||||
|
|
||||||
[workspace.package]
|
|
||||||
edition = "2021"
|
|
||||||
version = "0.1.0"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
clap = { version = "4.5.53", features = ["derive"] }
|
|
||||||
prost = "0.13.5"
|
|
||||||
prost-types = "0.13.5"
|
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
|
||||||
serde_json = "1.0.145"
|
|
||||||
thiserror = "2.0.17"
|
|
||||||
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
|
|
||||||
tonic = { version = "0.13.1", features = ["transport"] }
|
|
||||||
tonic-build = "0.13.1"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
prost = { workspace = true }
|
|
||||||
prost-types = { workspace = true }
|
|
||||||
thiserror = { workspace = true }
|
|
||||||
tonic = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
tokio = { workspace = true }
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tonic-build = { workspace = true }
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
# Rust Client Workspace
|
|
||||||
|
|
||||||
The Rust client workspace contains the MXAccess Gateway client library, a
|
|
||||||
test CLI, and scaffold tests for generated contract wiring. The library uses
|
|
||||||
the shared protobuf inputs documented in
|
|
||||||
`../../docs/client-proto-generation.md` so the Rust bindings compile against
|
|
||||||
the same public gateway and worker contracts as the server.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```text
|
|
||||||
clients/rust/
|
|
||||||
Cargo.toml
|
|
||||||
build.rs
|
|
||||||
src/
|
|
||||||
tests/
|
|
||||||
crates/mxgw-cli/
|
|
||||||
```
|
|
||||||
|
|
||||||
`build.rs` reads the `.proto` files from
|
|
||||||
`../../src/MxGateway.Contracts/Protos` and generates `tonic`/`prost` bindings
|
|
||||||
into Cargo build output. `src/generated.rs` declares the Rust modules that
|
|
||||||
include those generated files. `src/generated` remains reserved for checked-in
|
|
||||||
generator output if the crate later changes to source-tree generation.
|
|
||||||
|
|
||||||
## Build And Test
|
|
||||||
|
|
||||||
Run the Rust workspace checks from `clients/rust`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cargo fmt --all --check
|
|
||||||
cargo test --workspace
|
|
||||||
cargo check --workspace
|
|
||||||
```
|
|
||||||
|
|
||||||
The build script uses `protoc` from `PATH` or the Windows path recorded in
|
|
||||||
`../../docs/toolchain-links.md`.
|
|
||||||
|
|
||||||
## CLI
|
|
||||||
|
|
||||||
The scaffold CLI exposes version information:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cargo run -p mxgw-cli -- version --json
|
|
||||||
```
|
|
||||||
|
|
||||||
Additional commands are implemented with the client/session wrapper work.
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
|
||||||
- [Rust Client Detailed Design](../../docs/clients-rust-design.md)
|
|
||||||
- [Rust Style Guide](../../docs/style-guides/RustStyleGuide.md)
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
use std::env;
|
|
||||||
use std::error::Error;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
configure_protoc();
|
|
||||||
|
|
||||||
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
|
|
||||||
let repo_root = manifest_dir
|
|
||||||
.parent()
|
|
||||||
.and_then(Path::parent)
|
|
||||||
.ok_or("clients/rust must live two levels below the repository root")?;
|
|
||||||
let proto_root = repo_root.join("src/MxGateway.Contracts/Protos");
|
|
||||||
let gateway_proto = proto_root.join("mxaccess_gateway.proto");
|
|
||||||
let worker_proto = proto_root.join("mxaccess_worker.proto");
|
|
||||||
let descriptor_path = PathBuf::from(env::var("OUT_DIR")?).join("mxaccessgw-client-v1.protoset");
|
|
||||||
|
|
||||||
println!("cargo:rerun-if-changed={}", gateway_proto.display());
|
|
||||||
println!("cargo:rerun-if-changed={}", worker_proto.display());
|
|
||||||
|
|
||||||
tonic_build::configure()
|
|
||||||
.build_server(false)
|
|
||||||
.build_client(true)
|
|
||||||
.file_descriptor_set_path(descriptor_path)
|
|
||||||
.compile_protos(
|
|
||||||
&[gateway_proto.as_path(), worker_proto.as_path()],
|
|
||||||
&[proto_root.as_path()],
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn configure_protoc() {
|
|
||||||
if env::var_os("PROTOC").is_some() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for candidate in protoc_candidates() {
|
|
||||||
if candidate.is_file() {
|
|
||||||
env::set_var("PROTOC", candidate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn protoc_candidates() -> Vec<PathBuf> {
|
|
||||||
let mut candidates = Vec::new();
|
|
||||||
|
|
||||||
if cfg!(windows) {
|
|
||||||
if let Some(local_app_data) = env::var_os("LOCALAPPDATA") {
|
|
||||||
candidates.push(PathBuf::from(local_app_data).join(
|
|
||||||
"Microsoft/WinGet/Packages/Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe/bin/protoc.exe",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates.push(PathBuf::from("protoc"));
|
|
||||||
candidates
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "mxgw-cli"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
publish.workspace = true
|
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "mxgw"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
clap = { workspace = true }
|
|
||||||
mxgateway-client = { path = "../.." }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
use std::process::ExitCode;
|
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
use mxgateway_client::{CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
#[command(name = "mxgw")]
|
|
||||||
#[command(about = "MXAccess Gateway Rust test CLI")]
|
|
||||||
struct Cli {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
|
||||||
enum Command {
|
|
||||||
Version {
|
|
||||||
#[arg(long)]
|
|
||||||
json: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
|
||||||
let cli = Cli::parse();
|
|
||||||
run(cli);
|
|
||||||
ExitCode::SUCCESS
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(cli: Cli) {
|
|
||||||
match cli.command {
|
|
||||||
Command::Version { json } => print_version(json),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn print_version(use_json: bool) {
|
|
||||||
if use_json {
|
|
||||||
println!(
|
|
||||||
"{}",
|
|
||||||
json!({
|
|
||||||
"clientVersion": CLIENT_VERSION,
|
|
||||||
"gatewayProtocolVersion": GATEWAY_PROTOCOL_VERSION,
|
|
||||||
"workerProtocolVersion": WORKER_PROTOCOL_VERSION,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("mxgw {CLIENT_VERSION}");
|
|
||||||
println!("gateway protocol {GATEWAY_PROTOCOL_VERSION}");
|
|
||||||
println!("worker protocol {WORKER_PROTOCOL_VERSION}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
use super::Cli;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parses_version_json_command() {
|
|
||||||
let parsed = Cli::try_parse_from(["mxgw", "version", "--json"]);
|
|
||||||
assert!(parsed.is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
/// API key wrapper that avoids exposing raw credentials in formatted output.
|
|
||||||
#[derive(Clone, Eq, PartialEq)]
|
|
||||||
pub struct ApiKey(String);
|
|
||||||
|
|
||||||
impl ApiKey {
|
|
||||||
pub fn new(value: impl Into<String>) -> Self {
|
|
||||||
Self(value.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn expose_secret(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for ApiKey {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
formatter
|
|
||||||
.debug_tuple("ApiKey")
|
|
||||||
.field(&"<redacted>")
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ApiKey {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
formatter.write_str("<redacted>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
use tonic::transport::Channel;
|
|
||||||
|
|
||||||
use crate::error::Error;
|
|
||||||
use crate::generated::mxaccess_gateway::v1::mx_access_gateway_client::MxAccessGatewayClient;
|
|
||||||
use crate::options::ClientOptions;
|
|
||||||
|
|
||||||
/// Thin owner for the generated gateway client.
|
|
||||||
pub struct GatewayClient {
|
|
||||||
inner: MxAccessGatewayClient<Channel>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GatewayClient {
|
|
||||||
pub async fn connect(options: ClientOptions) -> Result<Self, Error> {
|
|
||||||
let endpoint = Channel::from_shared(options.endpoint().to_owned()).map_err(|source| {
|
|
||||||
Error::InvalidEndpoint {
|
|
||||||
endpoint: options.endpoint().to_owned(),
|
|
||||||
detail: source.to_string(),
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
let channel = endpoint.connect().await?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
inner: MxAccessGatewayClient::new(channel),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn into_inner(self) -> MxAccessGatewayClient<Channel> {
|
|
||||||
self.inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
use thiserror::Error as ThisError;
|
|
||||||
|
|
||||||
#[derive(Debug, ThisError)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("invalid gateway endpoint `{endpoint}`: {detail}")]
|
|
||||||
InvalidEndpoint { endpoint: String, detail: String },
|
|
||||||
|
|
||||||
#[error("gateway transport error: {0}")]
|
|
||||||
Transport(#[from] tonic::transport::Error),
|
|
||||||
|
|
||||||
#[error("gateway status error: {0}")]
|
|
||||||
Status(#[from] tonic::Status),
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
pub mod mxaccess_gateway {
|
|
||||||
pub mod v1 {
|
|
||||||
#![allow(clippy::large_enum_variant)]
|
|
||||||
|
|
||||||
tonic::include_proto!("mxaccess_gateway.v1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod mxaccess_worker {
|
|
||||||
pub mod v1 {
|
|
||||||
#![allow(clippy::large_enum_variant)]
|
|
||||||
|
|
||||||
tonic::include_proto!("mxaccess_worker.v1");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
//! Rust client scaffold for MXAccess Gateway.
|
|
||||||
//!
|
|
||||||
//! The crate compiles generated `tonic` bindings from the shared gateway
|
|
||||||
//! protobuf contracts and exposes a small handwritten surface for future client
|
|
||||||
//! implementation work.
|
|
||||||
|
|
||||||
pub mod auth;
|
|
||||||
pub mod client;
|
|
||||||
pub mod error;
|
|
||||||
pub mod generated;
|
|
||||||
pub mod options;
|
|
||||||
pub mod session;
|
|
||||||
pub mod value;
|
|
||||||
pub mod version;
|
|
||||||
|
|
||||||
pub use auth::ApiKey;
|
|
||||||
pub use client::GatewayClient;
|
|
||||||
pub use error::Error;
|
|
||||||
pub use options::ClientOptions;
|
|
||||||
pub use session::Session;
|
|
||||||
pub use version::{CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION};
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate::auth::ApiKey;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct ClientOptions {
|
|
||||||
endpoint: String,
|
|
||||||
api_key: Option<ApiKey>,
|
|
||||||
plaintext: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientOptions {
|
|
||||||
pub fn new(endpoint: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
endpoint: endpoint.into(),
|
|
||||||
api_key: None,
|
|
||||||
plaintext: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_api_key(mut self, api_key: ApiKey) -> Self {
|
|
||||||
self.api_key = Some(api_key);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn endpoint(&self) -> &str {
|
|
||||||
&self.endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn api_key(&self) -> Option<&ApiKey> {
|
|
||||||
self.api_key.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn plaintext(&self) -> bool {
|
|
||||||
self.plaintext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for ClientOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new("http://127.0.0.1:5000")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for ClientOptions {
|
|
||||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
formatter
|
|
||||||
.debug_struct("ClientOptions")
|
|
||||||
.field("endpoint", &self.endpoint)
|
|
||||||
.field("api_key", &self.api_key.as_ref().map(|_| "<redacted>"))
|
|
||||||
.field("plaintext", &self.plaintext)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/// Session identifier returned by the gateway.
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct Session {
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Session {
|
|
||||||
pub fn new(id: impl Into<String>) -> Self {
|
|
||||||
Self { id: id.into() }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> &str {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
use crate::generated::mxaccess_gateway::v1::MxValue;
|
|
||||||
|
|
||||||
pub fn int32_value(value: i32) -> MxValue {
|
|
||||||
MxValue {
|
|
||||||
data_type: crate::generated::mxaccess_gateway::v1::MxDataType::Integer as i32,
|
|
||||||
kind: Some(crate::generated::mxaccess_gateway::v1::mx_value::Kind::Int32Value(value)),
|
|
||||||
..MxValue::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
pub const CLIENT_VERSION: &str = "0.1.0-dev";
|
|
||||||
pub const GATEWAY_PROTOCOL_VERSION: u32 = 1;
|
|
||||||
pub const WORKER_PROTOCOL_VERSION: u32 = 1;
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use mxgateway_client::generated::mxaccess_gateway::v1::{
|
|
||||||
mx_command, mx_value, MxCommand, MxCommandKind, MxCommandRequest, MxDataType, MxEvent,
|
|
||||||
MxEventFamily, MxValue, OpenSessionReply, ProtocolStatusCode, RegisterCommand,
|
|
||||||
};
|
|
||||||
use mxgateway_client::{GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION};
|
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn generated_golden_fixtures_are_available() {
|
|
||||||
for fixture_name in [
|
|
||||||
"open-session-reply.ok.json",
|
|
||||||
"register-command-request.json",
|
|
||||||
"on-data-change-event.json",
|
|
||||||
] {
|
|
||||||
let fixture = read_fixture(fixture_name);
|
|
||||||
assert!(
|
|
||||||
fixture.is_object(),
|
|
||||||
"{fixture_name} must remain a protobuf JSON object"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn open_session_fixture_matches_protocol_versions() {
|
|
||||||
let fixture = read_fixture("open-session-reply.ok.json");
|
|
||||||
let reply = OpenSessionReply {
|
|
||||||
session_id: string_field(&fixture, "sessionId"),
|
|
||||||
backend_name: string_field(&fixture, "backendName"),
|
|
||||||
worker_process_id: i32_field(&fixture, "workerProcessId"),
|
|
||||||
worker_protocol_version: u32_field(&fixture, "workerProtocolVersion"),
|
|
||||||
gateway_protocol_version: u32_field(&fixture, "gatewayProtocolVersion"),
|
|
||||||
protocol_status: Some(
|
|
||||||
mxgateway_client::generated::mxaccess_gateway::v1::ProtocolStatus {
|
|
||||||
code: ProtocolStatusCode::Ok as i32,
|
|
||||||
message: string_field(&fixture["protocolStatus"], "message"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
..OpenSessionReply::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(reply.gateway_protocol_version, GATEWAY_PROTOCOL_VERSION);
|
|
||||||
assert_eq!(reply.worker_protocol_version, WORKER_PROTOCOL_VERSION);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn register_fixture_can_build_generated_request() {
|
|
||||||
let fixture = read_fixture("register-command-request.json");
|
|
||||||
let command = &fixture["command"];
|
|
||||||
let request = MxCommandRequest {
|
|
||||||
session_id: string_field(&fixture, "sessionId"),
|
|
||||||
client_correlation_id: string_field(&fixture, "clientCorrelationId"),
|
|
||||||
command: Some(MxCommand {
|
|
||||||
kind: MxCommandKind::Register as i32,
|
|
||||||
payload: Some(mx_command::Payload::Register(RegisterCommand {
|
|
||||||
client_name: string_field(&command["register"], "clientName"),
|
|
||||||
})),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(request.session_id, "session-fixture");
|
|
||||||
assert_eq!(
|
|
||||||
request.command.unwrap().kind,
|
|
||||||
MxCommandKind::Register as i32
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn on_data_change_fixture_can_build_generated_event() {
|
|
||||||
let fixture = read_fixture("on-data-change-event.json");
|
|
||||||
let event = MxEvent {
|
|
||||||
family: MxEventFamily::OnDataChange as i32,
|
|
||||||
session_id: string_field(&fixture, "sessionId"),
|
|
||||||
server_handle: i32_field(&fixture, "serverHandle"),
|
|
||||||
item_handle: i32_field(&fixture, "itemHandle"),
|
|
||||||
value: Some(MxValue {
|
|
||||||
data_type: MxDataType::Integer as i32,
|
|
||||||
variant_type: string_field(&fixture["value"], "variantType"),
|
|
||||||
kind: Some(mx_value::Kind::Int32Value(i32_field(
|
|
||||||
&fixture["value"],
|
|
||||||
"int32Value",
|
|
||||||
))),
|
|
||||||
..MxValue::default()
|
|
||||||
}),
|
|
||||||
quality: i32_field(&fixture, "quality"),
|
|
||||||
worker_sequence: u64_field(&fixture, "workerSequence"),
|
|
||||||
..MxEvent::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert_eq!(event.family, MxEventFamily::OnDataChange as i32);
|
|
||||||
assert_eq!(event.value.unwrap().data_type, MxDataType::Integer as i32);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_fixture(name: &str) -> Value {
|
|
||||||
let path = fixture_root().join(name);
|
|
||||||
let data = fs::read_to_string(&path).unwrap_or_else(|error| {
|
|
||||||
panic!("failed to read fixture {}: {error}", path.display());
|
|
||||||
});
|
|
||||||
|
|
||||||
serde_json::from_str(&data).unwrap_or_else(|error| {
|
|
||||||
panic!("failed to parse fixture {}: {error}", path.display());
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fixture_root() -> PathBuf {
|
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../proto/fixtures/golden")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn string_field(value: &Value, name: &str) -> String {
|
|
||||||
value[name]
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or_else(|| panic!("missing string field {name}"))
|
|
||||||
.to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn i32_field(value: &Value, name: &str) -> i32 {
|
|
||||||
value[name]
|
|
||||||
.as_i64()
|
|
||||||
.unwrap_or_else(|| panic!("missing i32 field {name}"))
|
|
||||||
.try_into()
|
|
||||||
.unwrap_or_else(|_| panic!("field {name} does not fit in i32"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn u32_field(value: &Value, name: &str) -> u32 {
|
|
||||||
value[name]
|
|
||||||
.as_u64()
|
|
||||||
.unwrap_or_else(|| panic!("missing u32 field {name}"))
|
|
||||||
.try_into()
|
|
||||||
.unwrap_or_else(|_| panic!("field {name} does not fit in u32"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn u64_field(value: &Value, name: &str) -> u64 {
|
|
||||||
if let Some(number) = value[name].as_u64() {
|
|
||||||
return number;
|
|
||||||
}
|
|
||||||
|
|
||||||
value[name]
|
|
||||||
.as_str()
|
|
||||||
.unwrap_or_else(|| panic!("missing u64 field {name}"))
|
|
||||||
.parse()
|
|
||||||
.unwrap_or_else(|_| panic!("field {name} does not parse as u64"))
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
# Client Behavior Fixtures
|
|
||||||
|
|
||||||
Client behavior fixtures define the shared expectations used by the official
|
|
||||||
.NET, Go, Rust, Python, and Java clients. They keep wrapper behavior aligned
|
|
||||||
while each language exposes idiomatic APIs over the same protobuf contract.
|
|
||||||
|
|
||||||
## Fixture Set
|
|
||||||
|
|
||||||
The fixture manifest is `clients/proto/fixtures/behavior/manifest.json`.
|
|
||||||
`clients/proto/proto-inputs.json` references the fixture root through
|
|
||||||
`behaviorFixtureRoot` so generators and client test projects can discover the
|
|
||||||
same files they use for descriptor inputs.
|
|
||||||
|
|
||||||
The fixture set contains:
|
|
||||||
|
|
||||||
- command reply protobuf JSON,
|
|
||||||
- ordered event stream protobuf JSON samples,
|
|
||||||
- `MxValue` conversion case sets,
|
|
||||||
- `MxStatusProxy` conversion case sets,
|
|
||||||
- authentication and authorization error expectations,
|
|
||||||
- timeout and cancellation behavior expectations.
|
|
||||||
|
|
||||||
Protobuf message fixtures use protobuf JSON field names and enum values. Files
|
|
||||||
that describe client wrapper behavior use explicit JSON fields instead of a
|
|
||||||
proto message because those expectations apply above the generated transport
|
|
||||||
types.
|
|
||||||
|
|
||||||
## Command Replies
|
|
||||||
|
|
||||||
Command reply fixtures live in
|
|
||||||
`clients/proto/fixtures/behavior/command-replies/`. They parse as
|
|
||||||
`mxaccess_gateway.v1.MxCommandReply`.
|
|
||||||
|
|
||||||
Clients use these fixtures to verify that successful and failed MXAccess
|
|
||||||
commands both carry the full reply details:
|
|
||||||
|
|
||||||
- `protocolStatus`,
|
|
||||||
- `hresult`,
|
|
||||||
- `returnValue`,
|
|
||||||
- repeated `statuses`,
|
|
||||||
- method-specific reply payloads when MXAccess returns out parameters.
|
|
||||||
|
|
||||||
MXAccess failures remain command replies when the gateway reached the worker and
|
|
||||||
the worker captured HRESULT or `MXSTATUS_PROXY` details. Client wrappers should
|
|
||||||
map those replies to rich command errors without discarding the raw reply.
|
|
||||||
|
|
||||||
## Event Streams
|
|
||||||
|
|
||||||
Event stream fixtures live in
|
|
||||||
`clients/proto/fixtures/behavior/event-streams/`. Each file contains an ordered
|
|
||||||
`events` array whose entries parse as `mxaccess_gateway.v1.MxEvent`.
|
|
||||||
|
|
||||||
Clients use these fixtures to verify that stream helpers preserve
|
|
||||||
`workerSequence` order and expose each native event family:
|
|
||||||
|
|
||||||
- `OnDataChange`,
|
|
||||||
- `OnWriteComplete`,
|
|
||||||
- `OperationComplete`,
|
|
||||||
- `OnBufferedDataChange`.
|
|
||||||
|
|
||||||
Wrappers must not reorder, coalesce, or drop events while reading the fixture.
|
|
||||||
|
|
||||||
## Value And Status Conversion
|
|
||||||
|
|
||||||
Value fixtures live in `clients/proto/fixtures/behavior/values/`. Each case
|
|
||||||
contains a `value` object that parses as `mxaccess_gateway.v1.MxValue`.
|
|
||||||
|
|
||||||
Status fixtures live in `clients/proto/fixtures/behavior/statuses/`. Each case
|
|
||||||
contains a `status` object that parses as
|
|
||||||
`mxaccess_gateway.v1.MxStatusProxy`.
|
|
||||||
|
|
||||||
Clients use these fixtures to verify typed projections and raw fallback
|
|
||||||
behavior. A language helper may expose native booleans, integers, strings,
|
|
||||||
arrays, and timestamps, but it must keep `rawDiagnostic`, raw data type fields,
|
|
||||||
and raw byte payloads accessible when conversion is incomplete.
|
|
||||||
|
|
||||||
## Auth, Timeout, And Cancel Behavior
|
|
||||||
|
|
||||||
Authentication fixtures live in `clients/proto/fixtures/behavior/auth/`. They
|
|
||||||
separate `UNAUTHENTICATED` from `PERMISSION_DENIED` so clients map missing or
|
|
||||||
invalid credentials differently from missing scopes. Expected output strings
|
|
||||||
contain only redacted credentials.
|
|
||||||
|
|
||||||
Timeout and cancellation fixtures live in
|
|
||||||
`clients/proto/fixtures/behavior/timeout-cancel/`. They document that canceling
|
|
||||||
or timing out a client call stops the client from waiting, but it does not abort
|
|
||||||
an in-flight MXAccess COM call on the worker STA. Clients should follow up with
|
|
||||||
`GetSessionState` or `CloseSession` before reusing handles after an uncertain
|
|
||||||
command timeout.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
Run the fixture validation tests after changing the behavior fixture set:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
powershell -ExecutionPolicy Bypass -File scripts/validate-client-behavior-fixtures.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
The script runs the focused C# contract tests that parse all protobuf JSON
|
|
||||||
fixtures and validate deterministic wrapper expectation files.
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- [Client Proto Generation](./client-proto-generation.md)
|
|
||||||
- [Client Libraries Detailed Design](./client-libraries-design.md)
|
|
||||||
- [Protobuf Contracts](./Contracts.md)
|
|
||||||
@@ -18,12 +18,6 @@ event, value, and status shapes.
|
|||||||
Generated C# output is written to `src/MxGateway.Contracts/Generated/`. Do not
|
Generated C# output is written to `src/MxGateway.Contracts/Generated/`. Do not
|
||||||
hand-edit generated files.
|
hand-edit generated files.
|
||||||
|
|
||||||
Client generation inputs are published through
|
|
||||||
`clients/proto/proto-inputs.json` and the descriptor set under
|
|
||||||
`clients/proto/descriptors/`. See
|
|
||||||
[Client Proto Generation](./client-proto-generation.md) for language-specific
|
|
||||||
generation inputs, output directories, and golden protobuf JSON fixtures.
|
|
||||||
|
|
||||||
## Generation
|
## Generation
|
||||||
|
|
||||||
Run the contracts build to regenerate C# protobuf and gRPC code:
|
Run the contracts build to regenerate C# protobuf and gRPC code:
|
||||||
@@ -45,15 +39,8 @@ gateway and test projects:
|
|||||||
dotnet build src/MxGateway.sln
|
dotnet build src/MxGateway.sln
|
||||||
```
|
```
|
||||||
|
|
||||||
Regenerate the client descriptor after changing either `.proto` file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
powershell -ExecutionPolicy Bypass -File scripts/publish-client-proto-inputs.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Client Proto Generation](./client-proto-generation.md)
|
|
||||||
- [Gateway Process Detailed Design](./gateway-process-design.md)
|
- [Gateway Process Detailed Design](./gateway-process-design.md)
|
||||||
- [MXAccess Worker Instance Detailed Design](./mxaccess-worker-instance-design.md)
|
- [MXAccess Worker Instance Detailed Design](./mxaccess-worker-instance-design.md)
|
||||||
- [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
|
- [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
# Gateway Testing
|
|
||||||
|
|
||||||
Gateway tests run without installed MXAccess by using fake workers, fake
|
|
||||||
transports, and in-process gRPC service fakes. Live MXAccess verification belongs
|
|
||||||
in opt-in integration tests because it depends on installed COM components and
|
|
||||||
provider state.
|
|
||||||
|
|
||||||
## Fake Worker Harness
|
|
||||||
|
|
||||||
`FakeWorkerHarness` in `src/MxGateway.Tests/Gateway/Workers/Fakes/` provides an
|
|
||||||
in-process worker side for named-pipe IPC tests. It uses the same
|
|
||||||
`WorkerFrameReader`, `WorkerFrameWriter`, and `WorkerEnvelope` contract as the
|
|
||||||
gateway so tests exercise real frame validation and worker-client state changes.
|
|
||||||
|
|
||||||
Use the harness when a gateway or session test needs worker behavior without
|
|
||||||
starting `MxGateway.Worker.exe` or loading MXAccess COM. The harness scripts:
|
|
||||||
|
|
||||||
- `WorkerHello` and `WorkerReady` startup,
|
|
||||||
- command replies with matching correlation ids,
|
|
||||||
- ordered `WorkerEvent` frames,
|
|
||||||
- `WorkerHeartbeat` frames,
|
|
||||||
- `WorkerFault` frames,
|
|
||||||
- shutdown acknowledgements,
|
|
||||||
- malformed protobuf payloads and oversized frame headers,
|
|
||||||
- slow or hung workers by withholding a reply.
|
|
||||||
|
|
||||||
Session-level tests can connect the harness to the pipe created by
|
|
||||||
`SessionWorkerClientFactory` with `ConnectToGatewayPipeAsync`. Lower-level
|
|
||||||
`WorkerClient` tests can use `CreateConnectedPairAsync` to create both pipe ends
|
|
||||||
inside the test.
|
|
||||||
|
|
||||||
`GatewayEndToEndFakeWorkerSmokeTests` composes the real gRPC service,
|
|
||||||
`SessionManager`, `SessionWorkerClientFactory`, `WorkerClient`, and
|
|
||||||
`EventStreamService` with a scripted fake worker launcher. The smoke test covers
|
|
||||||
`OpenSession`, `Register`, `AddItem`, `Advise`, one streamed `OnDataChange`
|
|
||||||
event, and `CloseSession` without loading MXAccess COM.
|
|
||||||
|
|
||||||
## Live MXAccess Smoke
|
|
||||||
|
|
||||||
`WorkerLiveMxAccessSmokeTests` in `src/MxGateway.IntegrationTests/` composes the
|
|
||||||
real gRPC service, `SessionManager`, `SessionWorkerClientFactory`,
|
|
||||||
`WorkerClient`, `WorkerProcessLauncher`, and `MxGateway.Worker.exe`. It is
|
|
||||||
skipped unless `MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1` is set because it creates
|
|
||||||
the installed MXAccess COM object and depends on live provider state.
|
|
||||||
|
|
||||||
The live smoke opens a gateway session, launches the x86 worker, runs
|
|
||||||
`Register`, `AddItem`, and `Advise`, waits a bounded time for one
|
|
||||||
`OnDataChange`, and closes the session in a `finally` block so the worker gets a
|
|
||||||
graceful shutdown request even when a command or event assertion fails.
|
|
||||||
|
|
||||||
Build the worker before running the smoke:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet build src/MxGateway.Worker/MxGateway.Worker.csproj -p:Platform=x86
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the smoke explicitly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$env:MXGATEWAY_RUN_LIVE_MXACCESS_TESTS = "1"
|
|
||||||
dotnet test src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj --filter FullyQualifiedName~WorkerLiveMxAccessSmokeTests
|
|
||||||
```
|
|
||||||
|
|
||||||
Optional live smoke variables:
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| `MXGATEWAY_LIVE_MXACCESS_WORKER_EXE` | First existing `MxGateway.Worker.exe` under `src/MxGateway.Worker/bin/...` | Worker executable path. Set this when running against a packaged worker or a non-default build output. |
|
|
||||||
| `MXGATEWAY_LIVE_MXACCESS_ITEM` | `TestChildObject.TestInt` | MXAccess item reference used by `AddItem`. |
|
|
||||||
| `MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME` | `MxGateway.IntegrationTests` | Client name passed to `Register`. |
|
|
||||||
| `MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS` | `15` | Maximum wait for the first `OnDataChange`. |
|
|
||||||
|
|
||||||
The test output includes session id, worker process id, command status,
|
|
||||||
HRESULT/status diagnostics, event sequence and handles, close status, and worker
|
|
||||||
stdout/stderr lines emitted during the run.
|
|
||||||
|
|
||||||
## Focused Commands
|
|
||||||
|
|
||||||
Run the fake worker tests after changing gateway worker IPC, session startup, or
|
|
||||||
event streaming behavior:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~FakeWorkerHarnessTests
|
|
||||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~SessionWorkerClientFactoryFakeWorkerTests
|
|
||||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~GatewayEndToEndFakeWorkerSmokeTests
|
|
||||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~WorkerClientTests
|
|
||||||
dotnet test src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj -p:Platform=x86 --filter FullyQualifiedName~WorkerPipeSessionTests
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the gateway test project after shared gateway test infrastructure changes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- [Gateway Process Design](./gateway-process-design.md)
|
|
||||||
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
|
|
||||||
- [MXAccess Worker Instance Detailed Design](./mxaccess-worker-instance-design.md)
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# Worker Process Launcher
|
|
||||||
|
|
||||||
The gateway uses `WorkerProcessLauncher` to validate and start one worker
|
|
||||||
process for a gateway session. The launcher owns process start semantics only;
|
|
||||||
pipe handshaking and `WorkerReady` validation remain part of the worker client
|
|
||||||
startup path.
|
|
||||||
|
|
||||||
## Launch Inputs
|
|
||||||
|
|
||||||
`WorkerProcessLaunchRequest` carries the per-session bootstrap values:
|
|
||||||
|
|
||||||
- `SessionId`,
|
|
||||||
- `PipeName`,
|
|
||||||
- `ProtocolVersion`,
|
|
||||||
- `Nonce`,
|
|
||||||
- optional `PipeReservation` cleanup handle.
|
|
||||||
|
|
||||||
The launcher passes `SessionId`, `PipeName`, and `ProtocolVersion` as command
|
|
||||||
line arguments:
|
|
||||||
|
|
||||||
```text
|
|
||||||
--session-id <sessionId> --pipe-name <pipeName> --protocol-version <version>
|
|
||||||
```
|
|
||||||
|
|
||||||
The launcher sets the nonce through the `MXGATEWAY_WORKER_NONCE` environment
|
|
||||||
variable. The nonce is not included in `WorkerProcessCommandLine` so logs and
|
|
||||||
diagnostics can report the launch command without exposing the secret.
|
|
||||||
|
|
||||||
## Validation And Cleanup
|
|
||||||
|
|
||||||
Before starting the process, the launcher validates that the configured worker
|
|
||||||
path exists, has a `.exe` extension, contains a valid Windows Portable
|
|
||||||
Executable header, and matches the configured `RequiredArchitecture`.
|
|
||||||
|
|
||||||
After the process starts, `IWorkerStartupProbe` waits for startup readiness.
|
|
||||||
The default probe only verifies that the worker did not exit immediately. The
|
|
||||||
worker client replaces this probe when pipe connection, hello, and
|
|
||||||
`WorkerReady` handling are implemented.
|
|
||||||
|
|
||||||
If startup fails or exceeds `WorkerOptions.StartupTimeoutSeconds`, the launcher
|
|
||||||
kills the worker process tree, disposes the process handle, disposes the
|
|
||||||
optional pipe reservation, records a worker kill metric, and reports a
|
|
||||||
`WorkerProcessLaunchException`.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
Run the focused launcher tests after changing process launch behavior:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter WorkerProcessLauncherTests
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the gateway build because the launcher is part of `MxGateway.Server`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet build src/MxGateway.Server/MxGateway.Server.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- [Gateway Process Detailed Design](./gateway-process-design.md)
|
|
||||||
- [Worker Frame Protocol](./WorkerFrameProtocol.md)
|
|
||||||
@@ -26,12 +26,6 @@ Language-specific plans:
|
|||||||
- `docs/clients-python-design.md`
|
- `docs/clients-python-design.md`
|
||||||
- `docs/clients-java-design.md`
|
- `docs/clients-java-design.md`
|
||||||
|
|
||||||
Shared generation inputs:
|
|
||||||
|
|
||||||
- `docs/client-proto-generation.md`
|
|
||||||
- `docs/ClientBehaviorFixtures.md`
|
|
||||||
- `clients/proto/proto-inputs.json`
|
|
||||||
|
|
||||||
Language style guides:
|
Language style guides:
|
||||||
|
|
||||||
| Client | Style guide |
|
| Client | Style guide |
|
||||||
@@ -311,11 +305,6 @@ CLI output should support JSON for automated tests.
|
|||||||
Unit tests must run without a live gateway. Use fake gRPC services, mock
|
Unit tests must run without a live gateway. Use fake gRPC services, mock
|
||||||
transports, or generated test servers depending on language.
|
transports, or generated test servers depending on language.
|
||||||
|
|
||||||
Shared behavior fixtures live in `clients/proto/fixtures/behavior`. Every
|
|
||||||
client should include tests that load the fixture manifest and verify wrapper
|
|
||||||
behavior against the common command reply, event stream, value conversion,
|
|
||||||
status conversion, auth error, and timeout/cancel cases.
|
|
||||||
|
|
||||||
Required unit test areas:
|
Required unit test areas:
|
||||||
|
|
||||||
- options parsing,
|
- options parsing,
|
||||||
@@ -376,16 +365,6 @@ examples/
|
|||||||
Generated code should be reproducible from `src/MxGateway.Contracts/Protos/`.
|
Generated code should be reproducible from `src/MxGateway.Contracts/Protos/`.
|
||||||
Do not hand-edit generated code.
|
Do not hand-edit generated code.
|
||||||
|
|
||||||
The stable client proto manifest defines the generated-code directories:
|
|
||||||
|
|
||||||
```text
|
|
||||||
clients/dotnet/generated
|
|
||||||
clients/go/internal/generated
|
|
||||||
clients/rust/src/generated
|
|
||||||
clients/python/src/mxgateway/generated
|
|
||||||
clients/java/src/main/generated
|
|
||||||
```
|
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
All clients should expose:
|
All clients should expose:
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
# Client Proto Generation
|
|
||||||
|
|
||||||
This document defines the stable protobuf inputs that official clients use to
|
|
||||||
generate language-specific gRPC bindings. The checked-in `.proto` files remain
|
|
||||||
the source of truth so clients do not drift from the gateway and worker
|
|
||||||
contracts.
|
|
||||||
|
|
||||||
## Stable Inputs
|
|
||||||
|
|
||||||
The stable client input manifest is `clients/proto/proto-inputs.json`. It
|
|
||||||
records:
|
|
||||||
|
|
||||||
- the public gateway protocol version,
|
|
||||||
- the worker IPC protocol version,
|
|
||||||
- the protobuf import root,
|
|
||||||
- the public and worker source files,
|
|
||||||
- the descriptor set path,
|
|
||||||
- golden fixture locations,
|
|
||||||
- behavior fixture locations,
|
|
||||||
- generated-code output directories for each planned client.
|
|
||||||
|
|
||||||
The source files listed by the manifest are:
|
|
||||||
|
|
||||||
- `src/MxGateway.Contracts/Protos/mxaccess_gateway.proto`
|
|
||||||
- `src/MxGateway.Contracts/Protos/mxaccess_worker.proto`
|
|
||||||
|
|
||||||
`mxaccess_gateway.proto` defines the public gRPC service and shared DTOs.
|
|
||||||
`mxaccess_worker.proto` is included in the descriptor because worker-aware
|
|
||||||
tests and fake-worker clients need the same command, reply, event, value, and
|
|
||||||
status shapes.
|
|
||||||
|
|
||||||
## Protocol Version
|
|
||||||
|
|
||||||
`GatewayContractInfo.GatewayProtocolVersion` is the public gateway protocol
|
|
||||||
version. `OpenSessionReply.gateway_protocol_version` returns the same value so
|
|
||||||
clients can compare their generated bindings against the gateway before issuing
|
|
||||||
MXAccess commands.
|
|
||||||
|
|
||||||
`GatewayContractInfo.WorkerProtocolVersion` remains the gateway-to-worker IPC
|
|
||||||
protocol version. It is also present in `OpenSessionReply` because parity
|
|
||||||
fixtures and fake-worker tests need to know the worker contract used by the
|
|
||||||
session.
|
|
||||||
|
|
||||||
## Descriptor Publishing
|
|
||||||
|
|
||||||
Run this command after changing either source `.proto` file or the client proto
|
|
||||||
manifest:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
scripts/publish-client-proto-inputs.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
The script writes
|
|
||||||
`clients/proto/descriptors/mxaccessgw-client-v1.protoset` with imports and
|
|
||||||
source information included. The descriptor is a generated artifact; do not edit
|
|
||||||
it by hand.
|
|
||||||
|
|
||||||
Use the check mode in CI or before committing:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
scripts/publish-client-proto-inputs.ps1 -Check
|
|
||||||
```
|
|
||||||
|
|
||||||
`-Check` rebuilds the descriptor in a temporary path and fails when the checked
|
|
||||||
in descriptor is stale.
|
|
||||||
|
|
||||||
## Output Directories
|
|
||||||
|
|
||||||
The manifest declares these generated-code directories:
|
|
||||||
|
|
||||||
| Client | Directory |
|
|
||||||
|--------|-----------|
|
|
||||||
| .NET | `clients/dotnet/generated` |
|
|
||||||
| Go | `clients/go/internal/generated` |
|
|
||||||
| Rust | `clients/rust/src/generated` |
|
|
||||||
| Python | `clients/python/src/mxgateway/generated` |
|
|
||||||
| Java | `clients/java/src/main/generated` |
|
|
||||||
|
|
||||||
Only generator output belongs in these directories. Handwritten client wrappers
|
|
||||||
belong in the language-specific source trees created by the client scaffold
|
|
||||||
issues.
|
|
||||||
|
|
||||||
## Language Generation Inputs
|
|
||||||
|
|
||||||
All generators use `src/MxGateway.Contracts/Protos` as the protobuf import
|
|
||||||
root. The checked-in descriptor is available when a language build prefers a
|
|
||||||
descriptor input, but the `.proto` files remain canonical.
|
|
||||||
|
|
||||||
.NET generation currently runs through the contracts project:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet build src/MxGateway.Contracts/MxGateway.Contracts.csproj
|
|
||||||
```
|
|
||||||
|
|
||||||
Future .NET client projects may either reference `MxGateway.Contracts` or
|
|
||||||
generate client-local files into `clients/dotnet/generated` with `Grpc.Tools`.
|
|
||||||
|
|
||||||
Go clients should generate `mxaccess_gateway.proto` and
|
|
||||||
`mxaccess_worker.proto` into `clients/go/internal/generated` with
|
|
||||||
`protoc-gen-go` and `protoc-gen-go-grpc`. Keep generated packages internal
|
|
||||||
unless the wrapper API intentionally exposes raw protobuf messages.
|
|
||||||
|
|
||||||
The Go scaffold provides a repo-local generation script:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
clients/go/generate-proto.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
The script maps both proto files into the internal Go package
|
|
||||||
`gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated` because
|
|
||||||
the source `.proto` files do not carry Go-specific `go_package` options. This
|
|
||||||
keeps language-specific packaging outside the public contract files.
|
|
||||||
|
|
||||||
Rust clients use `tonic-build` from `clients/rust/build.rs`. The build script
|
|
||||||
reads the shared `.proto` files and emits generated `tonic`/`prost` modules
|
|
||||||
into Cargo build output. `clients/rust/src/generated.rs` contains the module
|
|
||||||
declarations that include those generated files. `clients/rust/src/generated`
|
|
||||||
remains reserved for checked-in generator output if the crate later changes to
|
|
||||||
source-tree generation, and handwritten wrapper code stays outside that
|
|
||||||
directory.
|
|
||||||
|
|
||||||
Run the Rust workspace checks from `clients/rust`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
cargo fmt --all --check
|
|
||||||
cargo test --workspace
|
|
||||||
cargo check --workspace
|
|
||||||
```
|
|
||||||
|
|
||||||
Python clients should use `grpc_tools.protoc` and write generated modules under
|
|
||||||
`clients/python/src/mxgateway/generated` so imports stay separate from
|
|
||||||
handwritten async wrappers.
|
|
||||||
|
|
||||||
Java clients should use the Gradle protobuf plugin and write generated sources
|
|
||||||
under `clients/java/src/main/generated`. The Java client scaffold owns the
|
|
||||||
Gradle plugin versions and source-set wiring.
|
|
||||||
|
|
||||||
## Golden Fixtures
|
|
||||||
|
|
||||||
Golden protobuf JSON fixtures live in `clients/proto/fixtures/golden`. They
|
|
||||||
exercise payloads that every language client must parse:
|
|
||||||
|
|
||||||
- `open-session-reply.ok.json`
|
|
||||||
- `register-command-request.json`
|
|
||||||
- `on-data-change-event.json`
|
|
||||||
|
|
||||||
The fixtures use protobuf JSON field names and enum values. Contract tests parse
|
|
||||||
them with the generated C# types so schema drift is caught before client
|
|
||||||
generation work starts.
|
|
||||||
|
|
||||||
## Behavior Fixtures
|
|
||||||
|
|
||||||
Cross-language behavior fixtures live in
|
|
||||||
`clients/proto/fixtures/behavior`. The manifest
|
|
||||||
`clients/proto/fixtures/behavior/manifest.json` lists command replies, ordered
|
|
||||||
event stream samples, value conversion cases, status conversion cases, auth
|
|
||||||
error expectations, and timeout/cancel expectations.
|
|
||||||
|
|
||||||
The behavior fixtures let each generated client wrapper test the same
|
|
||||||
expectations without a live gateway. Protobuf message fixtures parse with the
|
|
||||||
generated types. Auth and timeout/cancel files describe wrapper behavior above
|
|
||||||
the generated transport layer, including credential redaction and the rule that
|
|
||||||
client cancellation does not abort an in-flight MXAccess COM call.
|
|
||||||
|
|
||||||
Run the focused validation script after changing these fixtures:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
scripts/validate-client-behavior-fixtures.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- [Protobuf Contracts](./Contracts.md)
|
|
||||||
- [Client Libraries Detailed Design](./client-libraries-design.md)
|
|
||||||
- [Client Behavior Fixtures](./ClientBehaviorFixtures.md)
|
|
||||||
- [Client Libraries Implementation Plan](./implementation-plan-clients.md)
|
|
||||||
- [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
|
|
||||||
@@ -16,7 +16,6 @@ Recommended layout:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
clients/dotnet/
|
clients/dotnet/
|
||||||
MxGateway.Client.sln
|
|
||||||
MxGateway.Client/
|
MxGateway.Client/
|
||||||
MxGateway.Client.csproj
|
MxGateway.Client.csproj
|
||||||
GatewayClient.cs
|
GatewayClient.cs
|
||||||
@@ -42,12 +41,6 @@ Target framework:
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
```
|
```
|
||||||
|
|
||||||
The scaffold uses a project reference to
|
|
||||||
`src/MxGateway.Contracts/MxGateway.Contracts.csproj` for generated protobuf and
|
|
||||||
gRPC types. `clients/dotnet/generated` remains reserved for client-local
|
|
||||||
generator output if the .NET client later needs to decouple from the contracts
|
|
||||||
project.
|
|
||||||
|
|
||||||
Expected packages:
|
Expected packages:
|
||||||
|
|
||||||
- `Grpc.Net.Client`
|
- `Grpc.Net.Client`
|
||||||
|
|||||||
@@ -34,13 +34,9 @@ SignalR circuit. Bootstrap is sufficient for a basic dashboard.
|
|||||||
|
|
||||||
## Hosting Model
|
## Hosting Model
|
||||||
|
|
||||||
The dashboard is hosted by `MxGateway.Server` alongside the gRPC API. When
|
The dashboard is hosted by `MxGateway.Server` alongside the gRPC API.
|
||||||
`MxGateway:Dashboard:Enabled` is `true`, `MapGatewayDashboard()` maps the
|
|
||||||
configured `Dashboard:PathBase` to the Blazor Server app and maps the login,
|
|
||||||
logout, and access-denied HTTP endpoints beside it. When dashboard hosting is
|
|
||||||
disabled, those routes are not mapped.
|
|
||||||
|
|
||||||
Endpoint layout:
|
Suggested endpoint layout:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/dashboard
|
/dashboard
|
||||||
@@ -49,7 +45,7 @@ Endpoint layout:
|
|||||||
/dashboard/workers
|
/dashboard/workers
|
||||||
/dashboard/events
|
/dashboard/events
|
||||||
/dashboard/settings
|
/dashboard/settings
|
||||||
/dashboard/_blazor
|
/_blazor
|
||||||
```
|
```
|
||||||
|
|
||||||
The app should redirect `/` to `/dashboard` only if the deployment wants the
|
The app should redirect `/` to `/dashboard` only if the deployment wants the
|
||||||
@@ -63,10 +59,9 @@ MxGateway.Server
|
|||||||
Components/
|
Components/
|
||||||
App.razor
|
App.razor
|
||||||
Routes.razor
|
Routes.razor
|
||||||
DashboardPageBase.cs
|
|
||||||
DashboardDisplay.cs
|
|
||||||
Layout/
|
Layout/
|
||||||
DashboardLayout.razor
|
DashboardLayout.razor
|
||||||
|
NavMenu.razor
|
||||||
Pages/
|
Pages/
|
||||||
DashboardHome.razor
|
DashboardHome.razor
|
||||||
SessionsPage.razor
|
SessionsPage.razor
|
||||||
@@ -74,21 +69,26 @@ MxGateway.Server
|
|||||||
WorkersPage.razor
|
WorkersPage.razor
|
||||||
EventsPage.razor
|
EventsPage.razor
|
||||||
SettingsPage.razor
|
SettingsPage.razor
|
||||||
Shared/
|
Components/
|
||||||
MetricCard.razor
|
MetricCard.razor
|
||||||
StatusBadge.razor
|
SessionTable.razor
|
||||||
|
WorkerTable.razor
|
||||||
|
EventRatePanel.razor
|
||||||
FaultList.razor
|
FaultList.razor
|
||||||
DashboardSnapshotService.cs
|
Services/
|
||||||
DashboardAuthorizationHandler.cs
|
DashboardSnapshotService.cs
|
||||||
DashboardAuthenticator.cs
|
DashboardUpdateHub.cs
|
||||||
DashboardSnapshot.cs
|
DashboardAuthorization.cs
|
||||||
DashboardSessionSummary.cs
|
Models/
|
||||||
DashboardWorkerSummary.cs
|
DashboardSnapshot.cs
|
||||||
DashboardMetricSummary.cs
|
SessionSummary.cs
|
||||||
|
WorkerSummary.cs
|
||||||
|
MetricSummary.cs
|
||||||
```
|
```
|
||||||
|
|
||||||
Blazor Server provides the SignalR circuit for UI updates. The implementation
|
`DashboardUpdateHub` here means an internal application update service, not a
|
||||||
does not add a separate public dashboard hub.
|
separate public SignalR hub unless implementation proves one is needed. Blazor
|
||||||
|
Server already uses SignalR for UI circuits.
|
||||||
|
|
||||||
## Dashboard Data Source
|
## Dashboard Data Source
|
||||||
|
|
||||||
@@ -105,12 +105,6 @@ Do not let Razor components directly mutate gateway session or worker objects.
|
|||||||
Create a small read-only dashboard service that projects gateway state into
|
Create a small read-only dashboard service that projects gateway state into
|
||||||
plain DTOs.
|
plain DTOs.
|
||||||
|
|
||||||
`GatewayMetrics.GetSnapshot()` is the metrics input for the first dashboard
|
|
||||||
projection. It carries current session and worker gauges, command and event
|
|
||||||
counters, queue depth, and fault totals. The dashboard reads that snapshot
|
|
||||||
instead of reading raw `Meter` instruments because exporter configuration is an
|
|
||||||
operations concern, not a UI dependency.
|
|
||||||
|
|
||||||
Suggested service:
|
Suggested service:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
@@ -137,7 +131,7 @@ gateway internals.
|
|||||||
|
|
||||||
Use Blazor Server component state updates for real-time dashboard refresh.
|
Use Blazor Server component state updates for real-time dashboard refresh.
|
||||||
|
|
||||||
Implemented pattern:
|
Recommended pattern:
|
||||||
|
|
||||||
1. Page/component subscribes to `WatchSnapshotsAsync`.
|
1. Page/component subscribes to `WatchSnapshotsAsync`.
|
||||||
2. Snapshot service emits updates from a bounded channel or timer.
|
2. Snapshot service emits updates from a bounded channel or timer.
|
||||||
@@ -147,8 +141,10 @@ Implemented pattern:
|
|||||||
|
|
||||||
Default update cadence:
|
Default update cadence:
|
||||||
|
|
||||||
|
- immediate update on session create/close/fault,
|
||||||
|
- immediate update on worker fault,
|
||||||
- periodic metrics refresh every 1 second,
|
- periodic metrics refresh every 1 second,
|
||||||
- event counters update on the next snapshot tick.
|
- event-rate windows updated every 1 second.
|
||||||
|
|
||||||
Avoid pushing every MXAccess data-change event to the dashboard. Aggregate event
|
Avoid pushing every MXAccess data-change event to the dashboard. Aggregate event
|
||||||
counts and rates instead.
|
counts and rates instead.
|
||||||
@@ -255,18 +251,19 @@ Do not show API key secrets or pepper values.
|
|||||||
|
|
||||||
## Authentication And Authorization
|
## Authentication And Authorization
|
||||||
|
|
||||||
Dashboard access uses the same API-key authentication model as gRPC where
|
Dashboard access should use the same API-key authentication model as gRPC where
|
||||||
practical.
|
practical.
|
||||||
|
|
||||||
Implemented v1 behavior:
|
Recommended v1 behavior:
|
||||||
|
|
||||||
|
- dashboard disabled by default unless configured,
|
||||||
- when enabled, require API key auth,
|
- when enabled, require API key auth,
|
||||||
- require `admin` scope for dashboard access,
|
- require `admin` scope for dashboard access,
|
||||||
- accept API key through a secure cookie established by a simple login form,
|
- accept API key through a secure cookie established by a simple login form, or
|
||||||
- do not put API keys in query strings,
|
through reverse-proxy/header configuration for local deployments,
|
||||||
- validate anti-forgery tokens for login and logout posts.
|
- do not put API keys in query strings.
|
||||||
|
|
||||||
The implementation path is:
|
Simplest implementation path:
|
||||||
|
|
||||||
1. Add `/dashboard/login`.
|
1. Add `/dashboard/login`.
|
||||||
2. User submits API key over HTTPS.
|
2. User submits API key over HTTPS.
|
||||||
@@ -278,13 +275,6 @@ The implementation path is:
|
|||||||
For local development, allow an explicit `Dashboard:AllowAnonymousLocalhost`
|
For local development, allow an explicit `Dashboard:AllowAnonymousLocalhost`
|
||||||
option. It must default to false.
|
option. It must default to false.
|
||||||
|
|
||||||
`DashboardAuthenticator` keeps API-key validation outside UI components. It
|
|
||||||
formats the submitted key as a bearer authorization header for
|
|
||||||
`IApiKeyVerifier`, rejects non-admin keys when `Dashboard:RequireAdminScope` is
|
|
||||||
enabled, and creates the dashboard cookie principal without storing raw API key
|
|
||||||
material. `DashboardAuthorizationHandler` enforces the cookie, admin-scope, and
|
|
||||||
explicit loopback bypass decisions for all protected dashboard routes.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Suggested configuration:
|
Suggested configuration:
|
||||||
@@ -318,9 +308,7 @@ Suggested configuration:
|
|||||||
|
|
||||||
## Styling
|
## Styling
|
||||||
|
|
||||||
The dashboard serves Bootstrap 5.3.3 assets from
|
Use Bootstrap utility classes and a small local stylesheet.
|
||||||
`src/MxGateway.Server/wwwroot/lib/bootstrap/` and local layout/status styling
|
|
||||||
from `src/MxGateway.Server/wwwroot/css/dashboard.css`.
|
|
||||||
|
|
||||||
Recommended visual language:
|
Recommended visual language:
|
||||||
|
|
||||||
@@ -361,18 +349,16 @@ Integration tests should verify:
|
|||||||
|
|
||||||
## Initial Implementation Slice
|
## Initial Implementation Slice
|
||||||
|
|
||||||
The first dashboard slice implements:
|
The first dashboard slice should implement:
|
||||||
|
|
||||||
1. Blazor Server hosting in `MxGateway.Server`.
|
1. Blazor Server hosting in `MxGateway.Server`.
|
||||||
2. local Bootstrap static assets.
|
2. Bootstrap static assets.
|
||||||
3. dashboard configuration binding.
|
3. dashboard configuration binding.
|
||||||
4. dashboard auth using API key login and HTTP-only cookie.
|
4. dashboard auth using API key login and HTTP-only cookie.
|
||||||
5. read-only `DashboardSnapshotService`.
|
5. read-only `DashboardSnapshotService`.
|
||||||
6. home page with metric cards.
|
6. home page with metric cards.
|
||||||
7. sessions page with active session table and session details.
|
7. sessions page with active session table.
|
||||||
8. workers page with worker table.
|
8. workers page with worker table.
|
||||||
9. events page with aggregate counters.
|
9. 1-second realtime refresh through Blazor Server.
|
||||||
10. settings page with redacted effective configuration.
|
10. redaction tests for secrets.
|
||||||
11. periodic realtime refresh through Blazor Server.
|
|
||||||
12. route-mapping tests, disabled-dashboard tests, auth tests, and snapshot
|
|
||||||
projection/redaction tests.
|
|
||||||
|
|||||||
+14
-197
@@ -64,8 +64,8 @@ MxGateway.Server
|
|||||||
Configuration
|
Configuration
|
||||||
Grpc
|
Grpc
|
||||||
MxAccessGatewayService
|
MxAccessGatewayService
|
||||||
MxAccessGrpcRequestValidator
|
RequestReplyMapper
|
||||||
MxAccessGrpcMapper
|
EventMapper
|
||||||
Dashboard
|
Dashboard
|
||||||
Pages
|
Pages
|
||||||
Components
|
Components
|
||||||
@@ -105,15 +105,6 @@ service MxAccessGateway {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`MxAccessGatewayService` implements these public RPCs in the gateway process.
|
|
||||||
It validates public requests with `MxAccessGrpcRequestValidator`, delegates
|
|
||||||
session lifecycle and command routing to `ISessionManager`, and maps worker
|
|
||||||
command replies and events through `MxAccessGrpcMapper`. Session lookup,
|
|
||||||
validation, and worker transport failures become gRPC status errors. MXAccess
|
|
||||||
method replies that reached the worker remain `MxCommandReply` payloads so
|
|
||||||
HRESULT values, status arrays, and method-specific reply fields survive
|
|
||||||
transport boundaries.
|
|
||||||
|
|
||||||
Add this later only after the command and event model is stable:
|
Add this later only after the command and event model is stable:
|
||||||
|
|
||||||
```protobuf
|
```protobuf
|
||||||
@@ -175,12 +166,6 @@ Behavior:
|
|||||||
`CloseSession` should be idempotent. Closing an already closed session should
|
`CloseSession` should be idempotent. Closing an already closed session should
|
||||||
return a successful close result with the final known state.
|
return a successful close result with the final known state.
|
||||||
|
|
||||||
`WorkerClient.ShutdownAsync` sends `WorkerShutdown`, waits for the worker read,
|
|
||||||
write, and heartbeat loops to stop, and waits for the launched worker process to
|
|
||||||
exit within the same shutdown timeout. If the pipe loops or process exit exceed
|
|
||||||
the timeout, the close operation fails with `ShutdownTimeout`; `GatewaySession`
|
|
||||||
then kills the worker process tree before surfacing the close failure.
|
|
||||||
|
|
||||||
### Invoke
|
### Invoke
|
||||||
|
|
||||||
`Invoke` forwards one MXAccess command to the worker that owns the session.
|
`Invoke` forwards one MXAccess command to the worker that owns the session.
|
||||||
@@ -212,23 +197,13 @@ accounting and a clear fan-out policy.
|
|||||||
Behavior:
|
Behavior:
|
||||||
|
|
||||||
1. Validate session id and authorize event access.
|
1. Validate session id and authorize event access.
|
||||||
2. Attach the single active subscriber lease for the session.
|
2. Attach a stream cursor to the session event channel.
|
||||||
3. Read worker events into a bounded public stream queue.
|
3. Send events in worker sequence order.
|
||||||
4. Send events in worker sequence order.
|
4. Stop on client cancellation, session close, or session fault.
|
||||||
5. Stop on client cancellation, session close, or session fault.
|
5. Emit a terminal status when the session faults if gRPC status alone cannot
|
||||||
6. Emit a terminal status when the session faults if gRPC status alone cannot
|
|
||||||
preserve the required details.
|
preserve the required details.
|
||||||
|
|
||||||
`EventStreamService` owns subscriber tracking and public stream backpressure.
|
The gateway must not reorder events from one worker.
|
||||||
The default policy allows one active subscriber per session. A second subscriber
|
|
||||||
is rejected with `EventSubscriberAlreadyActive`. Stream cancellation releases
|
|
||||||
the subscriber lease so a later stream can attach to the session.
|
|
||||||
|
|
||||||
The gateway must not reorder events from one worker. `EventStreamService` writes
|
|
||||||
mapped events to a bounded first-in, first-out queue and faults the session with
|
|
||||||
`EventQueueOverflow` if the queue fills. The gateway does not synthesize
|
|
||||||
`OperationComplete`; it forwards that family only when the worker reports a
|
|
||||||
native MXAccess `OperationComplete` event.
|
|
||||||
|
|
||||||
## Web Dashboard
|
## Web Dashboard
|
||||||
|
|
||||||
@@ -355,20 +330,6 @@ The worker remains authoritative for MXAccess handles. The gateway may keep a
|
|||||||
shadow state for diagnostics, but it must not invent, rewrite, or recycle
|
shadow state for diagnostics, but it must not invent, rewrite, or recycle
|
||||||
MXAccess handles.
|
MXAccess handles.
|
||||||
|
|
||||||
`SessionManager` owns the current in-memory session registry. It allocates a
|
|
||||||
session id, creates the worker pipe name and nonce, registers the session before
|
|
||||||
worker startup, and removes the session if startup fails. A successful
|
|
||||||
`OpenSession` attaches the ready `IWorkerClient` and transitions the session to
|
|
||||||
`Ready`.
|
|
||||||
|
|
||||||
Only `Ready` sessions accept command and event operations. `CloseSession` is
|
|
||||||
idempotent for sessions still known to the registry: the first close shuts down
|
|
||||||
the worker, and later closes return the final `Closed` state. Lease handling is
|
|
||||||
exposed as a session hook so a monitor can close expired sessions without
|
|
||||||
embedding lease policy in the worker client. Gateway shutdown walks the
|
|
||||||
registry, closes each known session, and kills a worker if graceful shutdown
|
|
||||||
fails.
|
|
||||||
|
|
||||||
## Worker Launch
|
## Worker Launch
|
||||||
|
|
||||||
The gateway should launch the worker using explicit configuration:
|
The gateway should launch the worker using explicit configuration:
|
||||||
@@ -399,15 +360,6 @@ Before launch, validate:
|
|||||||
- worker file version or product version is acceptable,
|
- worker file version or product version is acceptable,
|
||||||
- worker is expected to be x86.
|
- worker is expected to be x86.
|
||||||
|
|
||||||
`WorkerProcessLauncher` implements the first validation layer now: it resolves
|
|
||||||
the worker executable path, requires a `.exe`, validates the Windows Portable
|
|
||||||
Executable header, and verifies the configured processor architecture. It passes
|
|
||||||
only `--session-id`, `--pipe-name`, and `--protocol-version` on the command
|
|
||||||
line. The per-session nonce is set through `MXGATEWAY_WORKER_NONCE` so the
|
|
||||||
command line remains safe to log. Startup failures and startup timeouts kill and
|
|
||||||
dispose the worker process and the pre-created pipe reservation before the
|
|
||||||
session manager observes the failure.
|
|
||||||
|
|
||||||
## Worker IPC
|
## Worker IPC
|
||||||
|
|
||||||
The gateway creates the pipe server before launching the worker.
|
The gateway creates the pipe server before launching the worker.
|
||||||
@@ -450,7 +402,7 @@ session ids as protocol faults and close the session.
|
|||||||
|
|
||||||
`WorkerClient` is the gateway-side object that owns one worker connection.
|
`WorkerClient` is the gateway-side object that owns one worker connection.
|
||||||
|
|
||||||
Current public shape:
|
Suggested public shape:
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public interface IWorkerClient : IAsyncDisposable
|
public interface IWorkerClient : IAsyncDisposable
|
||||||
@@ -458,7 +410,6 @@ public interface IWorkerClient : IAsyncDisposable
|
|||||||
string SessionId { get; }
|
string SessionId { get; }
|
||||||
int? ProcessId { get; }
|
int? ProcessId { get; }
|
||||||
WorkerClientState State { get; }
|
WorkerClientState State { get; }
|
||||||
DateTimeOffset LastHeartbeatAt { get; }
|
|
||||||
|
|
||||||
Task StartAsync(CancellationToken cancellationToken);
|
Task StartAsync(CancellationToken cancellationToken);
|
||||||
Task<WorkerCommandReply> InvokeAsync(
|
Task<WorkerCommandReply> InvokeAsync(
|
||||||
@@ -478,17 +429,12 @@ Internally it owns:
|
|||||||
- pipe stream,
|
- pipe stream,
|
||||||
- read loop,
|
- read loop,
|
||||||
- write loop,
|
- write loop,
|
||||||
- outbound command/control channel serialized by the write loop,
|
- bounded outbound command/control channel,
|
||||||
- bounded inbound event channel,
|
- bounded inbound event channel,
|
||||||
- pending command dictionary keyed by correlation id,
|
- pending command dictionary keyed by correlation id,
|
||||||
- heartbeat monitor,
|
- heartbeat monitor,
|
||||||
- terminal fault source.
|
- terminal fault source.
|
||||||
|
|
||||||
`StartAsync` sends `GatewayHello`, verifies the `WorkerHello` protocol version
|
|
||||||
and nonce, waits for `WorkerReady`, and only then exposes `Ready` state. The
|
|
||||||
read loop starts after readiness so the handshake has a single owner for its
|
|
||||||
ordered frames.
|
|
||||||
|
|
||||||
### Read Loop
|
### Read Loop
|
||||||
|
|
||||||
The read loop:
|
The read loop:
|
||||||
@@ -521,11 +467,6 @@ It handles:
|
|||||||
The write loop should fail the session if a pipe write fails outside normal
|
The write loop should fail the session if a pipe write fails outside normal
|
||||||
shutdown.
|
shutdown.
|
||||||
|
|
||||||
During shutdown the worker client treats `WorkerShutdownAck` as the protocol
|
|
||||||
close signal, but the process handle remains authoritative for process lifetime.
|
|
||||||
The client waits for both the protocol close and process exit before reporting a
|
|
||||||
clean shutdown to `GatewaySession`.
|
|
||||||
|
|
||||||
## Command Correlation
|
## Command Correlation
|
||||||
|
|
||||||
Each command gets:
|
Each command gets:
|
||||||
@@ -605,8 +546,7 @@ worker MXAccess event
|
|||||||
-> worker outbound event queue
|
-> worker outbound event queue
|
||||||
-> worker pipe writer
|
-> worker pipe writer
|
||||||
-> gateway read loop
|
-> gateway read loop
|
||||||
-> worker client event queue
|
-> session event channel
|
||||||
-> EventStreamService bounded stream queue
|
|
||||||
-> gRPC StreamEvents
|
-> gRPC StreamEvents
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -620,15 +560,13 @@ The gateway should record:
|
|||||||
|
|
||||||
Default backpressure policy for parity testing should be fail-fast:
|
Default backpressure policy for parity testing should be fail-fast:
|
||||||
|
|
||||||
1. If the worker client event queue fills, fault the worker client.
|
1. If the session event channel fills, fault the session.
|
||||||
2. If the public stream queue fills, fault the gateway session.
|
|
||||||
2. Preserve the overflow details in logs and metrics.
|
2. Preserve the overflow details in logs and metrics.
|
||||||
3. Do not silently drop data-change events.
|
3. Do not silently drop data-change events.
|
||||||
|
|
||||||
Do not set a production event-rate target before measurement. `GatewayMetrics`
|
Do not set a production event-rate target before measurement. Emit event rate,
|
||||||
records received event counts by family, queue depth, stream disconnects, and
|
queue depth, stream send latency, and overflow metrics. Later production modes
|
||||||
overflow counts. Later production modes may support explicit coalescing by item
|
may support explicit coalescing by item handle as an opt-in behavior.
|
||||||
handle as an opt-in behavior.
|
|
||||||
|
|
||||||
The gateway should not synthesize `OperationComplete` from write completion,
|
The gateway should not synthesize `OperationComplete` from write completion,
|
||||||
command replies, ASB completion queues, or completion-only status frames. Forward
|
command replies, ASB completion queues, or completion-only status frames. Forward
|
||||||
@@ -651,39 +589,6 @@ The gateway should split the key into a stable key id and secret component,
|
|||||||
load the key record by id, hash the presented secret, and compare using a
|
load the key record by id, hash the presented secret, and compare using a
|
||||||
constant-time comparison.
|
constant-time comparison.
|
||||||
|
|
||||||
`ApiKeyParser` accepts only `authorization: Bearer mxgw_<key-id>_<secret>`.
|
|
||||||
Malformed headers fail before any database lookup. The parsed raw secret is
|
|
||||||
kept only long enough for `ApiKeySecretHasher` to compute an HMAC-SHA256 hash
|
|
||||||
using the configured `Authentication:PepperSecretName` lookup in application
|
|
||||||
configuration. The raw secret is not stored in the auth database, identity
|
|
||||||
model, logs, or verification result.
|
|
||||||
|
|
||||||
`ApiKeyVerifier` loads the stored key record by key id, rejects revoked keys,
|
|
||||||
hashes the presented secret, and compares the stored and presented hashes with
|
|
||||||
`CryptographicOperations.FixedTimeEquals`. A successful verification returns an
|
|
||||||
`ApiKeyIdentity` with key id, key prefix, display name, and scopes. Failure
|
|
||||||
results distinguish malformed credentials, missing keys, revoked keys, missing
|
|
||||||
pepper configuration, and hash mismatch for internal authorization handling.
|
|
||||||
|
|
||||||
`GatewayGrpcAuthorizationInterceptor` enforces this authentication model for
|
|
||||||
public gRPC calls. Missing, malformed, revoked, unknown, or mismatched keys fail
|
|
||||||
with `Unauthenticated`. Authenticated calls missing the scope required by the
|
|
||||||
RPC fail with `PermissionDenied`. The interceptor applies to unary calls and
|
|
||||||
server-streaming calls and stores the authenticated `ApiKeyIdentity` in
|
|
||||||
`IGatewayRequestIdentityAccessor` for the duration of the request handler.
|
|
||||||
`Authentication:Mode` set to `Disabled` bypasses API-key verification for local
|
|
||||||
development only.
|
|
||||||
|
|
||||||
Dashboard authentication reuses the API-key verifier and scope model. The
|
|
||||||
dashboard login endpoint accepts the key in a form post, checks `admin` scope
|
|
||||||
when `Dashboard:RequireAdminScope` is enabled, and signs in with the
|
|
||||||
`MxGateway.Dashboard` cookie scheme. The cookie is HTTP-only, secure, strict
|
|
||||||
SameSite, and scoped with the `__Host-MxGatewayDashboard` name. Logout clears
|
|
||||||
that cookie. Login and logout posts use anti-forgery validation, and dashboard
|
|
||||||
API keys are not accepted in query strings. `Dashboard:AllowAnonymousLocalhost`
|
|
||||||
allows only loopback requests to bypass the dashboard cookie requirement and
|
|
||||||
defaults to `false`.
|
|
||||||
|
|
||||||
Recommended scopes:
|
Recommended scopes:
|
||||||
|
|
||||||
- `session:open`
|
- `session:open`
|
||||||
@@ -703,44 +608,10 @@ gRPC admin API. It should initialize the auth database, create keys, list keys
|
|||||||
without secrets, revoke keys, rotate keys, and print raw secrets only once at
|
without secrets, revoke keys, rotate keys, and print raw secrets only once at
|
||||||
creation.
|
creation.
|
||||||
|
|
||||||
`MxGateway.Server` exposes local API-key administration as an `apikey`
|
|
||||||
subcommand before the web host starts:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MxGateway.Server apikey init-db --sqlite-path C:\ProgramData\MxGateway\gateway-auth.db
|
|
||||||
MxGateway.Server apikey create-key --key-id operator01 --display-name Operator --scopes session:open,events:read
|
|
||||||
MxGateway.Server apikey list-keys --json
|
|
||||||
MxGateway.Server apikey revoke-key --key-id operator01
|
|
||||||
MxGateway.Server apikey rotate-key --key-id operator01 --json
|
|
||||||
```
|
|
||||||
|
|
||||||
The subcommands accept `--sqlite-path`, `--pepper`, and `--json`. `--pepper`
|
|
||||||
sets the local `MxGateway:ApiKeyPepper` configuration value for the command
|
|
||||||
process; deployments should normally provide the pepper through the configured
|
|
||||||
secret source. `create-key` and `rotate-key` print the full raw API key exactly
|
|
||||||
once. `list-keys` never prints raw secrets or `secret_hash` values.
|
|
||||||
|
|
||||||
SQLite auth storage should use startup migrations with a `schema_version` table.
|
SQLite auth storage should use startup migrations with a `schema_version` table.
|
||||||
Migrations should run inside transactions and fail startup if the database
|
Migrations should run inside transactions and fail startup if the database
|
||||||
schema is newer than the running binary understands.
|
schema is newer than the running binary understands.
|
||||||
|
|
||||||
The v1 auth store uses `Microsoft.Data.Sqlite` and creates the
|
|
||||||
`schema_version`, `api_keys`, and `api_key_audit` tables through
|
|
||||||
`SqliteAuthStoreMigrator`. `AuthStoreMigrationHostedService` runs those
|
|
||||||
migrations at gateway startup when API-key authentication and
|
|
||||||
`Authentication:RunMigrationsOnStartup` are enabled. A database with a newer
|
|
||||||
schema version fails startup instead of being modified by an older gateway
|
|
||||||
binary.
|
|
||||||
|
|
||||||
`IApiKeyStore` reads stored key records and exposes an active-key lookup that
|
|
||||||
excludes rows with `revoked_utc` set. Hash verification belongs to the API-key
|
|
||||||
hashing layer, but the store preserves the `secret_hash` bytes, display name,
|
|
||||||
scopes, timestamps, and revocation state needed by that layer.
|
|
||||||
|
|
||||||
`IApiKeyAuditStore` appends audit events to `api_key_audit` and returns recent
|
|
||||||
events for diagnostics and future administrative tools. Audit records store key
|
|
||||||
ids and event metadata only; they do not store raw API key secrets.
|
|
||||||
|
|
||||||
Commands requiring authorization:
|
Commands requiring authorization:
|
||||||
|
|
||||||
- writes,
|
- writes,
|
||||||
@@ -749,20 +620,6 @@ Commands requiring authorization:
|
|||||||
- worker shutdown diagnostics,
|
- worker shutdown diagnostics,
|
||||||
- metadata queries if they expose sensitive plant structure.
|
- metadata queries if they expose sensitive plant structure.
|
||||||
|
|
||||||
Current gRPC scope mapping:
|
|
||||||
|
|
||||||
- `OpenSession` requires `session:open`.
|
|
||||||
- `CloseSession` requires `session:close`.
|
|
||||||
- `StreamEvents` and `DrainEvents` require `events:read`.
|
|
||||||
- read-style MXAccess commands such as `Register`, `AddItem`, `Advise`, and
|
|
||||||
`Ping` require `invoke:read`.
|
|
||||||
- `Write` and `Write2` require `invoke:write`.
|
|
||||||
- `WriteSecured`, `WriteSecured2`, and `AuthenticateUser` require
|
|
||||||
`invoke:secure`.
|
|
||||||
- metadata commands such as `ArchestrAUserToId`, `GetSessionState`, and
|
|
||||||
`GetWorkerInfo` require `metadata:read`.
|
|
||||||
- `ShutdownWorker` requires `admin`.
|
|
||||||
|
|
||||||
### Worker IPC
|
### Worker IPC
|
||||||
|
|
||||||
Named pipes should be local only. Pipe ACLs should restrict access to:
|
Named pipes should be local only. Pipe ACLs should restrict access to:
|
||||||
@@ -807,26 +664,6 @@ Metrics:
|
|||||||
|
|
||||||
Do not log credential values or full tag values by default.
|
Do not log credential values or full tag values by default.
|
||||||
|
|
||||||
The gateway registers `GatewayMetrics` as the in-process metrics foundation.
|
|
||||||
It emits .NET `Meter` instruments for collectors and keeps a
|
|
||||||
`GatewayMetricsSnapshot` for dashboard projection. The snapshot exists because
|
|
||||||
the dashboard needs current counters and queue depths without depending on a
|
|
||||||
specific metrics exporter.
|
|
||||||
|
|
||||||
HTTP request handling uses `UseGatewayRequestLoggingScope()` to attach common
|
|
||||||
structured log fields when request metadata is present:
|
|
||||||
|
|
||||||
- `SessionId`,
|
|
||||||
- `ClientIdentity`,
|
|
||||||
- `WorkerProcessId`,
|
|
||||||
- `CorrelationId`,
|
|
||||||
- `CommandMethod`.
|
|
||||||
|
|
||||||
`GatewayLogRedactor` redacts API key secrets and command values before they are
|
|
||||||
added to log state. Value logging remains opt-in and redacted by default so
|
|
||||||
secured writes, authentication commands, and ordinary tag values do not leak
|
|
||||||
through diagnostics.
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Suggested configuration shape:
|
Suggested configuration shape:
|
||||||
@@ -873,18 +710,6 @@ Suggested configuration shape:
|
|||||||
|
|
||||||
Do not scatter connection or path constants through implementation code.
|
Do not scatter connection or path constants through implementation code.
|
||||||
|
|
||||||
`MxGateway.Server` binds this section to `GatewayOptions` at startup and
|
|
||||||
registers validation with `ValidateOnStart()`. Startup fails before the gateway
|
|
||||||
begins serving traffic when required authentication settings are missing,
|
|
||||||
timeouts or queue sizes are not positive, dashboard settings are malformed, or
|
|
||||||
the configured worker protocol version does not match the contract version.
|
|
||||||
|
|
||||||
The gateway exposes read-only effective settings through
|
|
||||||
`IGatewayConfigurationProvider`. This projection is for dashboard settings and
|
|
||||||
diagnostics, so it redacts secret-related fields such as
|
|
||||||
`Authentication:PepperSecretName` and does not include raw API keys or key
|
|
||||||
material.
|
|
||||||
|
|
||||||
## Galaxy Repository Metadata
|
## Galaxy Repository Metadata
|
||||||
|
|
||||||
Galaxy hierarchy and tag metadata can be discovered through SQL Server when
|
Galaxy hierarchy and tag metadata can be discovered through SQL Server when
|
||||||
@@ -902,17 +727,9 @@ behavior unless an explicit non-parity backend is designed.
|
|||||||
Gateway tests should be able to run without installed MXAccess by using fake
|
Gateway tests should be able to run without installed MXAccess by using fake
|
||||||
workers and fake transports.
|
workers and fake transports.
|
||||||
|
|
||||||
Use `FakeWorkerHarness` for tests that need real gateway-to-worker framing,
|
|
||||||
handshake, command, event, fault, or malformed-protocol behavior without loading
|
|
||||||
MXAccess COM. See [Gateway Testing](./GatewayTesting.md) for the harness scope
|
|
||||||
and focused test commands.
|
|
||||||
|
|
||||||
Focused tests:
|
Focused tests:
|
||||||
|
|
||||||
- session state transitions,
|
- session state transitions,
|
||||||
- gRPC API-key authentication for unary and streaming calls,
|
|
||||||
- gRPC scope mapping for sessions, invokes, events, metadata, and admin
|
|
||||||
commands,
|
|
||||||
- worker startup failures,
|
- worker startup failures,
|
||||||
- protocol version mismatch,
|
- protocol version mismatch,
|
||||||
- malformed frame handling,
|
- malformed frame handling,
|
||||||
|
|||||||
@@ -189,8 +189,6 @@ Tests:
|
|||||||
|
|
||||||
Labels: `area:worker`, `type:feature`, `priority:p0`
|
Labels: `area:worker`, `type:feature`, `priority:p0`
|
||||||
|
|
||||||
Status: implemented.
|
|
||||||
|
|
||||||
Deliverables:
|
Deliverables:
|
||||||
|
|
||||||
- `Register`,
|
- `Register`,
|
||||||
@@ -218,8 +216,6 @@ Live tests:
|
|||||||
|
|
||||||
Labels: `area:worker`, `type:feature`, `priority:p0`
|
Labels: `area:worker`, `type:feature`, `priority:p0`
|
||||||
|
|
||||||
Status: implemented.
|
|
||||||
|
|
||||||
Deliverables:
|
Deliverables:
|
||||||
|
|
||||||
- `AddItem`,
|
- `AddItem`,
|
||||||
@@ -277,8 +273,6 @@ Live tests:
|
|||||||
|
|
||||||
Labels: `area:worker`, `type:feature`, `priority:p0`
|
Labels: `area:worker`, `type:feature`, `priority:p0`
|
||||||
|
|
||||||
Status: implemented.
|
|
||||||
|
|
||||||
Deliverables:
|
Deliverables:
|
||||||
|
|
||||||
- handlers for `OnDataChange`,
|
- handlers for `OnDataChange`,
|
||||||
@@ -453,3 +447,4 @@ Acceptance criteria:
|
|||||||
|
|
||||||
- each public method has planned parity fixture or documented gap,
|
- each public method has planned parity fixture or documented gap,
|
||||||
- gateway results preserve HRESULT/status/value/event shape.
|
- gateway results preserve HRESULT/status/value/event shape.
|
||||||
|
|
||||||
|
|||||||
@@ -26,33 +26,6 @@ Style guides:
|
|||||||
- [C# Style Guide](./style-guides/CSharpStyleGuide.md)
|
- [C# Style Guide](./style-guides/CSharpStyleGuide.md)
|
||||||
- [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
|
- [Protobuf Style Guide](./style-guides/ProtobufStyleGuide.md)
|
||||||
|
|
||||||
## Build And Test
|
|
||||||
|
|
||||||
Build the SDK-style worker project with the .NET SDK MSBuild entry point. The
|
|
||||||
project targets .NET Framework 4.8, but the SDK resolver comes from the .NET SDK
|
|
||||||
installation:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet msbuild src\MxGateway.Worker\MxGateway.Worker.csproj /restore /p:Configuration=Debug /p:Platform=x86
|
|
||||||
```
|
|
||||||
|
|
||||||
`docs/toolchain-links.md` records the Visual Studio MSBuild executable for
|
|
||||||
classic .NET Framework and COM interop builds:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
& "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe" src\MxGateway.Worker\MxGateway.Worker.csproj /p:Configuration=Debug /p:Platform=x86
|
|
||||||
```
|
|
||||||
|
|
||||||
Run the worker tests with the same platform target:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet test src\MxGateway.Worker.Tests\MxGateway.Worker.Tests.csproj -p:Platform=x86
|
|
||||||
```
|
|
||||||
|
|
||||||
The only MXAccess interop reference belongs in `MxGateway.Worker`. Gateway and
|
|
||||||
test projects may reference the worker project for metadata and scaffold tests,
|
|
||||||
but they must not reference `ArchestrA.MXAccess.dll` directly.
|
|
||||||
|
|
||||||
## Responsibilities
|
## Responsibilities
|
||||||
|
|
||||||
The worker owns:
|
The worker owns:
|
||||||
@@ -114,21 +87,6 @@ Startup sequence:
|
|||||||
If validation fails before MXAccess creation, exit quickly with a non-zero exit
|
If validation fails before MXAccess creation, exit quickly with a non-zero exit
|
||||||
code. If MXAccess creation fails, send `WorkerFault` when possible and exit.
|
code. If MXAccess creation fails, send `WorkerFault` when possible and exit.
|
||||||
|
|
||||||
The bootstrap layer returns structured exit codes before it creates pipes,
|
|
||||||
starts the STA, or touches MXAccess:
|
|
||||||
|
|
||||||
| Exit code | Name | Meaning |
|
|
||||||
|-----------|------|---------|
|
|
||||||
| `0` | `Success` | Required bootstrap options are valid. |
|
|
||||||
| `1` | `UnexpectedFailure` | A non-bootstrap exception reaches the process boundary. |
|
|
||||||
| `2` | `InvalidArguments` | Required arguments are missing or unknown arguments are present. |
|
|
||||||
| `3` | `InvalidProtocolVersion` | `--protocol-version` is not numeric or does not match the supported worker protocol. |
|
|
||||||
| `4` | `MissingNonce` | `MXGATEWAY_WORKER_NONCE` is absent or empty. |
|
|
||||||
|
|
||||||
Bootstrap logs use `WorkerConsoleLogger` key/value output. `WorkerLogRedactor`
|
|
||||||
redacts fields whose names indicate nonce, secret, password, token,
|
|
||||||
credential, or API key values before the message is written.
|
|
||||||
|
|
||||||
## Internal Components
|
## Internal Components
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -250,17 +208,6 @@ The loop should update a heartbeat timestamp after:
|
|||||||
- finishing a command,
|
- finishing a command,
|
||||||
- processing an MXAccess event.
|
- processing an MXAccess event.
|
||||||
|
|
||||||
`StaRuntime` implements this runtime boundary in the worker. It starts one
|
|
||||||
background thread named `MxGateway.Worker.STA`, sets it to `ApartmentState.STA`,
|
|
||||||
initializes COM through `StaComApartmentInitializer`, and runs
|
|
||||||
`StaMessagePump`. Commands are scheduled through `InvokeAsync`; the command
|
|
||||||
queue signals an `AutoResetEvent` so `MsgWaitForMultipleObjectsEx` can wake the
|
|
||||||
STA without busy-waiting. `LastActivityUtc` records pump, command, startup, and
|
|
||||||
shutdown activity so the future heartbeat/watchdog can report whether the STA
|
|
||||||
is still responsive. Shutdown marks the runtime as closing, wakes the pump,
|
|
||||||
rejects new commands, cancels queued work, uninitializes COM on the STA, and
|
|
||||||
waits for the thread to exit.
|
|
||||||
|
|
||||||
## COM Creation
|
## COM Creation
|
||||||
|
|
||||||
The MXAccess analysis source at `C:\Users\dohertj2\Desktop\mxaccess` identifies
|
The MXAccess analysis source at `C:\Users\dohertj2\Desktop\mxaccess` identifies
|
||||||
@@ -289,16 +236,6 @@ The worker should reference the interop assembly and instantiate
|
|||||||
`LMXProxyServerClass` on the dedicated STA thread. Keep the ProgID and assembly
|
`LMXProxyServerClass` on the dedicated STA thread. Keep the ProgID and assembly
|
||||||
path configurable for diagnostics, but this COM class is the v1 default.
|
path configurable for diagnostics, but this COM class is the v1 default.
|
||||||
|
|
||||||
`MxAccessStaSession` owns the initial COM creation path. It starts `StaRuntime`,
|
|
||||||
creates `LMXProxyServerClass` through `MxAccessComObjectFactory` on the STA,
|
|
||||||
attaches `MxAccessBaseEventSink`, and returns `WorkerReady` only after those
|
|
||||||
steps succeed. `MxAccessSession` keeps the raw COM object private, records the
|
|
||||||
STA managed thread id that created it, detaches the base event sink during
|
|
||||||
disposal, and releases the COM reference on the STA. After creation,
|
|
||||||
`MxAccessStaSession` owns a `StaCommandDispatcher` backed by
|
|
||||||
`MxAccessCommandExecutor`; `DispatchAsync` queues contract commands back to the
|
|
||||||
same STA instead of exposing the COM object to callers.
|
|
||||||
|
|
||||||
Creation rules:
|
Creation rules:
|
||||||
|
|
||||||
- Create COM object only on the STA.
|
- Create COM object only on the STA.
|
||||||
@@ -316,18 +253,6 @@ If COM creation fails, the worker should send a structured fault with:
|
|||||||
- worker process id,
|
- worker process id,
|
||||||
- session id.
|
- session id.
|
||||||
|
|
||||||
`WorkerPipeSession` maps startup exceptions from this path to
|
|
||||||
`WorkerFaultCategory.MxaccessCreationFailed`, includes the captured HRESULT
|
|
||||||
when the exception exposes one, and does not send `WorkerReady` after a failed
|
|
||||||
COM creation attempt.
|
|
||||||
|
|
||||||
After `WorkerReady`, `WorkerPipeSession` continues reading gateway frames for
|
|
||||||
the lifetime of the process. `WorkerCommand` frames are dispatched to
|
|
||||||
`MxAccessStaSession`, replies are written as `WorkerCommandReply`, and queued
|
|
||||||
worker events are drained after command replies. `WorkerShutdown` starts the
|
|
||||||
graceful shutdown path and returns `WorkerShutdownAck` only after the STA
|
|
||||||
cleanup path completes.
|
|
||||||
|
|
||||||
## Event Sink
|
## Event Sink
|
||||||
|
|
||||||
The worker must subscribe to every public MXAccess event family:
|
The worker must subscribe to every public MXAccess event family:
|
||||||
@@ -355,28 +280,9 @@ Event handling rules:
|
|||||||
- Enqueue to the outbound event queue.
|
- Enqueue to the outbound event queue.
|
||||||
- Return quickly to preserve message pumping.
|
- Return quickly to preserve message pumping.
|
||||||
|
|
||||||
`MxAccessBaseEventSink` implements the COM connection-point handlers and keeps
|
If event conversion throws, catch it inside the event handler, enqueue a
|
||||||
the handlers limited to event argument conversion plus enqueue. It uses
|
structured `WorkerFault` or diagnostic event, and keep the worker alive only if
|
||||||
`MxAccessEventMapper` to create `MxEvent` DTOs for `OnDataChange`,
|
the fault policy allows it.
|
||||||
`OnWriteComplete`, `OperationComplete`, and `OnBufferedDataChange`. The mapper
|
|
||||||
converts scalar and array values through `VariantConverter`, converts
|
|
||||||
`MXSTATUS_PROXY[]` through `MxStatusProxyConverter`, and maps installed
|
|
||||||
`MxDataType` values to the public protobuf enum while preserving the raw data
|
|
||||||
type on buffered events. `OperationComplete` is only emitted from the native
|
|
||||||
`OperationComplete` handler; write completion does not synthesize it.
|
|
||||||
|
|
||||||
`MxAccessEventQueue` is the bounded outbound event queue for one worker
|
|
||||||
session. It assigns the monotonic `WorkerSequence` and `WorkerTimestamp` when an
|
|
||||||
event is accepted, preserving the order in which MXAccess handlers enqueue
|
|
||||||
events. The default capacity is `10000`. When the queue reaches capacity it
|
|
||||||
records a `WorkerFaultCategory.QueueOverflow` fault and rejects further events.
|
|
||||||
The event handler catches conversion and enqueue failures, records the first
|
|
||||||
fault on the queue, and returns to the STA message pump instead of writing to
|
|
||||||
the pipe.
|
|
||||||
|
|
||||||
If event conversion throws, catch it inside the event handler, record a
|
|
||||||
structured `WorkerFault`, and keep the worker alive only if the fault policy
|
|
||||||
allows it.
|
|
||||||
|
|
||||||
## Command Queue
|
## Command Queue
|
||||||
|
|
||||||
@@ -443,60 +349,6 @@ Diagnostics:
|
|||||||
Implement method-specific dispatch instead of a generic string method invoker.
|
Implement method-specific dispatch instead of a generic string method invoker.
|
||||||
Parity tests need stable command-specific request and reply shapes.
|
Parity tests need stable command-specific request and reply shapes.
|
||||||
|
|
||||||
`MxAccessCommandExecutor` implements the first command pair:
|
|
||||||
|
|
||||||
- `Register` calls `LMXProxyServerClass.Register` with the requested client
|
|
||||||
name and preserves the returned server handle in both `ReturnValue` and
|
|
||||||
`RegisterReply.ServerHandle`.
|
|
||||||
- `Unregister` calls `LMXProxyServerClass.Unregister` with the requested server
|
|
||||||
handle. The reply has no method-specific payload because the public MXAccess
|
|
||||||
method returns `void`.
|
|
||||||
|
|
||||||
Both commands set `Hresult` to `0` only after the COM call returns normally.
|
|
||||||
COM exceptions flow through `StaCommandDispatcher`, which captures the thrown
|
|
||||||
HRESULT and converts the reply to `ProtocolStatusCode.MxaccessFailure`.
|
|
||||||
`MxAccessStaSession.GetRegisteredServerHandlesAsync` returns an STA-read
|
|
||||||
snapshot of tracked server handles for diagnostics and future cleanup logic.
|
|
||||||
|
|
||||||
`MxAccessCommandExecutor` also implements the item lifecycle commands:
|
|
||||||
|
|
||||||
- `AddItem` calls `LMXProxyServerClass.AddItem` with the requested server
|
|
||||||
handle and item definition. It preserves the returned item handle in both
|
|
||||||
`ReturnValue` and `AddItemReply.ItemHandle`.
|
|
||||||
- `AddItem2` calls `LMXProxyServerClass.AddItem2` with the requested server
|
|
||||||
handle, item definition, and context string. The context string is passed to
|
|
||||||
MXAccess exactly as received.
|
|
||||||
- `RemoveItem` calls `LMXProxyServerClass.RemoveItem` with the requested server
|
|
||||||
handle and item handle. The reply has no method-specific payload because the
|
|
||||||
public MXAccess method returns `void`.
|
|
||||||
|
|
||||||
The worker records item handles only after `AddItem` or `AddItem2` returns
|
|
||||||
normally, and removes item handles only after `RemoveItem` returns normally.
|
|
||||||
The registry does not prevalidate server or item handles, so invalid and
|
|
||||||
cross-server handle behavior remains owned by MXAccess. COM exceptions continue
|
|
||||||
through `StaCommandDispatcher`, which preserves the HRESULT and leaves
|
|
||||||
diagnostic registry state unchanged for failed cleanup calls.
|
|
||||||
|
|
||||||
`MxAccessCommandExecutor` implements advice lifecycle commands on the same STA
|
|
||||||
path:
|
|
||||||
|
|
||||||
- `Advise` calls `LMXProxyServerClass.Advise` with the requested server handle
|
|
||||||
and item handle.
|
|
||||||
- `AdviseSupervisory` calls `LMXProxyServerClass.AdviseSupervisory` with the
|
|
||||||
requested server handle and item handle. This remains a distinct command from
|
|
||||||
plain `Advise` even though observed scalar captures share the same lower-level
|
|
||||||
subscription body.
|
|
||||||
- `UnAdvise` calls `LMXProxyServerClass.UnAdvise` with the requested server
|
|
||||||
handle and item handle.
|
|
||||||
|
|
||||||
The worker records plain and supervisory advice separately only after the COM
|
|
||||||
call returns normally. Successful `UnAdvise` removes all tracked advice for the
|
|
||||||
server and item pair because the public MXAccess cleanup method has no plain
|
|
||||||
versus supervisory selector. Successful `RemoveItem` and `Unregister` also clear
|
|
||||||
related advice state from the worker registry. Failed advice and cleanup calls
|
|
||||||
leave registry state unchanged so diagnostics continue to reflect the last
|
|
||||||
successful MXAccess-owned state transition.
|
|
||||||
|
|
||||||
## Handle Registry
|
## Handle Registry
|
||||||
|
|
||||||
The worker should track MXAccess state for diagnostics and cleanup, while still
|
The worker should track MXAccess state for diagnostics and cleanup, while still
|
||||||
@@ -517,13 +369,6 @@ Rules:
|
|||||||
|
|
||||||
- Do not invent handles.
|
- Do not invent handles.
|
||||||
- Do not rewrite handles returned by MXAccess.
|
- Do not rewrite handles returned by MXAccess.
|
||||||
- Record server handles only after `Register` succeeds.
|
|
||||||
- Remove server handles only after `Unregister` succeeds.
|
|
||||||
- Record item handles only after `AddItem` or `AddItem2` succeeds.
|
|
||||||
- Remove item handles only after `RemoveItem` succeeds.
|
|
||||||
- Record advice state only after `Advise` or `AdviseSupervisory` succeeds.
|
|
||||||
- Remove advice state only after `UnAdvise`, `RemoveItem`, or `Unregister`
|
|
||||||
succeeds.
|
|
||||||
- Preserve invalid-handle behavior from MXAccess.
|
- Preserve invalid-handle behavior from MXAccess.
|
||||||
- Preserve cross-server handle behavior from MXAccess.
|
- Preserve cross-server handle behavior from MXAccess.
|
||||||
- Use registry state for cleanup and diagnostics, not semantic correction.
|
- Use registry state for cleanup and diagnostics, not semantic correction.
|
||||||
@@ -625,19 +470,13 @@ Do not drop or coalesce events in v1.
|
|||||||
|
|
||||||
## Heartbeat And Watchdog
|
## Heartbeat And Watchdog
|
||||||
|
|
||||||
`WorkerPipeSession` starts the heartbeat loop after the gateway validates
|
The worker heartbeat should prove that:
|
||||||
`WorkerHello` and receives `WorkerReady`. Heartbeats continue until
|
|
||||||
`WorkerShutdown`, cancellation, or a pipe/protocol failure stops the session.
|
|
||||||
The loop uses `WorkerPipeSessionOptions.HeartbeatInterval`; the default matches
|
|
||||||
the gateway worker heartbeat interval.
|
|
||||||
|
|
||||||
The worker heartbeat proves that:
|
|
||||||
|
|
||||||
- pipe writer is alive,
|
- pipe writer is alive,
|
||||||
- worker host is alive,
|
- worker host is alive,
|
||||||
- STA has recently pumped or completed work.
|
- STA has recently pumped or completed work.
|
||||||
|
|
||||||
Heartbeat payload includes:
|
Heartbeat payload should include:
|
||||||
|
|
||||||
- worker process id,
|
- worker process id,
|
||||||
- session id,
|
- session id,
|
||||||
@@ -648,19 +487,13 @@ Heartbeat payload includes:
|
|||||||
- event sequence,
|
- event sequence,
|
||||||
- current command correlation id if any.
|
- current command correlation id if any.
|
||||||
|
|
||||||
`MxAccessStaSession.CaptureHeartbeat()` reads `StaRuntime.LastActivityUtc` and
|
The STA watchdog should warn when:
|
||||||
`StaCommandDispatcher` queue state without touching the raw MXAccess COM object
|
|
||||||
outside the STA. Event queue depth and event sequence are reported as zero until
|
|
||||||
the event queue implementation owns those counters.
|
|
||||||
|
|
||||||
The STA watchdog currently emits a `WorkerFault` with
|
- one command exceeds its expected duration,
|
||||||
`WorkerFaultCategory.StaHung` when `LastStaActivityUtc` is older than
|
- the STA has not pumped messages within the heartbeat grace period,
|
||||||
`WorkerPipeSessionOptions.HeartbeatGrace`. The fault includes the current
|
- event queue depth remains high.
|
||||||
command correlation id when a command is active. Command duration and high event
|
|
||||||
queue depth remain observable through heartbeat fields until dedicated
|
The worker can report the problem, but the gateway owns the final kill decision.
|
||||||
thresholds own those warnings. The worker reports stale STA activity, but the
|
|
||||||
gateway owns the final kill decision through its existing heartbeat and worker
|
|
||||||
lifecycle policy.
|
|
||||||
|
|
||||||
## Shutdown
|
## Shutdown
|
||||||
|
|
||||||
@@ -682,29 +515,6 @@ Graceful shutdown sequence:
|
|||||||
If shutdown wedges, the gateway kills the process. The worker should be written
|
If shutdown wedges, the gateway kills the process. The worker should be written
|
||||||
so process kill does not corrupt other sessions.
|
so process kill does not corrupt other sessions.
|
||||||
|
|
||||||
`MxAccessStaSession.ShutdownGracefullyAsync` implements the current cleanup
|
|
||||||
path. It first calls `StaCommandDispatcher.RequestShutdown()` so new commands
|
|
||||||
are rejected and queued commands that have not started receive
|
|
||||||
`ProtocolStatusCode.WorkerUnavailable`. The command already executing on the
|
|
||||||
STA is allowed to finish until the shutdown grace period expires.
|
|
||||||
|
|
||||||
After command dispatch is closed, cleanup runs on the STA in MXAccess handle
|
|
||||||
order:
|
|
||||||
|
|
||||||
1. one `UnAdvise` call per advised server/item pair,
|
|
||||||
2. `RemoveItem` for active item handles,
|
|
||||||
3. `Unregister` for active server handles,
|
|
||||||
4. event sink detach,
|
|
||||||
5. COM release.
|
|
||||||
|
|
||||||
Each cleanup call is best effort. A failed cleanup operation is recorded as an
|
|
||||||
`MxAccessShutdownFailure`, logged by `WorkerPipeSession`, and does not prevent
|
|
||||||
later cleanup calls from running. A shutdown with cleanup failures still returns
|
|
||||||
`WorkerShutdownAck` with `ProtocolStatusCode.Ok` because the worker reached the
|
|
||||||
controlled release path. If the grace period expires before cleanup can run or
|
|
||||||
finish, the worker reports `WorkerFaultCategory.ShutdownTimeout` when possible
|
|
||||||
and relies on the gateway to kill the process.
|
|
||||||
|
|
||||||
## Fault Handling
|
## Fault Handling
|
||||||
|
|
||||||
Worker fault categories:
|
Worker fault categories:
|
||||||
@@ -802,18 +612,6 @@ Live MXAccess tests:
|
|||||||
|
|
||||||
Live tests should be opt-in and clearly marked because they depend on installed
|
Live tests should be opt-in and clearly marked because they depend on installed
|
||||||
MXAccess COM and provider state.
|
MXAccess COM and provider state.
|
||||||
The worker test suite uses `MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1` for these
|
|
||||||
tests. `AddItem` uses `TestChildObject.TestInt` by default and accepts an
|
|
||||||
override through `MXGATEWAY_LIVE_MXACCESS_ITEM`; `AddItem2` uses the captured
|
|
||||||
parity fixture shape `AddItem2("TestInt", "TestChildObject")`.
|
|
||||||
|
|
||||||
`WorkerLiveMxAccessSmokeTests` in `src/MxGateway.IntegrationTests/` uses the
|
|
||||||
same opt-in variable for the gateway-to-worker live smoke. It launches the x86
|
|
||||||
worker through `WorkerProcessLauncher`, opens a gateway session, runs
|
|
||||||
`Register`, `AddItem`, and `Advise`, waits for one `OnDataChange`, and closes
|
|
||||||
the session. The smoke accepts `MXGATEWAY_LIVE_MXACCESS_WORKER_EXE` for a
|
|
||||||
non-default worker executable path and
|
|
||||||
`MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS` for the bounded event wait.
|
|
||||||
|
|
||||||
## Initial Implementation Slice
|
## Initial Implementation Slice
|
||||||
|
|
||||||
|
|||||||
+11
-59
@@ -47,8 +47,6 @@ Detailed follow-up docs:
|
|||||||
security, observability, and test strategy.
|
security, observability, and test strategy.
|
||||||
- `docs/WorkerFrameProtocol.md` covers the gateway-side named-pipe frame
|
- `docs/WorkerFrameProtocol.md` covers the gateway-side named-pipe frame
|
||||||
reader/writer and `WorkerEnvelope` validation rules.
|
reader/writer and `WorkerEnvelope` validation rules.
|
||||||
- `docs/WorkerProcessLauncher.md` covers worker executable validation, process
|
|
||||||
launch arguments, nonce handling, and startup cleanup behavior.
|
|
||||||
- `docs/mxaccess-worker-instance-design.md` covers each .NET Framework 4.8 x86
|
- `docs/mxaccess-worker-instance-design.md` covers each .NET Framework 4.8 x86
|
||||||
MXAccess worker instance, including STA ownership, message pumping, COM
|
MXAccess worker instance, including STA ownership, message pumping, COM
|
||||||
lifetime, command dispatch, event sinks, conversion, and shutdown.
|
lifetime, command dispatch, event sinks, conversion, and shutdown.
|
||||||
@@ -101,31 +99,6 @@ Responsibilities:
|
|||||||
|
|
||||||
The gateway must never instantiate or call MXAccess directly.
|
The gateway must never instantiate or call MXAccess directly.
|
||||||
|
|
||||||
The gateway observability foundation lives in `MxGateway.Server.Diagnostics`
|
|
||||||
and `MxGateway.Server.Metrics`. Structured logging scopes carry session,
|
|
||||||
worker, correlation, command, and client identity fields with redaction applied
|
|
||||||
before values enter log state. `GatewayMetrics` exposes counters, gauges, and
|
|
||||||
histograms through .NET `Meter` and a snapshot API that dashboard services can
|
|
||||||
project without binding to a metrics exporter.
|
|
||||||
`DashboardSnapshotService` projects sessions, workers, metrics, faults, and
|
|
||||||
effective configuration into immutable DTOs for read-only dashboard rendering.
|
|
||||||
The Blazor Server dashboard renders those snapshots at `/dashboard`,
|
|
||||||
`/dashboard/sessions`, `/dashboard/workers`, `/dashboard/events`, and
|
|
||||||
`/dashboard/settings`. Components subscribe to
|
|
||||||
`IDashboardSnapshotService.WatchSnapshotsAsync()` and update on the configured
|
|
||||||
snapshot interval without mutating session or worker state. The dashboard uses
|
|
||||||
local Bootstrap CSS and JavaScript plus a small local stylesheet; it does not
|
|
||||||
use a Blazor UI component library.
|
|
||||||
|
|
||||||
Dashboard routes use the same API-key verifier as gRPC. `/dashboard/login`
|
|
||||||
accepts the API key in a form body, validates the configured `admin` scope,
|
|
||||||
and issues an HTTP-only secure cookie for subsequent dashboard requests.
|
|
||||||
`/dashboard/logout` clears that cookie. Login and logout posts validate
|
|
||||||
anti-forgery tokens, and API keys are never accepted through query strings.
|
|
||||||
`Dashboard:AllowAnonymousLocalhost` can bypass the cookie requirement for
|
|
||||||
loopback requests only when explicitly enabled. Setting
|
|
||||||
`MxGateway:Dashboard:Enabled` to `false` leaves the dashboard routes unmapped.
|
|
||||||
|
|
||||||
### Worker Process
|
### Worker Process
|
||||||
|
|
||||||
Runtime:
|
Runtime:
|
||||||
@@ -536,7 +509,11 @@ Worker policy:
|
|||||||
|
|
||||||
- bounded outbound event channel,
|
- bounded outbound event channel,
|
||||||
- never block MXAccess event handler on pipe writes,
|
- never block MXAccess event handler on pipe writes,
|
||||||
- fail the worker session when the outbound channel is full.
|
- if the outbound channel is full, apply configured policy:
|
||||||
|
- disconnect session,
|
||||||
|
- drop oldest low-priority data-change events,
|
||||||
|
- coalesce data changes by item handle,
|
||||||
|
- or block briefly then fault.
|
||||||
|
|
||||||
For full parity testing, default should be fail-fast rather than silent drop.
|
For full parity testing, default should be fail-fast rather than silent drop.
|
||||||
For production high-rate telemetry, add explicit coalescing modes.
|
For production high-rate telemetry, add explicit coalescing modes.
|
||||||
@@ -545,15 +522,9 @@ Gateway policy:
|
|||||||
|
|
||||||
- one event sequencer per session,
|
- one event sequencer per session,
|
||||||
- preserve per-session event order,
|
- preserve per-session event order,
|
||||||
- allow one active client event subscriber per session,
|
- support multiple client event subscribers only if explicitly required,
|
||||||
- reject a second subscriber with a clear session error,
|
- apply backpressure from slow gRPC streams,
|
||||||
- use a bounded `EventStreamService` queue between worker events and gRPC
|
- disconnect or coalesce according to client-selected mode.
|
||||||
writes,
|
|
||||||
- fault the session when the bounded stream queue overflows,
|
|
||||||
- detach the subscriber when the stream is canceled.
|
|
||||||
|
|
||||||
The gateway forwards only events reported by the worker. It does not synthesize
|
|
||||||
`OperationComplete` from write completion, command replies, or status frames.
|
|
||||||
|
|
||||||
## Isolation And Fault Handling
|
## Isolation And Fault Handling
|
||||||
|
|
||||||
@@ -586,13 +557,9 @@ Because each client owns one worker, a crash or leak affects only that session.
|
|||||||
External gateway:
|
External gateway:
|
||||||
|
|
||||||
- use TLS for remote gRPC if crossing machine boundaries,
|
- use TLS for remote gRPC if crossing machine boundaries,
|
||||||
- authenticate v1 gRPC clients with `authorization: Bearer
|
- authenticate clients with Windows auth, mTLS, or a deployment-specific token,
|
||||||
mxgw_<key-id>_<secret>` API-key metadata,
|
- authorize access to commands that can write, authenticate users, or alter
|
||||||
- reject missing or invalid API keys with gRPC `Unauthenticated`,
|
runtime state.
|
||||||
- reject valid keys that lack the required session, invoke, event, metadata, or
|
|
||||||
admin scope with gRPC `PermissionDenied`,
|
|
||||||
- authorize access to commands that can write, authenticate users, expose
|
|
||||||
metadata, stream events, or alter runtime state.
|
|
||||||
|
|
||||||
Internal worker IPC:
|
Internal worker IPC:
|
||||||
|
|
||||||
@@ -819,12 +786,6 @@ Core operations:
|
|||||||
- track worker state,
|
- track worker state,
|
||||||
- close or kill worker.
|
- close or kill worker.
|
||||||
|
|
||||||
The gateway implementation keeps sessions in an in-memory `SessionRegistry`
|
|
||||||
keyed by session id. `SessionManager` owns the state machine, creates
|
|
||||||
per-session pipe names and nonces, starts the worker through the worker-client
|
|
||||||
factory, gates commands to `Ready` sessions, exposes lease-close hooks, and
|
|
||||||
cleans up workers during gateway shutdown.
|
|
||||||
|
|
||||||
State machine:
|
State machine:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -872,15 +833,6 @@ The gRPC layer should be thin:
|
|||||||
Avoid embedding MXAccess-specific business logic in gRPC handlers. Keep the
|
Avoid embedding MXAccess-specific business logic in gRPC handlers. Keep the
|
||||||
translation code testable.
|
translation code testable.
|
||||||
|
|
||||||
The gateway maps `MxAccessGateway` to `MxAccessGatewayService`. The service
|
|
||||||
implements `OpenSession`, `CloseSession`, `Invoke`, and `StreamEvents` by
|
|
||||||
validating public requests, delegating session work to `ISessionManager`, and
|
|
||||||
using explicit mapper code for public-to-worker commands and worker replies.
|
|
||||||
`StreamEvents` delegates subscriber ownership, ordering, and backpressure to
|
|
||||||
`EventStreamService`. Missing sessions and transport failures return gRPC
|
|
||||||
status errors; worker command replies preserve MXAccess HRESULT and status
|
|
||||||
details in the public reply.
|
|
||||||
|
|
||||||
## C# Worker Versus C++ Worker
|
## C# Worker Versus C++ Worker
|
||||||
|
|
||||||
Start with a C# .NET Framework 4.8 x86 worker.
|
Start with a C# .NET Framework 4.8 x86 worker.
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[switch]$Check
|
|
||||||
)
|
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
|
||||||
$protoRoot = Join-Path $repoRoot "src/MxGateway.Contracts/Protos"
|
|
||||||
$manifestPath = Join-Path $repoRoot "clients/proto/proto-inputs.json"
|
|
||||||
$descriptorPath = Join-Path $repoRoot "clients/proto/descriptors/mxaccessgw-client-v1.protoset"
|
|
||||||
|
|
||||||
function Resolve-Protoc {
|
|
||||||
$pathCommand = Get-Command "protoc.exe" -ErrorAction SilentlyContinue
|
|
||||||
if ($null -ne $pathCommand) {
|
|
||||||
return $pathCommand.Source
|
|
||||||
}
|
|
||||||
|
|
||||||
$documentedPath = Join-Path $env:LOCALAPPDATA "Microsoft/WinGet/Packages/Google.Protobuf_Microsoft.Winget.Source_8wekyb3d8bbwe/bin/protoc.exe"
|
|
||||||
if (Test-Path $documentedPath) {
|
|
||||||
return $documentedPath
|
|
||||||
}
|
|
||||||
|
|
||||||
throw "Could not find protoc.exe. See docs/toolchain-links.md for the documented protobuf toolchain path."
|
|
||||||
}
|
|
||||||
|
|
||||||
function Ensure-Directory {
|
|
||||||
param([string]$Path)
|
|
||||||
|
|
||||||
if (-not (Test-Path $Path)) {
|
|
||||||
New-Item -ItemType Directory -Path $Path | Out-Null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Compare-FileBytes {
|
|
||||||
param(
|
|
||||||
[string]$ExpectedPath,
|
|
||||||
[string]$ActualPath
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not (Test-Path $ExpectedPath)) {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
$expected = [System.IO.File]::ReadAllBytes($ExpectedPath)
|
|
||||||
$actual = [System.IO.File]::ReadAllBytes($ActualPath)
|
|
||||||
if ($expected.Length -ne $actual.Length) {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
for ($index = 0; $index -lt $expected.Length; $index++) {
|
|
||||||
if ($expected[$index] -ne $actual[$index]) {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
|
|
||||||
$manifest = Get-Content -Raw $manifestPath | ConvertFrom-Json
|
|
||||||
|
|
||||||
Ensure-Directory (Split-Path $descriptorPath -Parent)
|
|
||||||
foreach ($output in $manifest.generatedOutputs.PSObject.Properties.Value) {
|
|
||||||
Ensure-Directory (Join-Path $repoRoot $output)
|
|
||||||
}
|
|
||||||
|
|
||||||
$protoc = Resolve-Protoc
|
|
||||||
$outputPath = $descriptorPath
|
|
||||||
if ($Check) {
|
|
||||||
$outputPath = Join-Path ([System.IO.Path]::GetTempPath()) ("mxaccessgw-client-v1-" + [System.Guid]::NewGuid().ToString("N") + ".protoset")
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
& $protoc `
|
|
||||||
"--proto_path=$protoRoot" `
|
|
||||||
"--include_imports" `
|
|
||||||
"--include_source_info" `
|
|
||||||
"--descriptor_set_out=$outputPath" `
|
|
||||||
"mxaccess_gateway.proto" `
|
|
||||||
"mxaccess_worker.proto"
|
|
||||||
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "protoc failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Check -and -not (Compare-FileBytes -ExpectedPath $descriptorPath -ActualPath $outputPath)) {
|
|
||||||
throw "Client proto descriptor is stale. Run scripts/publish-client-proto-inputs.ps1 and commit the updated descriptor."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if ($Check -and (Test-Path $outputPath)) {
|
|
||||||
Remove-Item -LiteralPath $outputPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[switch]$NoBuild
|
|
||||||
)
|
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
|
||||||
$ErrorActionPreference = "Stop"
|
|
||||||
|
|
||||||
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
|
|
||||||
$testProject = Join-Path $repoRoot "src/MxGateway.Tests/MxGateway.Tests.csproj"
|
|
||||||
$arguments = @(
|
|
||||||
"test",
|
|
||||||
$testProject,
|
|
||||||
"--filter",
|
|
||||||
"ClientBehaviorFixtureTests"
|
|
||||||
)
|
|
||||||
|
|
||||||
if ($NoBuild) {
|
|
||||||
$arguments += "--no-build"
|
|
||||||
}
|
|
||||||
|
|
||||||
& dotnet @arguments
|
|
||||||
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "Client behavior fixture validation failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,6 @@ namespace MxGateway.Contracts;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class GatewayContractInfo
|
public static class GatewayContractInfo
|
||||||
{
|
{
|
||||||
public const uint GatewayProtocolVersion = 1;
|
|
||||||
|
|
||||||
public const uint WorkerProtocolVersion = 1;
|
public const uint WorkerProtocolVersion = 1;
|
||||||
|
|
||||||
public const string DefaultBackendName = "mxaccess-worker";
|
public const string DefaultBackendName = "mxaccess-worker";
|
||||||
|
|||||||
@@ -30,282 +30,282 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
"ChFyZXF1ZXN0ZWRfYmFja2VuZBgBIAEoCRIbChNjbGllbnRfc2Vzc2lvbl9u",
|
"ChFyZXF1ZXN0ZWRfYmFja2VuZBgBIAEoCRIbChNjbGllbnRfc2Vzc2lvbl9u",
|
||||||
"YW1lGAIgASgJEh0KFWNsaWVudF9jb3JyZWxhdGlvbl9pZBgDIAEoCRIyCg9j",
|
"YW1lGAIgASgJEh0KFWNsaWVudF9jb3JyZWxhdGlvbl9pZBgDIAEoCRIyCg9j",
|
||||||
"b21tYW5kX3RpbWVvdXQYBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRp",
|
"b21tYW5kX3RpbWVvdXQYBCABKAsyGS5nb29nbGUucHJvdG9idWYuRHVyYXRp",
|
||||||
"b24iqgIKEE9wZW5TZXNzaW9uUmVwbHkSEgoKc2Vzc2lvbl9pZBgBIAEoCRIU",
|
"b24iiAIKEE9wZW5TZXNzaW9uUmVwbHkSEgoKc2Vzc2lvbl9pZBgBIAEoCRIU",
|
||||||
"CgxiYWNrZW5kX25hbWUYAiABKAkSGQoRd29ya2VyX3Byb2Nlc3NfaWQYAyAB",
|
"CgxiYWNrZW5kX25hbWUYAiABKAkSGQoRd29ya2VyX3Byb2Nlc3NfaWQYAyAB",
|
||||||
"KAUSHwoXd29ya2VyX3Byb3RvY29sX3ZlcnNpb24YBCABKA0SFAoMY2FwYWJp",
|
"KAUSHwoXd29ya2VyX3Byb3RvY29sX3ZlcnNpb24YBCABKA0SFAoMY2FwYWJp",
|
||||||
"bGl0aWVzGAUgAygJEjoKF2RlZmF1bHRfY29tbWFuZF90aW1lb3V0GAYgASgL",
|
"bGl0aWVzGAUgAygJEjoKF2RlZmF1bHRfY29tbWFuZF90aW1lb3V0GAYgASgL",
|
||||||
"MhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEjwKD3Byb3RvY29sX3N0YXR1",
|
"MhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uEjwKD3Byb3RvY29sX3N0YXR1",
|
||||||
"cxgHIAEoCzIjLm14YWNjZXNzX2dhdGV3YXkudjEuUHJvdG9jb2xTdGF0dXMS",
|
"cxgHIAEoCzIjLm14YWNjZXNzX2dhdGV3YXkudjEuUHJvdG9jb2xTdGF0dXMi",
|
||||||
"IAoYZ2F0ZXdheV9wcm90b2NvbF92ZXJzaW9uGAggASgNIkgKE0Nsb3NlU2Vz",
|
"SAoTQ2xvc2VTZXNzaW9uUmVxdWVzdBISCgpzZXNzaW9uX2lkGAEgASgJEh0K",
|
||||||
"c2lvblJlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIdChVjbGllbnRfY29y",
|
"FWNsaWVudF9jb3JyZWxhdGlvbl9pZBgCIAEoCSKdAQoRQ2xvc2VTZXNzaW9u",
|
||||||
"cmVsYXRpb25faWQYAiABKAkinQEKEUNsb3NlU2Vzc2lvblJlcGx5EhIKCnNl",
|
"UmVwbHkSEgoKc2Vzc2lvbl9pZBgBIAEoCRI2CgtmaW5hbF9zdGF0ZRgCIAEo",
|
||||||
"c3Npb25faWQYASABKAkSNgoLZmluYWxfc3RhdGUYAiABKA4yIS5teGFjY2Vz",
|
"DjIhLm14YWNjZXNzX2dhdGV3YXkudjEuU2Vzc2lvblN0YXRlEjwKD3Byb3Rv",
|
||||||
"c19nYXRld2F5LnYxLlNlc3Npb25TdGF0ZRI8Cg9wcm90b2NvbF9zdGF0dXMY",
|
"Y29sX3N0YXR1cxgDIAEoCzIjLm14YWNjZXNzX2dhdGV3YXkudjEuUHJvdG9j",
|
||||||
"AyABKAsyIy5teGFjY2Vzc19nYXRld2F5LnYxLlByb3RvY29sU3RhdHVzIkgK",
|
"b2xTdGF0dXMiSAoTU3RyZWFtRXZlbnRzUmVxdWVzdBISCgpzZXNzaW9uX2lk",
|
||||||
"E1N0cmVhbUV2ZW50c1JlcXVlc3QSEgoKc2Vzc2lvbl9pZBgBIAEoCRIdChVh",
|
"GAEgASgJEh0KFWFmdGVyX3dvcmtlcl9zZXF1ZW5jZRgCIAEoBCJ2ChBNeENv",
|
||||||
"ZnRlcl93b3JrZXJfc2VxdWVuY2UYAiABKAQidgoQTXhDb21tYW5kUmVxdWVz",
|
"bW1hbmRSZXF1ZXN0EhIKCnNlc3Npb25faWQYASABKAkSHQoVY2xpZW50X2Nv",
|
||||||
"dBISCgpzZXNzaW9uX2lkGAEgASgJEh0KFWNsaWVudF9jb3JyZWxhdGlvbl9p",
|
"cnJlbGF0aW9uX2lkGAIgASgJEi8KB2NvbW1hbmQYAyABKAsyHi5teGFjY2Vz",
|
||||||
"ZBgCIAEoCRIvCgdjb21tYW5kGAMgASgLMh4ubXhhY2Nlc3NfZ2F0ZXdheS52",
|
"c19nYXRld2F5LnYxLk14Q29tbWFuZCKiDAoJTXhDb21tYW5kEjAKBGtpbmQY",
|
||||||
"MS5NeENvbW1hbmQiogwKCU14Q29tbWFuZBIwCgRraW5kGAEgASgOMiIubXhh",
|
"ASABKA4yIi5teGFjY2Vzc19nYXRld2F5LnYxLk14Q29tbWFuZEtpbmQSOAoI",
|
||||||
"Y2Nlc3NfZ2F0ZXdheS52MS5NeENvbW1hbmRLaW5kEjgKCHJlZ2lzdGVyGAog",
|
"cmVnaXN0ZXIYCiABKAsyJC5teGFjY2Vzc19nYXRld2F5LnYxLlJlZ2lzdGVy",
|
||||||
"ASgLMiQubXhhY2Nlc3NfZ2F0ZXdheS52MS5SZWdpc3RlckNvbW1hbmRIABI8",
|
"Q29tbWFuZEgAEjwKCnVucmVnaXN0ZXIYCyABKAsyJi5teGFjY2Vzc19nYXRl",
|
||||||
"Cgp1bnJlZ2lzdGVyGAsgASgLMiYubXhhY2Nlc3NfZ2F0ZXdheS52MS5VbnJl",
|
"d2F5LnYxLlVucmVnaXN0ZXJDb21tYW5kSAASNwoIYWRkX2l0ZW0YDCABKAsy",
|
||||||
"Z2lzdGVyQ29tbWFuZEgAEjcKCGFkZF9pdGVtGAwgASgLMiMubXhhY2Nlc3Nf",
|
"Iy5teGFjY2Vzc19nYXRld2F5LnYxLkFkZEl0ZW1Db21tYW5kSAASOQoJYWRk",
|
||||||
"Z2F0ZXdheS52MS5BZGRJdGVtQ29tbWFuZEgAEjkKCWFkZF9pdGVtMhgNIAEo",
|
"X2l0ZW0yGA0gASgLMiQubXhhY2Nlc3NfZ2F0ZXdheS52MS5BZGRJdGVtMkNv",
|
||||||
"CzIkLm14YWNjZXNzX2dhdGV3YXkudjEuQWRkSXRlbTJDb21tYW5kSAASPQoL",
|
"bW1hbmRIABI9CgtyZW1vdmVfaXRlbRgOIAEoCzImLm14YWNjZXNzX2dhdGV3",
|
||||||
"cmVtb3ZlX2l0ZW0YDiABKAsyJi5teGFjY2Vzc19nYXRld2F5LnYxLlJlbW92",
|
"YXkudjEuUmVtb3ZlSXRlbUNvbW1hbmRIABI0CgZhZHZpc2UYDyABKAsyIi5t",
|
||||||
"ZUl0ZW1Db21tYW5kSAASNAoGYWR2aXNlGA8gASgLMiIubXhhY2Nlc3NfZ2F0",
|
"eGFjY2Vzc19nYXRld2F5LnYxLkFkdmlzZUNvbW1hbmRIABI5Cgl1bl9hZHZp",
|
||||||
"ZXdheS52MS5BZHZpc2VDb21tYW5kSAASOQoJdW5fYWR2aXNlGBAgASgLMiQu",
|
"c2UYECABKAsyJC5teGFjY2Vzc19nYXRld2F5LnYxLlVuQWR2aXNlQ29tbWFu",
|
||||||
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5VbkFkdmlzZUNvbW1hbmRIABJLChJhZHZp",
|
"ZEgAEksKEmFkdmlzZV9zdXBlcnZpc29yeRgRIAEoCzItLm14YWNjZXNzX2dh",
|
||||||
"c2Vfc3VwZXJ2aXNvcnkYESABKAsyLS5teGFjY2Vzc19nYXRld2F5LnYxLkFk",
|
"dGV3YXkudjEuQWR2aXNlU3VwZXJ2aXNvcnlDb21tYW5kSAASSAoRYWRkX2J1",
|
||||||
"dmlzZVN1cGVydmlzb3J5Q29tbWFuZEgAEkgKEWFkZF9idWZmZXJlZF9pdGVt",
|
"ZmZlcmVkX2l0ZW0YEiABKAsyKy5teGFjY2Vzc19nYXRld2F5LnYxLkFkZEJ1",
|
||||||
"GBIgASgLMisubXhhY2Nlc3NfZ2F0ZXdheS52MS5BZGRCdWZmZXJlZEl0ZW1D",
|
"ZmZlcmVkSXRlbUNvbW1hbmRIABJdChxzZXRfYnVmZmVyZWRfdXBkYXRlX2lu",
|
||||||
"b21tYW5kSAASXQocc2V0X2J1ZmZlcmVkX3VwZGF0ZV9pbnRlcnZhbBgTIAEo",
|
"dGVydmFsGBMgASgLMjUubXhhY2Nlc3NfZ2F0ZXdheS52MS5TZXRCdWZmZXJl",
|
||||||
"CzI1Lm14YWNjZXNzX2dhdGV3YXkudjEuU2V0QnVmZmVyZWRVcGRhdGVJbnRl",
|
"ZFVwZGF0ZUludGVydmFsQ29tbWFuZEgAEjYKB3N1c3BlbmQYFCABKAsyIy5t",
|
||||||
"cnZhbENvbW1hbmRIABI2CgdzdXNwZW5kGBQgASgLMiMubXhhY2Nlc3NfZ2F0",
|
"eGFjY2Vzc19nYXRld2F5LnYxLlN1c3BlbmRDb21tYW5kSAASOAoIYWN0aXZh",
|
||||||
"ZXdheS52MS5TdXNwZW5kQ29tbWFuZEgAEjgKCGFjdGl2YXRlGBUgASgLMiQu",
|
"dGUYFSABKAsyJC5teGFjY2Vzc19nYXRld2F5LnYxLkFjdGl2YXRlQ29tbWFu",
|
||||||
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5BY3RpdmF0ZUNvbW1hbmRIABIyCgV3cml0",
|
"ZEgAEjIKBXdyaXRlGBYgASgLMiEubXhhY2Nlc3NfZ2F0ZXdheS52MS5Xcml0",
|
||||||
"ZRgWIAEoCzIhLm14YWNjZXNzX2dhdGV3YXkudjEuV3JpdGVDb21tYW5kSAAS",
|
"ZUNvbW1hbmRIABI0CgZ3cml0ZTIYFyABKAsyIi5teGFjY2Vzc19nYXRld2F5",
|
||||||
"NAoGd3JpdGUyGBcgASgLMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5Xcml0ZTJD",
|
"LnYxLldyaXRlMkNvbW1hbmRIABJBCg13cml0ZV9zZWN1cmVkGBggASgLMigu",
|
||||||
"b21tYW5kSAASQQoNd3JpdGVfc2VjdXJlZBgYIAEoCzIoLm14YWNjZXNzX2dh",
|
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5Xcml0ZVNlY3VyZWRDb21tYW5kSAASQwoO",
|
||||||
"dGV3YXkudjEuV3JpdGVTZWN1cmVkQ29tbWFuZEgAEkMKDndyaXRlX3NlY3Vy",
|
"d3JpdGVfc2VjdXJlZDIYGSABKAsyKS5teGFjY2Vzc19nYXRld2F5LnYxLldy",
|
||||||
"ZWQyGBkgASgLMikubXhhY2Nlc3NfZ2F0ZXdheS52MS5Xcml0ZVNlY3VyZWQy",
|
"aXRlU2VjdXJlZDJDb21tYW5kSAASSQoRYXV0aGVudGljYXRlX3VzZXIYGiAB",
|
||||||
"Q29tbWFuZEgAEkkKEWF1dGhlbnRpY2F0ZV91c2VyGBogASgLMiwubXhhY2Nl",
|
"KAsyLC5teGFjY2Vzc19nYXRld2F5LnYxLkF1dGhlbnRpY2F0ZVVzZXJDb21t",
|
||||||
"c3NfZ2F0ZXdheS52MS5BdXRoZW50aWNhdGVVc2VyQ29tbWFuZEgAEk0KFGFy",
|
"YW5kSAASTQoUYXJjaGVzdHJhX3VzZXJfdG9faWQYGyABKAsyLS5teGFjY2Vz",
|
||||||
"Y2hlc3RyYV91c2VyX3RvX2lkGBsgASgLMi0ubXhhY2Nlc3NfZ2F0ZXdheS52",
|
"c19nYXRld2F5LnYxLkFyY2hlc3RyQVVzZXJUb0lkQ29tbWFuZEgAEjAKBHBp",
|
||||||
"MS5BcmNoZXN0ckFVc2VyVG9JZENvbW1hbmRIABIwCgRwaW5nGGQgASgLMiAu",
|
"bmcYZCABKAsyIC5teGFjY2Vzc19nYXRld2F5LnYxLlBpbmdDb21tYW5kSAAS",
|
||||||
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5QaW5nQ29tbWFuZEgAEkgKEWdldF9zZXNz",
|
"SAoRZ2V0X3Nlc3Npb25fc3RhdGUYZSABKAsyKy5teGFjY2Vzc19nYXRld2F5",
|
||||||
"aW9uX3N0YXRlGGUgASgLMisubXhhY2Nlc3NfZ2F0ZXdheS52MS5HZXRTZXNz",
|
"LnYxLkdldFNlc3Npb25TdGF0ZUNvbW1hbmRIABJECg9nZXRfd29ya2VyX2lu",
|
||||||
"aW9uU3RhdGVDb21tYW5kSAASRAoPZ2V0X3dvcmtlcl9pbmZvGGYgASgLMiku",
|
"Zm8YZiABKAsyKS5teGFjY2Vzc19nYXRld2F5LnYxLkdldFdvcmtlckluZm9D",
|
||||||
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5HZXRXb3JrZXJJbmZvQ29tbWFuZEgAEj8K",
|
"b21tYW5kSAASPwoMZHJhaW5fZXZlbnRzGGcgASgLMicubXhhY2Nlc3NfZ2F0",
|
||||||
"DGRyYWluX2V2ZW50cxhnIAEoCzInLm14YWNjZXNzX2dhdGV3YXkudjEuRHJh",
|
"ZXdheS52MS5EcmFpbkV2ZW50c0NvbW1hbmRIABJFCg9zaHV0ZG93bl93b3Jr",
|
||||||
"aW5FdmVudHNDb21tYW5kSAASRQoPc2h1dGRvd25fd29ya2VyGGggASgLMiou",
|
"ZXIYaCABKAsyKi5teGFjY2Vzc19nYXRld2F5LnYxLlNodXRkb3duV29ya2Vy",
|
||||||
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5TaHV0ZG93bldvcmtlckNvbW1hbmRIAEIJ",
|
"Q29tbWFuZEgAQgkKB3BheWxvYWQiJgoPUmVnaXN0ZXJDb21tYW5kEhMKC2Ns",
|
||||||
"CgdwYXlsb2FkIiYKD1JlZ2lzdGVyQ29tbWFuZBITCgtjbGllbnRfbmFtZRgB",
|
"aWVudF9uYW1lGAEgASgJIioKEVVucmVnaXN0ZXJDb21tYW5kEhUKDXNlcnZl",
|
||||||
"IAEoCSIqChFVbnJlZ2lzdGVyQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEg",
|
"cl9oYW5kbGUYASABKAUiQAoOQWRkSXRlbUNvbW1hbmQSFQoNc2VydmVyX2hh",
|
||||||
"ASgFIkAKDkFkZEl0ZW1Db21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUS",
|
"bmRsZRgBIAEoBRIXCg9pdGVtX2RlZmluaXRpb24YAiABKAkiVwoPQWRkSXRl",
|
||||||
"FwoPaXRlbV9kZWZpbml0aW9uGAIgASgJIlcKD0FkZEl0ZW0yQ29tbWFuZBIV",
|
"bTJDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSFwoPaXRlbV9kZWZp",
|
||||||
"Cg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhcKD2l0ZW1fZGVmaW5pdGlvbhgCIAEo",
|
"bml0aW9uGAIgASgJEhQKDGl0ZW1fY29udGV4dBgDIAEoCSI/ChFSZW1vdmVJ",
|
||||||
"CRIUCgxpdGVtX2NvbnRleHQYAyABKAkiPwoRUmVtb3ZlSXRlbUNvbW1hbmQS",
|
"dGVtQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFEhMKC2l0ZW1faGFu",
|
||||||
"FQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgCIAEoBSI7",
|
"ZGxlGAIgASgFIjsKDUFkdmlzZUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgB",
|
||||||
"Cg1BZHZpc2VDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRl",
|
"IAEoBRITCgtpdGVtX2hhbmRsZRgCIAEoBSI9Cg9VbkFkdmlzZUNvbW1hbmQS",
|
||||||
"bV9oYW5kbGUYAiABKAUiPQoPVW5BZHZpc2VDb21tYW5kEhUKDXNlcnZlcl9o",
|
"FQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgCIAEoBSJG",
|
||||||
"YW5kbGUYASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUiRgoYQWR2aXNlU3Vw",
|
"ChhBZHZpc2VTdXBlcnZpc29yeUNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgB",
|
||||||
"ZXJ2aXNvcnlDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRl",
|
"IAEoBRITCgtpdGVtX2hhbmRsZRgCIAEoBSJeChZBZGRCdWZmZXJlZEl0ZW1D",
|
||||||
"bV9oYW5kbGUYAiABKAUiXgoWQWRkQnVmZmVyZWRJdGVtQ29tbWFuZBIVCg1z",
|
"b21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSFwoPaXRlbV9kZWZpbml0",
|
||||||
"ZXJ2ZXJfaGFuZGxlGAEgASgFEhcKD2l0ZW1fZGVmaW5pdGlvbhgCIAEoCRIU",
|
"aW9uGAIgASgJEhQKDGl0ZW1fY29udGV4dBgDIAEoCSJfCiBTZXRCdWZmZXJl",
|
||||||
"CgxpdGVtX2NvbnRleHQYAyABKAkiXwogU2V0QnVmZmVyZWRVcGRhdGVJbnRl",
|
"ZFVwZGF0ZUludGVydmFsQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgF",
|
||||||
"cnZhbENvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIkChx1cGRhdGVf",
|
"EiQKHHVwZGF0ZV9pbnRlcnZhbF9taWxsaXNlY29uZHMYAiABKAUiPAoOU3Vz",
|
||||||
"aW50ZXJ2YWxfbWlsbGlzZWNvbmRzGAIgASgFIjwKDlN1c3BlbmRDb21tYW5k",
|
"cGVuZENvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hh",
|
||||||
"EhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUi",
|
"bmRsZRgCIAEoBSI9Cg9BY3RpdmF0ZUNvbW1hbmQSFQoNc2VydmVyX2hhbmRs",
|
||||||
"PQoPQWN0aXZhdGVDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoL",
|
"ZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgCIAEoBSJ4CgxXcml0ZUNvbW1hbmQS",
|
||||||
"aXRlbV9oYW5kbGUYAiABKAUieAoMV3JpdGVDb21tYW5kEhUKDXNlcnZlcl9o",
|
"FQoNc2VydmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgCIAEoBRIr",
|
||||||
|
"CgV2YWx1ZRgDIAEoCzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhWYWx1ZRIP",
|
||||||
|
"Cgd1c2VyX2lkGAQgASgFIrABCg1Xcml0ZTJDb21tYW5kEhUKDXNlcnZlcl9o",
|
||||||
"YW5kbGUYASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUSKwoFdmFsdWUYAyAB",
|
"YW5kbGUYASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUSKwoFdmFsdWUYAyAB",
|
||||||
"KAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14VmFsdWUSDwoHdXNlcl9pZBgE",
|
"KAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14VmFsdWUSNQoPdGltZXN0YW1w",
|
||||||
"IAEoBSKwAQoNV3JpdGUyQ29tbWFuZBIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgF",
|
"X3ZhbHVlGAQgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlEg8K",
|
||||||
"EhMKC2l0ZW1faGFuZGxlGAIgASgFEisKBXZhbHVlGAMgASgLMhwubXhhY2Nl",
|
"B3VzZXJfaWQYBSABKAUioQEKE1dyaXRlU2VjdXJlZENvbW1hbmQSFQoNc2Vy",
|
||||||
"c3NfZ2F0ZXdheS52MS5NeFZhbHVlEjUKD3RpbWVzdGFtcF92YWx1ZRgEIAEo",
|
"dmVyX2hhbmRsZRgBIAEoBRITCgtpdGVtX2hhbmRsZRgCIAEoBRIXCg9jdXJy",
|
||||||
"CzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhWYWx1ZRIPCgd1c2VyX2lkGAUg",
|
"ZW50X3VzZXJfaWQYAyABKAUSGAoQdmVyaWZpZXJfdXNlcl9pZBgEIAEoBRIr",
|
||||||
"ASgFIqEBChNXcml0ZVNlY3VyZWRDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUY",
|
"CgV2YWx1ZRgFIAEoCzIcLm14YWNjZXNzX2dhdGV3YXkudjEuTXhWYWx1ZSLZ",
|
||||||
"ASABKAUSEwoLaXRlbV9oYW5kbGUYAiABKAUSFwoPY3VycmVudF91c2VyX2lk",
|
"AQoUV3JpdGVTZWN1cmVkMkNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEo",
|
||||||
"GAMgASgFEhgKEHZlcmlmaWVyX3VzZXJfaWQYBCABKAUSKwoFdmFsdWUYBSAB",
|
"BRITCgtpdGVtX2hhbmRsZRgCIAEoBRIXCg9jdXJyZW50X3VzZXJfaWQYAyAB",
|
||||||
"KAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14VmFsdWUi2QEKFFdyaXRlU2Vj",
|
"KAUSGAoQdmVyaWZpZXJfdXNlcl9pZBgEIAEoBRIrCgV2YWx1ZRgFIAEoCzIc",
|
||||||
"dXJlZDJDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLaXRlbV9o",
|
"Lm14YWNjZXNzX2dhdGV3YXkudjEuTXhWYWx1ZRI1Cg90aW1lc3RhbXBfdmFs",
|
||||||
"YW5kbGUYAiABKAUSFwoPY3VycmVudF91c2VyX2lkGAMgASgFEhgKEHZlcmlm",
|
"dWUYBiABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14VmFsdWUiYwoXQXV0",
|
||||||
"aWVyX3VzZXJfaWQYBCABKAUSKwoFdmFsdWUYBSABKAsyHC5teGFjY2Vzc19n",
|
"aGVudGljYXRlVXNlckNvbW1hbmQSFQoNc2VydmVyX2hhbmRsZRgBIAEoBRIT",
|
||||||
"YXRld2F5LnYxLk14VmFsdWUSNQoPdGltZXN0YW1wX3ZhbHVlGAYgASgLMhwu",
|
"Cgt2ZXJpZnlfdXNlchgCIAEoCRIcChR2ZXJpZnlfdXNlcl9wYXNzd29yZBgD",
|
||||||
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZhbHVlImMKF0F1dGhlbnRpY2F0ZVVz",
|
"IAEoCSJHChhBcmNoZXN0ckFVc2VyVG9JZENvbW1hbmQSFQoNc2VydmVyX2hh",
|
||||||
"ZXJDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUSEwoLdmVyaWZ5X3Vz",
|
"bmRsZRgBIAEoBRIUCgx1c2VyX2lkX2d1aWQYAiABKAkiHgoLUGluZ0NvbW1h",
|
||||||
"ZXIYAiABKAkSHAoUdmVyaWZ5X3VzZXJfcGFzc3dvcmQYAyABKAkiRwoYQXJj",
|
"bmQSDwoHbWVzc2FnZRgBIAEoCSIYChZHZXRTZXNzaW9uU3RhdGVDb21tYW5k",
|
||||||
"aGVzdHJBVXNlclRvSWRDb21tYW5kEhUKDXNlcnZlcl9oYW5kbGUYASABKAUS",
|
"IhYKFEdldFdvcmtlckluZm9Db21tYW5kIigKEkRyYWluRXZlbnRzQ29tbWFu",
|
||||||
"FAoMdXNlcl9pZF9ndWlkGAIgASgJIh4KC1BpbmdDb21tYW5kEg8KB21lc3Nh",
|
"ZBISCgptYXhfZXZlbnRzGAEgASgNIkgKFVNodXRkb3duV29ya2VyQ29tbWFu",
|
||||||
"Z2UYASABKAkiGAoWR2V0U2Vzc2lvblN0YXRlQ29tbWFuZCIWChRHZXRXb3Jr",
|
"ZBIvCgxncmFjZV9wZXJpb2QYASABKAsyGS5nb29nbGUucHJvdG9idWYuRHVy",
|
||||||
"ZXJJbmZvQ29tbWFuZCIoChJEcmFpbkV2ZW50c0NvbW1hbmQSEgoKbWF4X2V2",
|
"YXRpb24ikAgKDk14Q29tbWFuZFJlcGx5EhIKCnNlc3Npb25faWQYASABKAkS",
|
||||||
"ZW50cxgBIAEoDSJIChVTaHV0ZG93bldvcmtlckNvbW1hbmQSLwoMZ3JhY2Vf",
|
"FgoOY29ycmVsYXRpb25faWQYAiABKAkSMAoEa2luZBgDIAEoDjIiLm14YWNj",
|
||||||
"cGVyaW9kGAEgASgLMhkuZ29vZ2xlLnByb3RvYnVmLkR1cmF0aW9uIpAICg5N",
|
"ZXNzX2dhdGV3YXkudjEuTXhDb21tYW5kS2luZBI8Cg9wcm90b2NvbF9zdGF0",
|
||||||
"eENvbW1hbmRSZXBseRISCgpzZXNzaW9uX2lkGAEgASgJEhYKDmNvcnJlbGF0",
|
"dXMYBCABKAsyIy5teGFjY2Vzc19nYXRld2F5LnYxLlByb3RvY29sU3RhdHVz",
|
||||||
"aW9uX2lkGAIgASgJEjAKBGtpbmQYAyABKA4yIi5teGFjY2Vzc19nYXRld2F5",
|
"EhQKB2hyZXN1bHQYBSABKAVIAYgBARIyCgxyZXR1cm5fdmFsdWUYBiABKAsy",
|
||||||
"LnYxLk14Q29tbWFuZEtpbmQSPAoPcHJvdG9jb2xfc3RhdHVzGAQgASgLMiMu",
|
"HC5teGFjY2Vzc19nYXRld2F5LnYxLk14VmFsdWUSNAoIc3RhdHVzZXMYByAD",
|
||||||
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5Qcm90b2NvbFN0YXR1cxIUCgdocmVzdWx0",
|
"KAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLk14U3RhdHVzUHJveHkSGgoSZGlh",
|
||||||
"GAUgASgFSAGIAQESMgoMcmV0dXJuX3ZhbHVlGAYgASgLMhwubXhhY2Nlc3Nf",
|
"Z25vc3RpY19tZXNzYWdlGAggASgJEjYKCHJlZ2lzdGVyGBQgASgLMiIubXhh",
|
||||||
"Z2F0ZXdheS52MS5NeFZhbHVlEjQKCHN0YXR1c2VzGAcgAygLMiIubXhhY2Nl",
|
"Y2Nlc3NfZ2F0ZXdheS52MS5SZWdpc3RlclJlcGx5SAASNQoIYWRkX2l0ZW0Y",
|
||||||
"c3NfZ2F0ZXdheS52MS5NeFN0YXR1c1Byb3h5EhoKEmRpYWdub3N0aWNfbWVz",
|
"FSABKAsyIS5teGFjY2Vzc19nYXRld2F5LnYxLkFkZEl0ZW1SZXBseUgAEjcK",
|
||||||
"c2FnZRgIIAEoCRI2CghyZWdpc3RlchgUIAEoCzIiLm14YWNjZXNzX2dhdGV3",
|
"CWFkZF9pdGVtMhgWIAEoCzIiLm14YWNjZXNzX2dhdGV3YXkudjEuQWRkSXRl",
|
||||||
"YXkudjEuUmVnaXN0ZXJSZXBseUgAEjUKCGFkZF9pdGVtGBUgASgLMiEubXhh",
|
"bTJSZXBseUgAEkYKEWFkZF9idWZmZXJlZF9pdGVtGBcgASgLMikubXhhY2Nl",
|
||||||
"Y2Nlc3NfZ2F0ZXdheS52MS5BZGRJdGVtUmVwbHlIABI3CglhZGRfaXRlbTIY",
|
"c3NfZ2F0ZXdheS52MS5BZGRCdWZmZXJlZEl0ZW1SZXBseUgAEjQKB3N1c3Bl",
|
||||||
"FiABKAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLkFkZEl0ZW0yUmVwbHlIABJG",
|
"bmQYGCABKAsyIS5teGFjY2Vzc19nYXRld2F5LnYxLlN1c3BlbmRSZXBseUgA",
|
||||||
"ChFhZGRfYnVmZmVyZWRfaXRlbRgXIAEoCzIpLm14YWNjZXNzX2dhdGV3YXku",
|
"EjYKCGFjdGl2YXRlGBkgASgLMiIubXhhY2Nlc3NfZ2F0ZXdheS52MS5BY3Rp",
|
||||||
"djEuQWRkQnVmZmVyZWRJdGVtUmVwbHlIABI0CgdzdXNwZW5kGBggASgLMiEu",
|
"dmF0ZVJlcGx5SAASRwoRYXV0aGVudGljYXRlX3VzZXIYGiABKAsyKi5teGFj",
|
||||||
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5TdXNwZW5kUmVwbHlIABI2CghhY3RpdmF0",
|
"Y2Vzc19nYXRld2F5LnYxLkF1dGhlbnRpY2F0ZVVzZXJSZXBseUgAEksKFGFy",
|
||||||
"ZRgZIAEoCzIiLm14YWNjZXNzX2dhdGV3YXkudjEuQWN0aXZhdGVSZXBseUgA",
|
"Y2hlc3RyYV91c2VyX3RvX2lkGBsgASgLMisubXhhY2Nlc3NfZ2F0ZXdheS52",
|
||||||
"EkcKEWF1dGhlbnRpY2F0ZV91c2VyGBogASgLMioubXhhY2Nlc3NfZ2F0ZXdh",
|
"MS5BcmNoZXN0ckFVc2VyVG9JZFJlcGx5SAASPwoNc2Vzc2lvbl9zdGF0ZRhk",
|
||||||
"eS52MS5BdXRoZW50aWNhdGVVc2VyUmVwbHlIABJLChRhcmNoZXN0cmFfdXNl",
|
"IAEoCzImLm14YWNjZXNzX2dhdGV3YXkudjEuU2Vzc2lvblN0YXRlUmVwbHlI",
|
||||||
"cl90b19pZBgbIAEoCzIrLm14YWNjZXNzX2dhdGV3YXkudjEuQXJjaGVzdHJB",
|
"ABI7Cgt3b3JrZXJfaW5mbxhlIAEoCzIkLm14YWNjZXNzX2dhdGV3YXkudjEu",
|
||||||
"VXNlclRvSWRSZXBseUgAEj8KDXNlc3Npb25fc3RhdGUYZCABKAsyJi5teGFj",
|
"V29ya2VySW5mb1JlcGx5SAASPQoMZHJhaW5fZXZlbnRzGGYgASgLMiUubXhh",
|
||||||
"Y2Vzc19nYXRld2F5LnYxLlNlc3Npb25TdGF0ZVJlcGx5SAASOwoLd29ya2Vy",
|
"Y2Nlc3NfZ2F0ZXdheS52MS5EcmFpbkV2ZW50c1JlcGx5SABCCQoHcGF5bG9h",
|
||||||
"X2luZm8YZSABKAsyJC5teGFjY2Vzc19nYXRld2F5LnYxLldvcmtlckluZm9S",
|
"ZEIKCghfaHJlc3VsdCImCg1SZWdpc3RlclJlcGx5EhUKDXNlcnZlcl9oYW5k",
|
||||||
"ZXBseUgAEj0KDGRyYWluX2V2ZW50cxhmIAEoCzIlLm14YWNjZXNzX2dhdGV3",
|
"bGUYASABKAUiIwoMQWRkSXRlbVJlcGx5EhMKC2l0ZW1faGFuZGxlGAEgASgF",
|
||||||
"YXkudjEuRHJhaW5FdmVudHNSZXBseUgAQgkKB3BheWxvYWRCCgoIX2hyZXN1",
|
"IiQKDUFkZEl0ZW0yUmVwbHkSEwoLaXRlbV9oYW5kbGUYASABKAUiKwoUQWRk",
|
||||||
"bHQiJgoNUmVnaXN0ZXJSZXBseRIVCg1zZXJ2ZXJfaGFuZGxlGAEgASgFIiMK",
|
"QnVmZmVyZWRJdGVtUmVwbHkSEwoLaXRlbV9oYW5kbGUYASABKAUiQgoMU3Vz",
|
||||||
"DEFkZEl0ZW1SZXBseRITCgtpdGVtX2hhbmRsZRgBIAEoBSIkCg1BZGRJdGVt",
|
"cGVuZFJlcGx5EjIKBnN0YXR1cxgBIAEoCzIiLm14YWNjZXNzX2dhdGV3YXku",
|
||||||
"MlJlcGx5EhMKC2l0ZW1faGFuZGxlGAEgASgFIisKFEFkZEJ1ZmZlcmVkSXRl",
|
"djEuTXhTdGF0dXNQcm94eSJDCg1BY3RpdmF0ZVJlcGx5EjIKBnN0YXR1cxgB",
|
||||||
"bVJlcGx5EhMKC2l0ZW1faGFuZGxlGAEgASgFIkIKDFN1c3BlbmRSZXBseRIy",
|
"IAEoCzIiLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNQcm94eSIoChVB",
|
||||||
"CgZzdGF0dXMYASABKAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLk14U3RhdHVz",
|
"dXRoZW50aWNhdGVVc2VyUmVwbHkSDwoHdXNlcl9pZBgBIAEoBSIpChZBcmNo",
|
||||||
"UHJveHkiQwoNQWN0aXZhdGVSZXBseRIyCgZzdGF0dXMYASABKAsyIi5teGFj",
|
"ZXN0ckFVc2VyVG9JZFJlcGx5Eg8KB3VzZXJfaWQYASABKAUiRQoRU2Vzc2lv",
|
||||||
"Y2Vzc19nYXRld2F5LnYxLk14U3RhdHVzUHJveHkiKAoVQXV0aGVudGljYXRl",
|
"blN0YXRlUmVwbHkSMAoFc3RhdGUYASABKA4yIS5teGFjY2Vzc19nYXRld2F5",
|
||||||
"VXNlclJlcGx5Eg8KB3VzZXJfaWQYASABKAUiKQoWQXJjaGVzdHJBVXNlclRv",
|
"LnYxLlNlc3Npb25TdGF0ZSJ1Cg9Xb3JrZXJJbmZvUmVwbHkSGQoRd29ya2Vy",
|
||||||
"SWRSZXBseRIPCgd1c2VyX2lkGAEgASgFIkUKEVNlc3Npb25TdGF0ZVJlcGx5",
|
"X3Byb2Nlc3NfaWQYASABKAUSFgoOd29ya2VyX3ZlcnNpb24YAiABKAkSFwoP",
|
||||||
"EjAKBXN0YXRlGAEgASgOMiEubXhhY2Nlc3NfZ2F0ZXdheS52MS5TZXNzaW9u",
|
"bXhhY2Nlc3NfcHJvZ2lkGAMgASgJEhYKDm14YWNjZXNzX2Nsc2lkGAQgASgJ",
|
||||||
"U3RhdGUidQoPV29ya2VySW5mb1JlcGx5EhkKEXdvcmtlcl9wcm9jZXNzX2lk",
|
"IkAKEERyYWluRXZlbnRzUmVwbHkSLAoGZXZlbnRzGAEgAygLMhwubXhhY2Nl",
|
||||||
"GAEgASgFEhYKDndvcmtlcl92ZXJzaW9uGAIgASgJEhcKD214YWNjZXNzX3By",
|
"c3NfZ2F0ZXdheS52MS5NeEV2ZW50IpsGCgdNeEV2ZW50EjIKBmZhbWlseRgB",
|
||||||
"b2dpZBgDIAEoCRIWCg5teGFjY2Vzc19jbHNpZBgEIAEoCSJAChBEcmFpbkV2",
|
"IAEoDjIiLm14YWNjZXNzX2dhdGV3YXkudjEuTXhFdmVudEZhbWlseRISCgpz",
|
||||||
"ZW50c1JlcGx5EiwKBmV2ZW50cxgBIAMoCzIcLm14YWNjZXNzX2dhdGV3YXku",
|
"ZXNzaW9uX2lkGAIgASgJEhUKDXNlcnZlcl9oYW5kbGUYAyABKAUSEwoLaXRl",
|
||||||
"djEuTXhFdmVudCKbBgoHTXhFdmVudBIyCgZmYW1pbHkYASABKA4yIi5teGFj",
|
"bV9oYW5kbGUYBCABKAUSKwoFdmFsdWUYBSABKAsyHC5teGFjY2Vzc19nYXRl",
|
||||||
"Y2Vzc19nYXRld2F5LnYxLk14RXZlbnRGYW1pbHkSEgoKc2Vzc2lvbl9pZBgC",
|
"d2F5LnYxLk14VmFsdWUSDwoHcXVhbGl0eRgGIAEoBRI0ChBzb3VyY2VfdGlt",
|
||||||
"IAEoCRIVCg1zZXJ2ZXJfaGFuZGxlGAMgASgFEhMKC2l0ZW1faGFuZGxlGAQg",
|
"ZXN0YW1wGAcgASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcBI0Cghz",
|
||||||
"ASgFEisKBXZhbHVlGAUgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFZh",
|
"dGF0dXNlcxgIIAMoCzIiLm14YWNjZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNQ",
|
||||||
"bHVlEg8KB3F1YWxpdHkYBiABKAUSNAoQc291cmNlX3RpbWVzdGFtcBgHIAEo",
|
"cm94eRIXCg93b3JrZXJfc2VxdWVuY2UYCSABKAQSNAoQd29ya2VyX3RpbWVz",
|
||||||
"CzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASNAoIc3RhdHVzZXMYCCAD",
|
"dGFtcBgKIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXASPQoZZ2F0",
|
||||||
"KAsyIi5teGFjY2Vzc19nYXRld2F5LnYxLk14U3RhdHVzUHJveHkSFwoPd29y",
|
"ZXdheV9yZWNlaXZlX3RpbWVzdGFtcBgLIAEoCzIaLmdvb2dsZS5wcm90b2J1",
|
||||||
"a2VyX3NlcXVlbmNlGAkgASgEEjQKEHdvcmtlcl90aW1lc3RhbXAYCiABKAsy",
|
"Zi5UaW1lc3RhbXASFAoHaHJlc3VsdBgMIAEoBUgBiAEBEhIKCnJhd19zdGF0",
|
||||||
"Gi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEj0KGWdhdGV3YXlfcmVjZWl2",
|
"dXMYDSABKAkSQAoOb25fZGF0YV9jaGFuZ2UYFCABKAsyJi5teGFjY2Vzc19n",
|
||||||
"ZV90aW1lc3RhbXAYCyABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1w",
|
"YXRld2F5LnYxLk9uRGF0YUNoYW5nZUV2ZW50SAASRgoRb25fd3JpdGVfY29t",
|
||||||
"EhQKB2hyZXN1bHQYDCABKAVIAYgBARISCgpyYXdfc3RhdHVzGA0gASgJEkAK",
|
"cGxldGUYFSABKAsyKS5teGFjY2Vzc19nYXRld2F5LnYxLk9uV3JpdGVDb21w",
|
||||||
"Dm9uX2RhdGFfY2hhbmdlGBQgASgLMiYubXhhY2Nlc3NfZ2F0ZXdheS52MS5P",
|
"bGV0ZUV2ZW50SAASSQoSb3BlcmF0aW9uX2NvbXBsZXRlGBYgASgLMisubXhh",
|
||||||
"bkRhdGFDaGFuZ2VFdmVudEgAEkYKEW9uX3dyaXRlX2NvbXBsZXRlGBUgASgL",
|
"Y2Nlc3NfZ2F0ZXdheS52MS5PcGVyYXRpb25Db21wbGV0ZUV2ZW50SAASUQoX",
|
||||||
"MikubXhhY2Nlc3NfZ2F0ZXdheS52MS5PbldyaXRlQ29tcGxldGVFdmVudEgA",
|
"b25fYnVmZmVyZWRfZGF0YV9jaGFuZ2UYFyABKAsyLi5teGFjY2Vzc19nYXRl",
|
||||||
"EkkKEm9wZXJhdGlvbl9jb21wbGV0ZRgWIAEoCzIrLm14YWNjZXNzX2dhdGV3",
|
"d2F5LnYxLk9uQnVmZmVyZWREYXRhQ2hhbmdlRXZlbnRIAEIGCgRib2R5QgoK",
|
||||||
"YXkudjEuT3BlcmF0aW9uQ29tcGxldGVFdmVudEgAElEKF29uX2J1ZmZlcmVk",
|
"CF9ocmVzdWx0IhMKEU9uRGF0YUNoYW5nZUV2ZW50IhYKFE9uV3JpdGVDb21w",
|
||||||
"X2RhdGFfY2hhbmdlGBcgASgLMi4ubXhhY2Nlc3NfZ2F0ZXdheS52MS5PbkJ1",
|
"bGV0ZUV2ZW50IhgKFk9wZXJhdGlvbkNvbXBsZXRlRXZlbnQi1AEKGU9uQnVm",
|
||||||
"ZmZlcmVkRGF0YUNoYW5nZUV2ZW50SABCBgoEYm9keUIKCghfaHJlc3VsdCIT",
|
"ZmVyZWREYXRhQ2hhbmdlRXZlbnQSMgoJZGF0YV90eXBlGAEgASgOMh8ubXhh",
|
||||||
"ChFPbkRhdGFDaGFuZ2VFdmVudCIWChRPbldyaXRlQ29tcGxldGVFdmVudCIY",
|
"Y2Nlc3NfZ2F0ZXdheS52MS5NeERhdGFUeXBlEjQKDnF1YWxpdHlfdmFsdWVz",
|
||||||
"ChZPcGVyYXRpb25Db21wbGV0ZUV2ZW50ItQBChlPbkJ1ZmZlcmVkRGF0YUNo",
|
"GAIgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeEFycmF5EjYKEHRpbWVz",
|
||||||
"YW5nZUV2ZW50EjIKCWRhdGFfdHlwZRgBIAEoDjIfLm14YWNjZXNzX2dhdGV3",
|
"dGFtcF92YWx1ZXMYAyABKAsyHC5teGFjY2Vzc19nYXRld2F5LnYxLk14QXJy",
|
||||||
"YXkudjEuTXhEYXRhVHlwZRI0Cg5xdWFsaXR5X3ZhbHVlcxgCIAEoCzIcLm14",
|
"YXkSFQoNcmF3X2RhdGFfdHlwZRgEIAEoBSLrAQoNTXhTdGF0dXNQcm94eRIP",
|
||||||
"YWNjZXNzX2dhdGV3YXkudjEuTXhBcnJheRI2ChB0aW1lc3RhbXBfdmFsdWVz",
|
"CgdzdWNjZXNzGAEgASgFEjcKCGNhdGVnb3J5GAIgASgOMiUubXhhY2Nlc3Nf",
|
||||||
"GAMgASgLMhwubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeEFycmF5EhUKDXJhd19k",
|
"Z2F0ZXdheS52MS5NeFN0YXR1c0NhdGVnb3J5EjgKC2RldGVjdGVkX2J5GAMg",
|
||||||
"YXRhX3R5cGUYBCABKAUi6wEKDU14U3RhdHVzUHJveHkSDwoHc3VjY2VzcxgB",
|
"ASgOMiMubXhhY2Nlc3NfZ2F0ZXdheS52MS5NeFN0YXR1c1NvdXJjZRIOCgZk",
|
||||||
"IAEoBRI3CghjYXRlZ29yeRgCIAEoDjIlLm14YWNjZXNzX2dhdGV3YXkudjEu",
|
"ZXRhaWwYBCABKAUSFAoMcmF3X2NhdGVnb3J5GAUgASgFEhcKD3Jhd19kZXRl",
|
||||||
"TXhTdGF0dXNDYXRlZ29yeRI4CgtkZXRlY3RlZF9ieRgDIAEoDjIjLm14YWNj",
|
"Y3RlZF9ieRgGIAEoBRIXCg9kaWFnbm9zdGljX3RleHQYByABKAkipwMKB014",
|
||||||
"ZXNzX2dhdGV3YXkudjEuTXhTdGF0dXNTb3VyY2USDgoGZGV0YWlsGAQgASgF",
|
"VmFsdWUSMgoJZGF0YV90eXBlGAEgASgOMh8ubXhhY2Nlc3NfZ2F0ZXdheS52",
|
||||||
"EhQKDHJhd19jYXRlZ29yeRgFIAEoBRIXCg9yYXdfZGV0ZWN0ZWRfYnkYBiAB",
|
"MS5NeERhdGFUeXBlEhQKDHZhcmlhbnRfdHlwZRgCIAEoCRIPCgdpc19udWxs",
|
||||||
"KAUSFwoPZGlhZ25vc3RpY190ZXh0GAcgASgJIqcDCgdNeFZhbHVlEjIKCWRh",
|
"GAMgASgIEhYKDnJhd19kaWFnbm9zdGljGAQgASgJEhUKDXJhd19kYXRhX3R5",
|
||||||
"dGFfdHlwZRgBIAEoDjIfLm14YWNjZXNzX2dhdGV3YXkudjEuTXhEYXRhVHlw",
|
"cGUYBSABKAUSFAoKYm9vbF92YWx1ZRgKIAEoCEgAEhUKC2ludDMyX3ZhbHVl",
|
||||||
"ZRIUCgx2YXJpYW50X3R5cGUYAiABKAkSDwoHaXNfbnVsbBgDIAEoCBIWCg5y",
|
"GAsgASgFSAASFQoLaW50NjRfdmFsdWUYDCABKANIABIVCgtmbG9hdF92YWx1",
|
||||||
"YXdfZGlhZ25vc3RpYxgEIAEoCRIVCg1yYXdfZGF0YV90eXBlGAUgASgFEhQK",
|
"ZRgNIAEoAkgAEhYKDGRvdWJsZV92YWx1ZRgOIAEoAUgAEhYKDHN0cmluZ192",
|
||||||
"CmJvb2xfdmFsdWUYCiABKAhIABIVCgtpbnQzMl92YWx1ZRgLIAEoBUgAEhUK",
|
"YWx1ZRgPIAEoCUgAEjUKD3RpbWVzdGFtcF92YWx1ZRgQIAEoCzIaLmdvb2ds",
|
||||||
"C2ludDY0X3ZhbHVlGAwgASgDSAASFQoLZmxvYXRfdmFsdWUYDSABKAJIABIW",
|
"ZS5wcm90b2J1Zi5UaW1lc3RhbXBIABIzCgthcnJheV92YWx1ZRgRIAEoCzIc",
|
||||||
"Cgxkb3VibGVfdmFsdWUYDiABKAFIABIWCgxzdHJpbmdfdmFsdWUYDyABKAlI",
|
"Lm14YWNjZXNzX2dhdGV3YXkudjEuTXhBcnJheUgAEhMKCXJhd192YWx1ZRgS",
|
||||||
"ABI1Cg90aW1lc3RhbXBfdmFsdWUYECABKAsyGi5nb29nbGUucHJvdG9idWYu",
|
"IAEoDEgAQgYKBGtpbmQi/gQKB014QXJyYXkSOgoRZWxlbWVudF9kYXRhX3R5",
|
||||||
"VGltZXN0YW1wSAASMwoLYXJyYXlfdmFsdWUYESABKAsyHC5teGFjY2Vzc19n",
|
"cGUYASABKA4yHy5teGFjY2Vzc19nYXRld2F5LnYxLk14RGF0YVR5cGUSFAoM",
|
||||||
"YXRld2F5LnYxLk14QXJyYXlIABITCglyYXdfdmFsdWUYEiABKAxIAEIGCgRr",
|
"dmFyaWFudF90eXBlGAIgASgJEhIKCmRpbWVuc2lvbnMYAyADKA0SFgoOcmF3",
|
||||||
"aW5kIv4ECgdNeEFycmF5EjoKEWVsZW1lbnRfZGF0YV90eXBlGAEgASgOMh8u",
|
"X2RpYWdub3N0aWMYBCABKAkSHQoVcmF3X2VsZW1lbnRfZGF0YV90eXBlGAUg",
|
||||||
"bXhhY2Nlc3NfZ2F0ZXdheS52MS5NeERhdGFUeXBlEhQKDHZhcmlhbnRfdHlw",
|
"ASgFEjUKC2Jvb2xfdmFsdWVzGAogASgLMh4ubXhhY2Nlc3NfZ2F0ZXdheS52",
|
||||||
"ZRgCIAEoCRISCgpkaW1lbnNpb25zGAMgAygNEhYKDnJhd19kaWFnbm9zdGlj",
|
"MS5Cb29sQXJyYXlIABI3CgxpbnQzMl92YWx1ZXMYCyABKAsyHy5teGFjY2Vz",
|
||||||
"GAQgASgJEh0KFXJhd19lbGVtZW50X2RhdGFfdHlwZRgFIAEoBRI1Cgtib29s",
|
"c19nYXRld2F5LnYxLkludDMyQXJyYXlIABI3CgxpbnQ2NF92YWx1ZXMYDCAB",
|
||||||
"X3ZhbHVlcxgKIAEoCzIeLm14YWNjZXNzX2dhdGV3YXkudjEuQm9vbEFycmF5",
|
"KAsyHy5teGFjY2Vzc19nYXRld2F5LnYxLkludDY0QXJyYXlIABI3CgxmbG9h",
|
||||||
"SAASNwoMaW50MzJfdmFsdWVzGAsgASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52",
|
"dF92YWx1ZXMYDSABKAsyHy5teGFjY2Vzc19nYXRld2F5LnYxLkZsb2F0QXJy",
|
||||||
"MS5JbnQzMkFycmF5SAASNwoMaW50NjRfdmFsdWVzGAwgASgLMh8ubXhhY2Nl",
|
"YXlIABI5Cg1kb3VibGVfdmFsdWVzGA4gASgLMiAubXhhY2Nlc3NfZ2F0ZXdh",
|
||||||
"c3NfZ2F0ZXdheS52MS5JbnQ2NEFycmF5SAASNwoMZmxvYXRfdmFsdWVzGA0g",
|
"eS52MS5Eb3VibGVBcnJheUgAEjkKDXN0cmluZ192YWx1ZXMYDyABKAsyIC5t",
|
||||||
"ASgLMh8ubXhhY2Nlc3NfZ2F0ZXdheS52MS5GbG9hdEFycmF5SAASOQoNZG91",
|
"eGFjY2Vzc19nYXRld2F5LnYxLlN0cmluZ0FycmF5SAASPwoQdGltZXN0YW1w",
|
||||||
"YmxlX3ZhbHVlcxgOIAEoCzIgLm14YWNjZXNzX2dhdGV3YXkudjEuRG91Ymxl",
|
"X3ZhbHVlcxgQIAEoCzIjLm14YWNjZXNzX2dhdGV3YXkudjEuVGltZXN0YW1w",
|
||||||
"QXJyYXlIABI5Cg1zdHJpbmdfdmFsdWVzGA8gASgLMiAubXhhY2Nlc3NfZ2F0",
|
"QXJyYXlIABIzCgpyYXdfdmFsdWVzGBEgASgLMh0ubXhhY2Nlc3NfZ2F0ZXdh",
|
||||||
"ZXdheS52MS5TdHJpbmdBcnJheUgAEj8KEHRpbWVzdGFtcF92YWx1ZXMYECAB",
|
"eS52MS5SYXdBcnJheUgAQggKBnZhbHVlcyIbCglCb29sQXJyYXkSDgoGdmFs",
|
||||||
"KAsyIy5teGFjY2Vzc19nYXRld2F5LnYxLlRpbWVzdGFtcEFycmF5SAASMwoK",
|
"dWVzGAEgAygIIhwKCkludDMyQXJyYXkSDgoGdmFsdWVzGAEgAygFIhwKCklu",
|
||||||
"cmF3X3ZhbHVlcxgRIAEoCzIdLm14YWNjZXNzX2dhdGV3YXkudjEuUmF3QXJy",
|
"dDY0QXJyYXkSDgoGdmFsdWVzGAEgAygDIhwKCkZsb2F0QXJyYXkSDgoGdmFs",
|
||||||
"YXlIAEIICgZ2YWx1ZXMiGwoJQm9vbEFycmF5Eg4KBnZhbHVlcxgBIAMoCCIc",
|
"dWVzGAEgAygCIh0KC0RvdWJsZUFycmF5Eg4KBnZhbHVlcxgBIAMoASIdCgtT",
|
||||||
"CgpJbnQzMkFycmF5Eg4KBnZhbHVlcxgBIAMoBSIcCgpJbnQ2NEFycmF5Eg4K",
|
"dHJpbmdBcnJheRIOCgZ2YWx1ZXMYASADKAkiPAoOVGltZXN0YW1wQXJyYXkS",
|
||||||
"BnZhbHVlcxgBIAMoAyIcCgpGbG9hdEFycmF5Eg4KBnZhbHVlcxgBIAMoAiId",
|
"KgoGdmFsdWVzGAEgAygLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcCIa",
|
||||||
"CgtEb3VibGVBcnJheRIOCgZ2YWx1ZXMYASADKAEiHQoLU3RyaW5nQXJyYXkS",
|
"CghSYXdBcnJheRIOCgZ2YWx1ZXMYASADKAwiWAoOUHJvdG9jb2xTdGF0dXMS",
|
||||||
"DgoGdmFsdWVzGAEgAygJIjwKDlRpbWVzdGFtcEFycmF5EioKBnZhbHVlcxgB",
|
"NQoEY29kZRgBIAEoDjInLm14YWNjZXNzX2dhdGV3YXkudjEuUHJvdG9jb2xT",
|
||||||
"IAMoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAiGgoIUmF3QXJyYXkS",
|
"dGF0dXNDb2RlEg8KB21lc3NhZ2UYAiABKAkqvwYKDU14Q29tbWFuZEtpbmQS",
|
||||||
"DgoGdmFsdWVzGAEgAygMIlgKDlByb3RvY29sU3RhdHVzEjUKBGNvZGUYASAB",
|
"HwobTVhfQ09NTUFORF9LSU5EX1VOU1BFQ0lGSUVEEAASHAoYTVhfQ09NTUFO",
|
||||||
"KA4yJy5teGFjY2Vzc19nYXRld2F5LnYxLlByb3RvY29sU3RhdHVzQ29kZRIP",
|
"RF9LSU5EX1JFR0lTVEVSEAESHgoaTVhfQ09NTUFORF9LSU5EX1VOUkVHSVNU",
|
||||||
"CgdtZXNzYWdlGAIgASgJKr8GCg1NeENvbW1hbmRLaW5kEh8KG01YX0NPTU1B",
|
"RVIQAhIcChhNWF9DT01NQU5EX0tJTkRfQUREX0lURU0QAxIdChlNWF9DT01N",
|
||||||
"TkRfS0lORF9VTlNQRUNJRklFRBAAEhwKGE1YX0NPTU1BTkRfS0lORF9SRUdJ",
|
"QU5EX0tJTkRfQUREX0lURU0yEAQSHwobTVhfQ09NTUFORF9LSU5EX1JFTU9W",
|
||||||
"U1RFUhABEh4KGk1YX0NPTU1BTkRfS0lORF9VTlJFR0lTVEVSEAISHAoYTVhf",
|
"RV9JVEVNEAUSGgoWTVhfQ09NTUFORF9LSU5EX0FEVklTRRAGEh0KGU1YX0NP",
|
||||||
"Q09NTUFORF9LSU5EX0FERF9JVEVNEAMSHQoZTVhfQ09NTUFORF9LSU5EX0FE",
|
"TU1BTkRfS0lORF9VTl9BRFZJU0UQBxImCiJNWF9DT01NQU5EX0tJTkRfQURW",
|
||||||
"RF9JVEVNMhAEEh8KG01YX0NPTU1BTkRfS0lORF9SRU1PVkVfSVRFTRAFEhoK",
|
"SVNFX1NVUEVSVklTT1JZEAgSJQohTVhfQ09NTUFORF9LSU5EX0FERF9CVUZG",
|
||||||
"Fk1YX0NPTU1BTkRfS0lORF9BRFZJU0UQBhIdChlNWF9DT01NQU5EX0tJTkRf",
|
"RVJFRF9JVEVNEAkSMAosTVhfQ09NTUFORF9LSU5EX1NFVF9CVUZGRVJFRF9V",
|
||||||
"VU5fQURWSVNFEAcSJgoiTVhfQ09NTUFORF9LSU5EX0FEVklTRV9TVVBFUlZJ",
|
"UERBVEVfSU5URVJWQUwQChIbChdNWF9DT01NQU5EX0tJTkRfU1VTUEVORBAL",
|
||||||
"U09SWRAIEiUKIU1YX0NPTU1BTkRfS0lORF9BRERfQlVGRkVSRURfSVRFTRAJ",
|
"EhwKGE1YX0NPTU1BTkRfS0lORF9BQ1RJVkFURRAMEhkKFU1YX0NPTU1BTkRf",
|
||||||
"EjAKLE1YX0NPTU1BTkRfS0lORF9TRVRfQlVGRkVSRURfVVBEQVRFX0lOVEVS",
|
"S0lORF9XUklURRANEhoKFk1YX0NPTU1BTkRfS0lORF9XUklURTIQDhIhCh1N",
|
||||||
"VkFMEAoSGwoXTVhfQ09NTUFORF9LSU5EX1NVU1BFTkQQCxIcChhNWF9DT01N",
|
"WF9DT01NQU5EX0tJTkRfV1JJVEVfU0VDVVJFRBAPEiIKHk1YX0NPTU1BTkRf",
|
||||||
"QU5EX0tJTkRfQUNUSVZBVEUQDBIZChVNWF9DT01NQU5EX0tJTkRfV1JJVEUQ",
|
"S0lORF9XUklURV9TRUNVUkVEMhAQEiUKIU1YX0NPTU1BTkRfS0lORF9BVVRI",
|
||||||
"DRIaChZNWF9DT01NQU5EX0tJTkRfV1JJVEUyEA4SIQodTVhfQ09NTUFORF9L",
|
"RU5USUNBVEVfVVNFUhAREigKJE1YX0NPTU1BTkRfS0lORF9BUkNIRVNUUkFf",
|
||||||
"SU5EX1dSSVRFX1NFQ1VSRUQQDxIiCh5NWF9DT01NQU5EX0tJTkRfV1JJVEVf",
|
"VVNFUl9UT19JRBASEhgKFE1YX0NPTU1BTkRfS0lORF9QSU5HEGQSJQohTVhf",
|
||||||
"U0VDVVJFRDIQEBIlCiFNWF9DT01NQU5EX0tJTkRfQVVUSEVOVElDQVRFX1VT",
|
"Q09NTUFORF9LSU5EX0dFVF9TRVNTSU9OX1NUQVRFEGUSIwofTVhfQ09NTUFO",
|
||||||
"RVIQERIoCiRNWF9DT01NQU5EX0tJTkRfQVJDSEVTVFJBX1VTRVJfVE9fSUQQ",
|
"RF9LSU5EX0dFVF9XT1JLRVJfSU5GTxBmEiAKHE1YX0NPTU1BTkRfS0lORF9E",
|
||||||
"EhIYChRNWF9DT01NQU5EX0tJTkRfUElORxBkEiUKIU1YX0NPTU1BTkRfS0lO",
|
"UkFJTl9FVkVOVFMQZxIjCh9NWF9DT01NQU5EX0tJTkRfU0hVVERPV05fV09S",
|
||||||
"RF9HRVRfU0VTU0lPTl9TVEFURRBlEiMKH01YX0NPTU1BTkRfS0lORF9HRVRf",
|
"S0VSEGgq0AEKDU14RXZlbnRGYW1pbHkSHwobTVhfRVZFTlRfRkFNSUxZX1VO",
|
||||||
"V09SS0VSX0lORk8QZhIgChxNWF9DT01NQU5EX0tJTkRfRFJBSU5fRVZFTlRT",
|
"U1BFQ0lGSUVEEAASIgoeTVhfRVZFTlRfRkFNSUxZX09OX0RBVEFfQ0hBTkdF",
|
||||||
"EGcSIwofTVhfQ09NTUFORF9LSU5EX1NIVVRET1dOX1dPUktFUhBoKtABCg1N",
|
"EAESJQohTVhfRVZFTlRfRkFNSUxZX09OX1dSSVRFX0NPTVBMRVRFEAISJgoi",
|
||||||
"eEV2ZW50RmFtaWx5Eh8KG01YX0VWRU5UX0ZBTUlMWV9VTlNQRUNJRklFRBAA",
|
"TVhfRVZFTlRfRkFNSUxZX09QRVJBVElPTl9DT01QTEVURRADEisKJ01YX0VW",
|
||||||
"EiIKHk1YX0VWRU5UX0ZBTUlMWV9PTl9EQVRBX0NIQU5HRRABEiUKIU1YX0VW",
|
"RU5UX0ZBTUlMWV9PTl9CVUZGRVJFRF9EQVRBX0NIQU5HRRAEKqUDChBNeFN0",
|
||||||
"RU5UX0ZBTUlMWV9PTl9XUklURV9DT01QTEVURRACEiYKIk1YX0VWRU5UX0ZB",
|
"YXR1c0NhdGVnb3J5EiIKHk1YX1NUQVRVU19DQVRFR09SWV9VTlNQRUNJRklF",
|
||||||
"TUlMWV9PUEVSQVRJT05fQ09NUExFVEUQAxIrCidNWF9FVkVOVF9GQU1JTFlf",
|
"RBAAEh4KGk1YX1NUQVRVU19DQVRFR09SWV9VTktOT1dOEAESGQoVTVhfU1RB",
|
||||||
"T05fQlVGRkVSRURfREFUQV9DSEFOR0UQBCqlAwoQTXhTdGF0dXNDYXRlZ29y",
|
"VFVTX0NBVEVHT1JZX09LEAISHgoaTVhfU1RBVFVTX0NBVEVHT1JZX1BFTkRJ",
|
||||||
"eRIiCh5NWF9TVEFUVVNfQ0FURUdPUllfVU5TUEVDSUZJRUQQABIeChpNWF9T",
|
"TkcQAxIeChpNWF9TVEFUVVNfQ0FURUdPUllfV0FSTklORxAEEioKJk1YX1NU",
|
||||||
"VEFUVVNfQ0FURUdPUllfVU5LTk9XThABEhkKFU1YX1NUQVRVU19DQVRFR09S",
|
"QVRVU19DQVRFR09SWV9DT01NVU5JQ0FUSU9OX0VSUk9SEAUSKgomTVhfU1RB",
|
||||||
"WV9PSxACEh4KGk1YX1NUQVRVU19DQVRFR09SWV9QRU5ESU5HEAMSHgoaTVhf",
|
"VFVTX0NBVEVHT1JZX0NPTkZJR1VSQVRJT05fRVJST1IQBhIoCiRNWF9TVEFU",
|
||||||
"U1RBVFVTX0NBVEVHT1JZX1dBUk5JTkcQBBIqCiZNWF9TVEFUVVNfQ0FURUdP",
|
"VVNfQ0FURUdPUllfT1BFUkFUSU9OQUxfRVJST1IQBxIlCiFNWF9TVEFUVVNf",
|
||||||
"UllfQ09NTVVOSUNBVElPTl9FUlJPUhAFEioKJk1YX1NUQVRVU19DQVRFR09S",
|
"Q0FURUdPUllfU0VDVVJJVFlfRVJST1IQCBIlCiFNWF9TVEFUVVNfQ0FURUdP",
|
||||||
"WV9DT05GSUdVUkFUSU9OX0VSUk9SEAYSKAokTVhfU1RBVFVTX0NBVEVHT1JZ",
|
"UllfU09GVFdBUkVfRVJST1IQCRIiCh5NWF9TVEFUVVNfQ0FURUdPUllfT1RI",
|
||||||
"X09QRVJBVElPTkFMX0VSUk9SEAcSJQohTVhfU1RBVFVTX0NBVEVHT1JZX1NF",
|
"RVJfRVJST1IQCirKAgoOTXhTdGF0dXNTb3VyY2USIAocTVhfU1RBVFVTX1NP",
|
||||||
"Q1VSSVRZX0VSUk9SEAgSJQohTVhfU1RBVFVTX0NBVEVHT1JZX1NPRlRXQVJF",
|
"VVJDRV9VTlNQRUNJRklFRBAAEhwKGE1YX1NUQVRVU19TT1VSQ0VfVU5LTk9X",
|
||||||
"X0VSUk9SEAkSIgoeTVhfU1RBVFVTX0NBVEVHT1JZX09USEVSX0VSUk9SEAoq",
|
"ThABEiMKH01YX1NUQVRVU19TT1VSQ0VfUkVRVUVTVElOR19MTVgQAhIjCh9N",
|
||||||
"ygIKDk14U3RhdHVzU291cmNlEiAKHE1YX1NUQVRVU19TT1VSQ0VfVU5TUEVD",
|
"WF9TVEFUVVNfU09VUkNFX1JFU1BPTkRJTkdfTE1YEAMSIwofTVhfU1RBVFVT",
|
||||||
"SUZJRUQQABIcChhNWF9TVEFUVVNfU09VUkNFX1VOS05PV04QARIjCh9NWF9T",
|
"X1NPVVJDRV9SRVFVRVNUSU5HX05NWBAEEiMKH01YX1NUQVRVU19TT1VSQ0Vf",
|
||||||
"VEFUVVNfU09VUkNFX1JFUVVFU1RJTkdfTE1YEAISIwofTVhfU1RBVFVTX1NP",
|
"UkVTUE9ORElOR19OTVgQBRIxCi1NWF9TVEFUVVNfU09VUkNFX1JFUVVFU1RJ",
|
||||||
"VVJDRV9SRVNQT05ESU5HX0xNWBADEiMKH01YX1NUQVRVU19TT1VSQ0VfUkVR",
|
"TkdfQVVUT01BVElPTl9PQkpFQ1QQBhIxCi1NWF9TVEFUVVNfU09VUkNFX1JF",
|
||||||
"VUVTVElOR19OTVgQBBIjCh9NWF9TVEFUVVNfU09VUkNFX1JFU1BPTkRJTkdf",
|
"U1BPTkRJTkdfQVVUT01BVElPTl9PQkpFQ1QQByrdBAoKTXhEYXRhVHlwZRIc",
|
||||||
"Tk1YEAUSMQotTVhfU1RBVFVTX1NPVVJDRV9SRVFVRVNUSU5HX0FVVE9NQVRJ",
|
"ChhNWF9EQVRBX1RZUEVfVU5TUEVDSUZJRUQQABIYChRNWF9EQVRBX1RZUEVf",
|
||||||
"T05fT0JKRUNUEAYSMQotTVhfU1RBVFVTX1NPVVJDRV9SRVNQT05ESU5HX0FV",
|
"VU5LTk9XThABEhgKFE1YX0RBVEFfVFlQRV9OT19EQVRBEAISGAoUTVhfREFU",
|
||||||
"VE9NQVRJT05fT0JKRUNUEAcq3QQKCk14RGF0YVR5cGUSHAoYTVhfREFUQV9U",
|
"QV9UWVBFX0JPT0xFQU4QAxIYChRNWF9EQVRBX1RZUEVfSU5URUdFUhAEEhYK",
|
||||||
"WVBFX1VOU1BFQ0lGSUVEEAASGAoUTVhfREFUQV9UWVBFX1VOS05PV04QARIY",
|
"Ek1YX0RBVEFfVFlQRV9GTE9BVBAFEhcKE01YX0RBVEFfVFlQRV9ET1VCTEUQ",
|
||||||
"ChRNWF9EQVRBX1RZUEVfTk9fREFUQRACEhgKFE1YX0RBVEFfVFlQRV9CT09M",
|
"BhIXChNNWF9EQVRBX1RZUEVfU1RSSU5HEAcSFQoRTVhfREFUQV9UWVBFX1RJ",
|
||||||
"RUFOEAMSGAoUTVhfREFUQV9UWVBFX0lOVEVHRVIQBBIWChJNWF9EQVRBX1RZ",
|
"TUUQCBIdChlNWF9EQVRBX1RZUEVfRUxBUFNFRF9USU1FEAkSHwobTVhfREFU",
|
||||||
"UEVfRkxPQVQQBRIXChNNWF9EQVRBX1RZUEVfRE9VQkxFEAYSFwoTTVhfREFU",
|
"QV9UWVBFX1JFRkVSRU5DRV9UWVBFEAoSHAoYTVhfREFUQV9UWVBFX1NUQVRV",
|
||||||
"QV9UWVBFX1NUUklORxAHEhUKEU1YX0RBVEFfVFlQRV9USU1FEAgSHQoZTVhf",
|
"U19UWVBFEAsSFQoRTVhfREFUQV9UWVBFX0VOVU0QDBItCilNWF9EQVRBX1RZ",
|
||||||
"REFUQV9UWVBFX0VMQVBTRURfVElNRRAJEh8KG01YX0RBVEFfVFlQRV9SRUZF",
|
"UEVfU0VDVVJJVFlfQ0xBU1NJRklDQVRJT05fRU5VTRANEiIKHk1YX0RBVEFf",
|
||||||
"UkVOQ0VfVFlQRRAKEhwKGE1YX0RBVEFfVFlQRV9TVEFUVVNfVFlQRRALEhUK",
|
"VFlQRV9EQVRBX1FVQUxJVFlfVFlQRRAOEh8KG01YX0RBVEFfVFlQRV9RVUFM",
|
||||||
"EU1YX0RBVEFfVFlQRV9FTlVNEAwSLQopTVhfREFUQV9UWVBFX1NFQ1VSSVRZ",
|
"SUZJRURfRU5VTRAPEiEKHU1YX0RBVEFfVFlQRV9RVUFMSUZJRURfU1RSVUNU",
|
||||||
"X0NMQVNTSUZJQ0FUSU9OX0VOVU0QDRIiCh5NWF9EQVRBX1RZUEVfREFUQV9R",
|
"EBASKQolTVhfREFUQV9UWVBFX0lOVEVSTkFUSU9OQUxJWkVEX1NUUklORxAR",
|
||||||
"VUFMSVRZX1RZUEUQDhIfChtNWF9EQVRBX1RZUEVfUVVBTElGSUVEX0VOVU0Q",
|
"EhsKF01YX0RBVEFfVFlQRV9CSUdfU1RSSU5HEBISFAoQTVhfREFUQV9UWVBF",
|
||||||
"DxIhCh1NWF9EQVRBX1RZUEVfUVVBTElGSUVEX1NUUlVDVBAQEikKJU1YX0RB",
|
"X0VORBATKqMDChJQcm90b2NvbFN0YXR1c0NvZGUSJAogUFJPVE9DT0xfU1RB",
|
||||||
"VEFfVFlQRV9JTlRFUk5BVElPTkFMSVpFRF9TVFJJTkcQERIbChdNWF9EQVRB",
|
"VFVTX0NPREVfVU5TUEVDSUZJRUQQABIbChdQUk9UT0NPTF9TVEFUVVNfQ09E",
|
||||||
"X1RZUEVfQklHX1NUUklORxASEhQKEE1YX0RBVEFfVFlQRV9FTkQQEyqjAwoS",
|
"RV9PSxABEigKJFBST1RPQ09MX1NUQVRVU19DT0RFX0lOVkFMSURfUkVRVUVT",
|
||||||
"UHJvdG9jb2xTdGF0dXNDb2RlEiQKIFBST1RPQ09MX1NUQVRVU19DT0RFX1VO",
|
"VBACEioKJlBST1RPQ09MX1NUQVRVU19DT0RFX1NFU1NJT05fTk9UX0ZPVU5E",
|
||||||
"U1BFQ0lGSUVEEAASGwoXUFJPVE9DT0xfU1RBVFVTX0NPREVfT0sQARIoCiRQ",
|
"EAMSKgomUFJPVE9DT0xfU1RBVFVTX0NPREVfU0VTU0lPTl9OT1RfUkVBRFkQ",
|
||||||
"Uk9UT0NPTF9TVEFUVVNfQ09ERV9JTlZBTElEX1JFUVVFU1QQAhIqCiZQUk9U",
|
"BBIrCidQUk9UT0NPTF9TVEFUVVNfQ09ERV9XT1JLRVJfVU5BVkFJTEFCTEUQ",
|
||||||
"T0NPTF9TVEFUVVNfQ09ERV9TRVNTSU9OX05PVF9GT1VORBADEioKJlBST1RP",
|
"BRIgChxQUk9UT0NPTF9TVEFUVVNfQ09ERV9USU1FT1VUEAYSIQodUFJPVE9D",
|
||||||
"Q09MX1NUQVRVU19DT0RFX1NFU1NJT05fTk9UX1JFQURZEAQSKwonUFJPVE9D",
|
"T0xfU1RBVFVTX0NPREVfQ0FOQ0VMRUQQBxIrCidQUk9UT0NPTF9TVEFUVVNf",
|
||||||
"T0xfU1RBVFVTX0NPREVfV09SS0VSX1VOQVZBSUxBQkxFEAUSIAocUFJPVE9D",
|
"Q09ERV9QUk9UT0NPTF9WSU9MQVRJT04QCBIpCiVQUk9UT0NPTF9TVEFUVVNf",
|
||||||
"T0xfU1RBVFVTX0NPREVfVElNRU9VVBAGEiEKHVBST1RPQ09MX1NUQVRVU19D",
|
"Q09ERV9NWEFDQ0VTU19GQUlMVVJFEAkqvwIKDFNlc3Npb25TdGF0ZRIdChlT",
|
||||||
"T0RFX0NBTkNFTEVEEAcSKwonUFJPVE9DT0xfU1RBVFVTX0NPREVfUFJPVE9D",
|
"RVNTSU9OX1NUQVRFX1VOU1BFQ0lGSUVEEAASGgoWU0VTU0lPTl9TVEFURV9D",
|
||||||
"T0xfVklPTEFUSU9OEAgSKQolUFJPVE9DT0xfU1RBVFVTX0NPREVfTVhBQ0NF",
|
"UkVBVElORxABEiEKHVNFU1NJT05fU1RBVEVfU1RBUlRJTkdfV09SS0VSEAIS",
|
||||||
"U1NfRkFJTFVSRRAJKr8CCgxTZXNzaW9uU3RhdGUSHQoZU0VTU0lPTl9TVEFU",
|
"IgoeU0VTU0lPTl9TVEFURV9XQUlUSU5HX0ZPUl9QSVBFEAMSHQoZU0VTU0lP",
|
||||||
"RV9VTlNQRUNJRklFRBAAEhoKFlNFU1NJT05fU1RBVEVfQ1JFQVRJTkcQARIh",
|
"Tl9TVEFURV9IQU5EU0hBS0lORxAEEiUKIVNFU1NJT05fU1RBVEVfSU5JVElB",
|
||||||
"Ch1TRVNTSU9OX1NUQVRFX1NUQVJUSU5HX1dPUktFUhACEiIKHlNFU1NJT05f",
|
"TElaSU5HX1dPUktFUhAFEhcKE1NFU1NJT05fU1RBVEVfUkVBRFkQBhIZChVT",
|
||||||
"U1RBVEVfV0FJVElOR19GT1JfUElQRRADEh0KGVNFU1NJT05fU1RBVEVfSEFO",
|
"RVNTSU9OX1NUQVRFX0NMT1NJTkcQBxIYChRTRVNTSU9OX1NUQVRFX0NMT1NF",
|
||||||
"RFNIQUtJTkcQBBIlCiFTRVNTSU9OX1NUQVRFX0lOSVRJQUxJWklOR19XT1JL",
|
"RBAIEhkKFVNFU1NJT05fU1RBVEVfRkFVTFRFRBAJMoIDCg9NeEFjY2Vzc0dh",
|
||||||
"RVIQBRIXChNTRVNTSU9OX1NUQVRFX1JFQURZEAYSGQoVU0VTU0lPTl9TVEFU",
|
"dGV3YXkSXQoLT3BlblNlc3Npb24SJy5teGFjY2Vzc19nYXRld2F5LnYxLk9w",
|
||||||
"RV9DTE9TSU5HEAcSGAoUU0VTU0lPTl9TVEFURV9DTE9TRUQQCBIZChVTRVNT",
|
"ZW5TZXNzaW9uUmVxdWVzdBolLm14YWNjZXNzX2dhdGV3YXkudjEuT3BlblNl",
|
||||||
"SU9OX1NUQVRFX0ZBVUxURUQQCTKCAwoPTXhBY2Nlc3NHYXRld2F5El0KC09w",
|
"c3Npb25SZXBseRJgCgxDbG9zZVNlc3Npb24SKC5teGFjY2Vzc19nYXRld2F5",
|
||||||
"ZW5TZXNzaW9uEicubXhhY2Nlc3NfZ2F0ZXdheS52MS5PcGVuU2Vzc2lvblJl",
|
"LnYxLkNsb3NlU2Vzc2lvblJlcXVlc3QaJi5teGFjY2Vzc19nYXRld2F5LnYx",
|
||||||
"cXVlc3QaJS5teGFjY2Vzc19nYXRld2F5LnYxLk9wZW5TZXNzaW9uUmVwbHkS",
|
"LkNsb3NlU2Vzc2lvblJlcGx5ElQKBkludm9rZRIlLm14YWNjZXNzX2dhdGV3",
|
||||||
"YAoMQ2xvc2VTZXNzaW9uEigubXhhY2Nlc3NfZ2F0ZXdheS52MS5DbG9zZVNl",
|
"YXkudjEuTXhDb21tYW5kUmVxdWVzdBojLm14YWNjZXNzX2dhdGV3YXkudjEu",
|
||||||
"c3Npb25SZXF1ZXN0GiYubXhhY2Nlc3NfZ2F0ZXdheS52MS5DbG9zZVNlc3Np",
|
"TXhDb21tYW5kUmVwbHkSWAoMU3RyZWFtRXZlbnRzEigubXhhY2Nlc3NfZ2F0",
|
||||||
"b25SZXBseRJUCgZJbnZva2USJS5teGFjY2Vzc19nYXRld2F5LnYxLk14Q29t",
|
"ZXdheS52MS5TdHJlYW1FdmVudHNSZXF1ZXN0GhwubXhhY2Nlc3NfZ2F0ZXdh",
|
||||||
"bWFuZFJlcXVlc3QaIy5teGFjY2Vzc19nYXRld2F5LnYxLk14Q29tbWFuZFJl",
|
"eS52MS5NeEV2ZW50MAFCHKoCGU14R2F0ZXdheS5Db250cmFjdHMuUHJvdG9i",
|
||||||
"cGx5ElgKDFN0cmVhbUV2ZW50cxIoLm14YWNjZXNzX2dhdGV3YXkudjEuU3Ry",
|
"BnByb3RvMw=="));
|
||||||
"ZWFtRXZlbnRzUmVxdWVzdBocLm14YWNjZXNzX2dhdGV3YXkudjEuTXhFdmVu",
|
|
||||||
"dDABQhyqAhlNeEdhdGV3YXkuQ29udHJhY3RzLlByb3RvYgZwcm90bzM="));
|
|
||||||
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
|
descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
|
||||||
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, },
|
new pbr::FileDescriptor[] { global::Google.Protobuf.WellKnownTypes.DurationReflection.Descriptor, global::Google.Protobuf.WellKnownTypes.TimestampReflection.Descriptor, },
|
||||||
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::MxGateway.Contracts.Proto.MxCommandKind), typeof(global::MxGateway.Contracts.Proto.MxEventFamily), typeof(global::MxGateway.Contracts.Proto.MxStatusCategory), typeof(global::MxGateway.Contracts.Proto.MxStatusSource), typeof(global::MxGateway.Contracts.Proto.MxDataType), typeof(global::MxGateway.Contracts.Proto.ProtocolStatusCode), typeof(global::MxGateway.Contracts.Proto.SessionState), }, null, new pbr::GeneratedClrTypeInfo[] {
|
new pbr::GeneratedClrTypeInfo(new[] {typeof(global::MxGateway.Contracts.Proto.MxCommandKind), typeof(global::MxGateway.Contracts.Proto.MxEventFamily), typeof(global::MxGateway.Contracts.Proto.MxStatusCategory), typeof(global::MxGateway.Contracts.Proto.MxStatusSource), typeof(global::MxGateway.Contracts.Proto.MxDataType), typeof(global::MxGateway.Contracts.Proto.ProtocolStatusCode), typeof(global::MxGateway.Contracts.Proto.SessionState), }, null, new pbr::GeneratedClrTypeInfo[] {
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OpenSessionRequest), global::MxGateway.Contracts.Proto.OpenSessionRequest.Parser, new[]{ "RequestedBackend", "ClientSessionName", "ClientCorrelationId", "CommandTimeout" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OpenSessionRequest), global::MxGateway.Contracts.Proto.OpenSessionRequest.Parser, new[]{ "RequestedBackend", "ClientSessionName", "ClientCorrelationId", "CommandTimeout" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OpenSessionReply), global::MxGateway.Contracts.Proto.OpenSessionReply.Parser, new[]{ "SessionId", "BackendName", "WorkerProcessId", "WorkerProtocolVersion", "Capabilities", "DefaultCommandTimeout", "ProtocolStatus", "GatewayProtocolVersion" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.OpenSessionReply), global::MxGateway.Contracts.Proto.OpenSessionReply.Parser, new[]{ "SessionId", "BackendName", "WorkerProcessId", "WorkerProtocolVersion", "Capabilities", "DefaultCommandTimeout", "ProtocolStatus" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.CloseSessionRequest), global::MxGateway.Contracts.Proto.CloseSessionRequest.Parser, new[]{ "SessionId", "ClientCorrelationId" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.CloseSessionRequest), global::MxGateway.Contracts.Proto.CloseSessionRequest.Parser, new[]{ "SessionId", "ClientCorrelationId" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.CloseSessionReply), global::MxGateway.Contracts.Proto.CloseSessionReply.Parser, new[]{ "SessionId", "FinalState", "ProtocolStatus" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.CloseSessionReply), global::MxGateway.Contracts.Proto.CloseSessionReply.Parser, new[]{ "SessionId", "FinalState", "ProtocolStatus" }, null, null, null, null),
|
||||||
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.StreamEventsRequest), global::MxGateway.Contracts.Proto.StreamEventsRequest.Parser, new[]{ "SessionId", "AfterWorkerSequence" }, null, null, null, null),
|
new pbr::GeneratedClrTypeInfo(typeof(global::MxGateway.Contracts.Proto.StreamEventsRequest), global::MxGateway.Contracts.Proto.StreamEventsRequest.Parser, new[]{ "SessionId", "AfterWorkerSequence" }, null, null, null, null),
|
||||||
@@ -841,7 +841,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
capabilities_ = other.capabilities_.Clone();
|
capabilities_ = other.capabilities_.Clone();
|
||||||
defaultCommandTimeout_ = other.defaultCommandTimeout_ != null ? other.defaultCommandTimeout_.Clone() : null;
|
defaultCommandTimeout_ = other.defaultCommandTimeout_ != null ? other.defaultCommandTimeout_.Clone() : null;
|
||||||
protocolStatus_ = other.protocolStatus_ != null ? other.protocolStatus_.Clone() : null;
|
protocolStatus_ = other.protocolStatus_ != null ? other.protocolStatus_.Clone() : null;
|
||||||
gatewayProtocolVersion_ = other.gatewayProtocolVersion_;
|
|
||||||
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
_unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,23 +933,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Field number for the "gateway_protocol_version" field.</summary>
|
|
||||||
public const int GatewayProtocolVersionFieldNumber = 8;
|
|
||||||
private uint gatewayProtocolVersion_;
|
|
||||||
/// <summary>
|
|
||||||
/// Public gateway contract version implemented by this endpoint. Clients use
|
|
||||||
/// this value to reject incompatible generated-code inputs before issuing
|
|
||||||
/// command-specific MXAccess calls.
|
|
||||||
/// </summary>
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
|
||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
|
||||||
public uint GatewayProtocolVersion {
|
|
||||||
get { return gatewayProtocolVersion_; }
|
|
||||||
set {
|
|
||||||
gatewayProtocolVersion_ = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||||
public override bool Equals(object other) {
|
public override bool Equals(object other) {
|
||||||
@@ -973,7 +955,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
if(!capabilities_.Equals(other.capabilities_)) return false;
|
if(!capabilities_.Equals(other.capabilities_)) return false;
|
||||||
if (!object.Equals(DefaultCommandTimeout, other.DefaultCommandTimeout)) return false;
|
if (!object.Equals(DefaultCommandTimeout, other.DefaultCommandTimeout)) return false;
|
||||||
if (!object.Equals(ProtocolStatus, other.ProtocolStatus)) return false;
|
if (!object.Equals(ProtocolStatus, other.ProtocolStatus)) return false;
|
||||||
if (GatewayProtocolVersion != other.GatewayProtocolVersion) return false;
|
|
||||||
return Equals(_unknownFields, other._unknownFields);
|
return Equals(_unknownFields, other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -988,7 +969,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
hash ^= capabilities_.GetHashCode();
|
hash ^= capabilities_.GetHashCode();
|
||||||
if (defaultCommandTimeout_ != null) hash ^= DefaultCommandTimeout.GetHashCode();
|
if (defaultCommandTimeout_ != null) hash ^= DefaultCommandTimeout.GetHashCode();
|
||||||
if (protocolStatus_ != null) hash ^= ProtocolStatus.GetHashCode();
|
if (protocolStatus_ != null) hash ^= ProtocolStatus.GetHashCode();
|
||||||
if (GatewayProtocolVersion != 0) hash ^= GatewayProtocolVersion.GetHashCode();
|
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
hash ^= _unknownFields.GetHashCode();
|
hash ^= _unknownFields.GetHashCode();
|
||||||
}
|
}
|
||||||
@@ -1032,10 +1012,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
output.WriteRawTag(58);
|
output.WriteRawTag(58);
|
||||||
output.WriteMessage(ProtocolStatus);
|
output.WriteMessage(ProtocolStatus);
|
||||||
}
|
}
|
||||||
if (GatewayProtocolVersion != 0) {
|
|
||||||
output.WriteRawTag(64);
|
|
||||||
output.WriteUInt32(GatewayProtocolVersion);
|
|
||||||
}
|
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
_unknownFields.WriteTo(output);
|
_unknownFields.WriteTo(output);
|
||||||
}
|
}
|
||||||
@@ -1071,10 +1047,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
output.WriteRawTag(58);
|
output.WriteRawTag(58);
|
||||||
output.WriteMessage(ProtocolStatus);
|
output.WriteMessage(ProtocolStatus);
|
||||||
}
|
}
|
||||||
if (GatewayProtocolVersion != 0) {
|
|
||||||
output.WriteRawTag(64);
|
|
||||||
output.WriteUInt32(GatewayProtocolVersion);
|
|
||||||
}
|
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
_unknownFields.WriteTo(ref output);
|
_unknownFields.WriteTo(ref output);
|
||||||
}
|
}
|
||||||
@@ -1104,9 +1076,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
if (protocolStatus_ != null) {
|
if (protocolStatus_ != null) {
|
||||||
size += 1 + pb::CodedOutputStream.ComputeMessageSize(ProtocolStatus);
|
size += 1 + pb::CodedOutputStream.ComputeMessageSize(ProtocolStatus);
|
||||||
}
|
}
|
||||||
if (GatewayProtocolVersion != 0) {
|
|
||||||
size += 1 + pb::CodedOutputStream.ComputeUInt32Size(GatewayProtocolVersion);
|
|
||||||
}
|
|
||||||
if (_unknownFields != null) {
|
if (_unknownFields != null) {
|
||||||
size += _unknownFields.CalculateSize();
|
size += _unknownFields.CalculateSize();
|
||||||
}
|
}
|
||||||
@@ -1144,9 +1113,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
}
|
}
|
||||||
ProtocolStatus.MergeFrom(other.ProtocolStatus);
|
ProtocolStatus.MergeFrom(other.ProtocolStatus);
|
||||||
}
|
}
|
||||||
if (other.GatewayProtocolVersion != 0) {
|
|
||||||
GatewayProtocolVersion = other.GatewayProtocolVersion;
|
|
||||||
}
|
|
||||||
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
_unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1200,10 +1166,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
input.ReadMessage(ProtocolStatus);
|
input.ReadMessage(ProtocolStatus);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 64: {
|
|
||||||
GatewayProtocolVersion = input.ReadUInt32();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -1257,10 +1219,6 @@ namespace MxGateway.Contracts.Proto {
|
|||||||
input.ReadMessage(ProtocolStatus);
|
input.ReadMessage(ProtocolStatus);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 64: {
|
|
||||||
GatewayProtocolVersion = input.ReadUInt32();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>net10.0;net48</TargetFrameworks>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ message OpenSessionReply {
|
|||||||
repeated string capabilities = 5;
|
repeated string capabilities = 5;
|
||||||
google.protobuf.Duration default_command_timeout = 6;
|
google.protobuf.Duration default_command_timeout = 6;
|
||||||
ProtocolStatus protocol_status = 7;
|
ProtocolStatus protocol_status = 7;
|
||||||
// Public gateway contract version implemented by this endpoint. Clients use
|
|
||||||
// this value to reject incompatible generated-code inputs before issuing
|
|
||||||
// command-specific MXAccess calls.
|
|
||||||
uint32 gateway_protocol_version = 8;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message CloseSessionRequest {
|
message CloseSessionRequest {
|
||||||
|
|||||||
@@ -3,92 +3,10 @@ namespace MxGateway.IntegrationTests;
|
|||||||
public static class IntegrationTestEnvironment
|
public static class IntegrationTestEnvironment
|
||||||
{
|
{
|
||||||
public const string LiveMxAccessVariableName = "MXGATEWAY_RUN_LIVE_MXACCESS_TESTS";
|
public const string LiveMxAccessVariableName = "MXGATEWAY_RUN_LIVE_MXACCESS_TESTS";
|
||||||
public const string LiveMxAccessWorkerExecutableVariableName = "MXGATEWAY_LIVE_MXACCESS_WORKER_EXE";
|
|
||||||
public const string LiveMxAccessItemVariableName = "MXGATEWAY_LIVE_MXACCESS_ITEM";
|
|
||||||
public const string LiveMxAccessClientNameVariableName = "MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME";
|
|
||||||
public const string LiveMxAccessEventTimeoutSecondsVariableName = "MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS";
|
|
||||||
|
|
||||||
public static bool LiveMxAccessTestsEnabled =>
|
public static bool LiveMxAccessTestsEnabled =>
|
||||||
string.Equals(
|
string.Equals(
|
||||||
Environment.GetEnvironmentVariable(LiveMxAccessVariableName),
|
Environment.GetEnvironmentVariable(LiveMxAccessVariableName),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
public static string LiveMxAccessItem =>
|
|
||||||
GetOptionalEnvironmentVariable(
|
|
||||||
LiveMxAccessItemVariableName,
|
|
||||||
"TestChildObject.TestInt");
|
|
||||||
|
|
||||||
public static string LiveMxAccessClientName =>
|
|
||||||
GetOptionalEnvironmentVariable(
|
|
||||||
LiveMxAccessClientNameVariableName,
|
|
||||||
"MxGateway.IntegrationTests");
|
|
||||||
|
|
||||||
public static TimeSpan LiveMxAccessEventTimeout =>
|
|
||||||
TimeSpan.FromSeconds(GetPositiveIntegerEnvironmentVariable(
|
|
||||||
LiveMxAccessEventTimeoutSecondsVariableName,
|
|
||||||
defaultValue: 15));
|
|
||||||
|
|
||||||
public static string ResolveLiveMxAccessWorkerExecutablePath()
|
|
||||||
{
|
|
||||||
string? configuredPath = Environment.GetEnvironmentVariable(LiveMxAccessWorkerExecutableVariableName);
|
|
||||||
if (!string.IsNullOrWhiteSpace(configuredPath))
|
|
||||||
{
|
|
||||||
return Path.GetFullPath(configuredPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
string repositoryRoot = ResolveRepositoryRoot(AppContext.BaseDirectory);
|
|
||||||
string[] candidatePaths =
|
|
||||||
[
|
|
||||||
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "x86", "Debug", "net48", "MxGateway.Worker.exe"),
|
|
||||||
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "Debug", "net48", "MxGateway.Worker.exe"),
|
|
||||||
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "x86", "Release", "net48", "MxGateway.Worker.exe"),
|
|
||||||
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "Release", "net48", "MxGateway.Worker.exe"),
|
|
||||||
Path.Combine(repositoryRoot, "src", "MxGateway.Worker", "bin", "x86", "Release", "MxGateway.Worker.exe"),
|
|
||||||
];
|
|
||||||
|
|
||||||
return candidatePaths.FirstOrDefault(File.Exists)
|
|
||||||
?? candidatePaths[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetOptionalEnvironmentVariable(
|
|
||||||
string name,
|
|
||||||
string defaultValue)
|
|
||||||
{
|
|
||||||
string? value = Environment.GetEnvironmentVariable(name);
|
|
||||||
return string.IsNullOrWhiteSpace(value)
|
|
||||||
? defaultValue
|
|
||||||
: value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int GetPositiveIntegerEnvironmentVariable(
|
|
||||||
string name,
|
|
||||||
int defaultValue)
|
|
||||||
{
|
|
||||||
string? value = Environment.GetEnvironmentVariable(name);
|
|
||||||
if (int.TryParse(value, out int parsed) && parsed > 0)
|
|
||||||
{
|
|
||||||
return parsed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static string ResolveRepositoryRoot(string startDirectory)
|
|
||||||
{
|
|
||||||
DirectoryInfo? directory = new(startDirectory);
|
|
||||||
while (directory is not null)
|
|
||||||
{
|
|
||||||
if ((Directory.Exists(Path.Combine(directory.FullName, ".git"))
|
|
||||||
|| File.Exists(Path.Combine(directory.FullName, ".git")))
|
|
||||||
&& Directory.Exists(Path.Combine(directory.FullName, "src")))
|
|
||||||
{
|
|
||||||
return directory.FullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = directory.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Directory.GetCurrentDirectory();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,37 +9,4 @@ public sealed class IntegrationTestEnvironmentTests
|
|||||||
"MXGATEWAY_RUN_LIVE_MXACCESS_TESTS",
|
"MXGATEWAY_RUN_LIVE_MXACCESS_TESTS",
|
||||||
IntegrationTestEnvironment.LiveMxAccessVariableName);
|
IntegrationTestEnvironment.LiveMxAccessVariableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void LiveMxAccessWorkerExecutable_UsesDocumentedEnvironmentVariable()
|
|
||||||
{
|
|
||||||
Assert.Equal(
|
|
||||||
"MXGATEWAY_LIVE_MXACCESS_WORKER_EXE",
|
|
||||||
IntegrationTestEnvironment.LiveMxAccessWorkerExecutableVariableName);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ResolveRepositoryRoot_AcceptsGitWorktreeFile()
|
|
||||||
{
|
|
||||||
string temporaryRoot = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
|
|
||||||
string nestedDirectory = Path.Combine(temporaryRoot, "tests", "bin");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(nestedDirectory);
|
|
||||||
Directory.CreateDirectory(Path.Combine(temporaryRoot, "src"));
|
|
||||||
File.WriteAllText(Path.Combine(temporaryRoot, ".git"), "gitdir: ../.git/worktrees/test");
|
|
||||||
|
|
||||||
string repositoryRoot = IntegrationTestEnvironment.ResolveRepositoryRoot(nestedDirectory);
|
|
||||||
|
|
||||||
Assert.Equal(temporaryRoot, repositoryRoot);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (Directory.Exists(temporaryRoot))
|
|
||||||
{
|
|
||||||
Directory.Delete(temporaryRoot, recursive: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace MxGateway.IntegrationTests;
|
|
||||||
|
|
||||||
public sealed class LiveMxAccessFactAttribute : FactAttribute
|
|
||||||
{
|
|
||||||
public LiveMxAccessFactAttribute()
|
|
||||||
{
|
|
||||||
if (!IntegrationTestEnvironment.LiveMxAccessTestsEnabled)
|
|
||||||
{
|
|
||||||
Skip = $"Set {IntegrationTestEnvironment.LiveMxAccessVariableName}=1 to run live MXAccess tests.";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MxGateway.Contracts\MxGateway.Contracts.csproj" />
|
<ProjectReference Include="..\MxGateway.Contracts\MxGateway.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\MxGateway.Server\MxGateway.Server.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,517 +0,0 @@
|
|||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
|
||||||
using Grpc.Core;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using MxGateway.Contracts;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
using MxGateway.Server.Configuration;
|
|
||||||
using MxGateway.Server.Grpc;
|
|
||||||
using MxGateway.Server.Metrics;
|
|
||||||
using MxGateway.Server.Security.Authorization;
|
|
||||||
using MxGateway.Server.Sessions;
|
|
||||||
using MxGateway.Server.Workers;
|
|
||||||
using Xunit.Abstractions;
|
|
||||||
|
|
||||||
namespace MxGateway.IntegrationTests;
|
|
||||||
|
|
||||||
public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|
||||||
{
|
|
||||||
private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(15);
|
|
||||||
private static readonly TimeSpan StreamShutdownTimeout = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
[LiveMxAccessFact]
|
|
||||||
[Trait("Category", "LiveMxAccess")]
|
|
||||||
public async Task GatewaySession_WithLiveWorker_RegistersAdvisesStreamsDataAndCloses()
|
|
||||||
{
|
|
||||||
string workerExecutablePath = IntegrationTestEnvironment.ResolveLiveMxAccessWorkerExecutablePath();
|
|
||||||
Assert.True(
|
|
||||||
File.Exists(workerExecutablePath),
|
|
||||||
$"Live MXAccess worker executable was not found at {workerExecutablePath}. Build the worker or set {IntegrationTestEnvironment.LiveMxAccessWorkerExecutableVariableName}.");
|
|
||||||
|
|
||||||
TestWorkerProcessFactory processFactory = new(output);
|
|
||||||
await using GatewayServiceFixture fixture = new(workerExecutablePath, processFactory, output);
|
|
||||||
|
|
||||||
string? sessionId = null;
|
|
||||||
RecordingServerStreamWriter<MxEvent>? eventWriter = null;
|
|
||||||
Task? streamTask = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
OpenSessionReply openReply = await fixture.Service.OpenSession(
|
|
||||||
new OpenSessionRequest
|
|
||||||
{
|
|
||||||
ClientSessionName = "live-mxaccess-smoke",
|
|
||||||
ClientCorrelationId = "live-open",
|
|
||||||
CommandTimeout = Duration.FromTimeSpan(CommandTimeout),
|
|
||||||
},
|
|
||||||
new TestServerCallContext()).ConfigureAwait(false);
|
|
||||||
|
|
||||||
sessionId = openReply.SessionId;
|
|
||||||
output.WriteLine($"OpenSession status={openReply.ProtocolStatus.Code} session={sessionId} worker_pid={openReply.WorkerProcessId}");
|
|
||||||
Assert.Equal(ProtocolStatusCode.Ok, openReply.ProtocolStatus.Code);
|
|
||||||
Assert.True(openReply.WorkerProcessId > 0);
|
|
||||||
|
|
||||||
eventWriter = new RecordingServerStreamWriter<MxEvent>();
|
|
||||||
streamTask = fixture.Service.StreamEvents(
|
|
||||||
new StreamEventsRequest { SessionId = sessionId },
|
|
||||||
eventWriter,
|
|
||||||
new TestServerCallContext());
|
|
||||||
|
|
||||||
MxCommandReply registerReply = await fixture.Service.Invoke(
|
|
||||||
CreateRegisterRequest(sessionId),
|
|
||||||
new TestServerCallContext()).ConfigureAwait(false);
|
|
||||||
LogReply("Register", registerReply);
|
|
||||||
Assert.Equal(ProtocolStatusCode.Ok, registerReply.ProtocolStatus.Code);
|
|
||||||
Assert.True(registerReply.Register.ServerHandle > 0);
|
|
||||||
|
|
||||||
MxCommandReply addItemReply = await fixture.Service.Invoke(
|
|
||||||
CreateAddItemRequest(sessionId, registerReply.Register.ServerHandle),
|
|
||||||
new TestServerCallContext()).ConfigureAwait(false);
|
|
||||||
LogReply("AddItem", addItemReply);
|
|
||||||
Assert.Equal(ProtocolStatusCode.Ok, addItemReply.ProtocolStatus.Code);
|
|
||||||
Assert.True(addItemReply.AddItem.ItemHandle > 0);
|
|
||||||
|
|
||||||
MxCommandReply adviseReply = await fixture.Service.Invoke(
|
|
||||||
CreateAdviseRequest(
|
|
||||||
sessionId,
|
|
||||||
registerReply.Register.ServerHandle,
|
|
||||||
addItemReply.AddItem.ItemHandle),
|
|
||||||
new TestServerCallContext()).ConfigureAwait(false);
|
|
||||||
LogReply("Advise", adviseReply);
|
|
||||||
Assert.Equal(ProtocolStatusCode.Ok, adviseReply.ProtocolStatus.Code);
|
|
||||||
|
|
||||||
MxEvent dataChange = await eventWriter
|
|
||||||
.WaitForFirstMessageAsync(IntegrationTestEnvironment.LiveMxAccessEventTimeout)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
LogEvent(dataChange);
|
|
||||||
|
|
||||||
Assert.Equal(MxEventFamily.OnDataChange, dataChange.Family);
|
|
||||||
Assert.Equal(sessionId, dataChange.SessionId);
|
|
||||||
Assert.Equal(registerReply.Register.ServerHandle, dataChange.ServerHandle);
|
|
||||||
Assert.Equal(addItemReply.AddItem.ItemHandle, dataChange.ItemHandle);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(sessionId))
|
|
||||||
{
|
|
||||||
await CloseSessionAsync(fixture, sessionId).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (streamTask is not null)
|
|
||||||
{
|
|
||||||
await streamTask.WaitAsync(StreamShutdownTimeout).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
await processFactory.WaitForProcessesAsync(StreamShutdownTimeout).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxCommandRequest CreateRegisterRequest(string sessionId)
|
|
||||||
{
|
|
||||||
return new MxCommandRequest
|
|
||||||
{
|
|
||||||
SessionId = sessionId,
|
|
||||||
ClientCorrelationId = "live-register",
|
|
||||||
Command = new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Register,
|
|
||||||
Register = new RegisterCommand
|
|
||||||
{
|
|
||||||
ClientName = IntegrationTestEnvironment.LiveMxAccessClientName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxCommandRequest CreateAddItemRequest(
|
|
||||||
string sessionId,
|
|
||||||
int serverHandle)
|
|
||||||
{
|
|
||||||
return new MxCommandRequest
|
|
||||||
{
|
|
||||||
SessionId = sessionId,
|
|
||||||
ClientCorrelationId = "live-add-item",
|
|
||||||
Command = new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.AddItem,
|
|
||||||
AddItem = new AddItemCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemDefinition = IntegrationTestEnvironment.LiveMxAccessItem,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxCommandRequest CreateAdviseRequest(
|
|
||||||
string sessionId,
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle)
|
|
||||||
{
|
|
||||||
return new MxCommandRequest
|
|
||||||
{
|
|
||||||
SessionId = sessionId,
|
|
||||||
ClientCorrelationId = "live-advise",
|
|
||||||
Command = new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Advise,
|
|
||||||
Advise = new AdviseCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CloseSessionAsync(
|
|
||||||
GatewayServiceFixture fixture,
|
|
||||||
string sessionId)
|
|
||||||
{
|
|
||||||
CloseSessionReply closeReply = await fixture.Service.CloseSession(
|
|
||||||
new CloseSessionRequest
|
|
||||||
{
|
|
||||||
SessionId = sessionId,
|
|
||||||
ClientCorrelationId = "live-close",
|
|
||||||
},
|
|
||||||
new TestServerCallContext()).ConfigureAwait(false);
|
|
||||||
|
|
||||||
output.WriteLine($"CloseSession status={closeReply.ProtocolStatus.Code} final_state={closeReply.FinalState}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LogReply(
|
|
||||||
string method,
|
|
||||||
MxCommandReply reply)
|
|
||||||
{
|
|
||||||
output.WriteLine(
|
|
||||||
$"{method} status={reply.ProtocolStatus.Code} hresult={reply.Hresult} diagnostic={reply.DiagnosticMessage}");
|
|
||||||
|
|
||||||
foreach (MxStatusProxy status in reply.Statuses)
|
|
||||||
{
|
|
||||||
output.WriteLine(
|
|
||||||
$"{method} mxstatus success={status.Success} category={status.Category} detail={status.Detail} text={status.DiagnosticText}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LogEvent(MxEvent dataChange)
|
|
||||||
{
|
|
||||||
output.WriteLine(
|
|
||||||
$"Event family={dataChange.Family} worker_sequence={dataChange.WorkerSequence} server_handle={dataChange.ServerHandle} item_handle={dataChange.ItemHandle} quality={dataChange.Quality}");
|
|
||||||
output.WriteLine(
|
|
||||||
$"Event value_type={dataChange.Value?.DataType} raw_status={dataChange.RawStatus}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class GatewayServiceFixture : IAsyncDisposable
|
|
||||||
{
|
|
||||||
private readonly GatewayMetrics _metrics = new();
|
|
||||||
private readonly SessionRegistry _registry = new();
|
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
|
||||||
|
|
||||||
public GatewayServiceFixture(
|
|
||||||
string workerExecutablePath,
|
|
||||||
IWorkerProcessFactory processFactory,
|
|
||||||
ITestOutputHelper output)
|
|
||||||
{
|
|
||||||
IOptions<GatewayOptions> options = Options.Create(CreateOptions(workerExecutablePath));
|
|
||||||
_loggerFactory = LoggerFactory.Create(builder => builder.AddProvider(new TestOutputLoggerProvider(output)));
|
|
||||||
WorkerProcessLauncher launcher = new(
|
|
||||||
options,
|
|
||||||
processFactory,
|
|
||||||
new WorkerProcessStartedProbe(),
|
|
||||||
_metrics);
|
|
||||||
SessionWorkerClientFactory workerClientFactory = new(
|
|
||||||
launcher,
|
|
||||||
options,
|
|
||||||
_metrics,
|
|
||||||
_loggerFactory);
|
|
||||||
SessionManager sessionManager = new(
|
|
||||||
_registry,
|
|
||||||
workerClientFactory,
|
|
||||||
options,
|
|
||||||
_metrics,
|
|
||||||
logger: _loggerFactory.CreateLogger<SessionManager>());
|
|
||||||
MxAccessGrpcMapper mapper = new();
|
|
||||||
EventStreamService eventStreamService = new(
|
|
||||||
sessionManager,
|
|
||||||
options,
|
|
||||||
mapper,
|
|
||||||
_metrics,
|
|
||||||
_loggerFactory.CreateLogger<EventStreamService>());
|
|
||||||
|
|
||||||
Service = new MxAccessGatewayService(
|
|
||||||
sessionManager,
|
|
||||||
new GatewayRequestIdentityAccessor(),
|
|
||||||
new MxAccessGrpcRequestValidator(),
|
|
||||||
mapper,
|
|
||||||
eventStreamService,
|
|
||||||
_loggerFactory.CreateLogger<MxAccessGatewayService>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxAccessGatewayService Service { get; }
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
foreach (GatewaySession session in _registry.Snapshot())
|
|
||||||
{
|
|
||||||
await session.DisposeAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_loggerFactory.Dispose();
|
|
||||||
_metrics.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GatewayOptions CreateOptions(string workerExecutablePath)
|
|
||||||
{
|
|
||||||
return new GatewayOptions
|
|
||||||
{
|
|
||||||
Worker = new WorkerOptions
|
|
||||||
{
|
|
||||||
ExecutablePath = workerExecutablePath,
|
|
||||||
StartupTimeoutSeconds = 30,
|
|
||||||
ShutdownTimeoutSeconds = 15,
|
|
||||||
HeartbeatIntervalSeconds = 5,
|
|
||||||
HeartbeatGraceSeconds = 15,
|
|
||||||
MaxMessageBytes = WorkerFrameProtocolOptions.DefaultMaxMessageBytes,
|
|
||||||
RequiredArchitecture = WorkerArchitecture.X86,
|
|
||||||
},
|
|
||||||
Sessions = new SessionOptions
|
|
||||||
{
|
|
||||||
DefaultCommandTimeoutSeconds = 15,
|
|
||||||
MaxSessions = 1,
|
|
||||||
},
|
|
||||||
Events = new EventOptions
|
|
||||||
{
|
|
||||||
QueueCapacity = 32,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class RecordingServerStreamWriter<T> : IServerStreamWriter<T>
|
|
||||||
{
|
|
||||||
private readonly object syncRoot = new();
|
|
||||||
private readonly TaskCompletionSource<T> firstMessage = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
||||||
private readonly List<T> messages = [];
|
|
||||||
|
|
||||||
public IReadOnlyList<T> Messages
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
lock (syncRoot)
|
|
||||||
{
|
|
||||||
return messages.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public WriteOptions? WriteOptions { get; set; }
|
|
||||||
|
|
||||||
public Task WriteAsync(T message)
|
|
||||||
{
|
|
||||||
lock (syncRoot)
|
|
||||||
{
|
|
||||||
messages.Add(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
firstMessage.TrySetResult(message);
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<T> WaitForFirstMessageAsync(TimeSpan timeout)
|
|
||||||
{
|
|
||||||
return await firstMessage.Task.WaitAsync(timeout).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TestServerCallContext(CancellationToken cancellationToken = default) : ServerCallContext
|
|
||||||
{
|
|
||||||
private readonly Metadata requestHeaders = [];
|
|
||||||
private readonly Metadata responseTrailers = [];
|
|
||||||
private readonly Dictionary<object, object> userState = [];
|
|
||||||
private Status status;
|
|
||||||
private WriteOptions? writeOptions;
|
|
||||||
|
|
||||||
protected override string MethodCore => "/mxaccess_gateway.v1.MxAccessGateway/Test";
|
|
||||||
|
|
||||||
protected override string HostCore => "localhost";
|
|
||||||
|
|
||||||
protected override string PeerCore => "ipv4:127.0.0.1:5000";
|
|
||||||
|
|
||||||
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
|
|
||||||
|
|
||||||
protected override Metadata RequestHeadersCore => requestHeaders;
|
|
||||||
|
|
||||||
protected override CancellationToken CancellationTokenCore => cancellationToken;
|
|
||||||
|
|
||||||
protected override Metadata ResponseTrailersCore => responseTrailers;
|
|
||||||
|
|
||||||
protected override Status StatusCore
|
|
||||||
{
|
|
||||||
get => status;
|
|
||||||
set => status = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override WriteOptions? WriteOptionsCore
|
|
||||||
{
|
|
||||||
get => writeOptions;
|
|
||||||
set => writeOptions = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthContext AuthContextCore { get; } = new(
|
|
||||||
string.Empty,
|
|
||||||
new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));
|
|
||||||
|
|
||||||
protected override IDictionary<object, object> UserStateCore => userState;
|
|
||||||
|
|
||||||
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override ContextPropagationToken CreatePropagationTokenCore(
|
|
||||||
ContextPropagationOptions? options)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TestWorkerProcessFactory(ITestOutputHelper output) : IWorkerProcessFactory
|
|
||||||
{
|
|
||||||
private readonly ConcurrentBag<TestWorkerProcess> processes = [];
|
|
||||||
|
|
||||||
public IWorkerProcess Start(ProcessStartInfo startInfo)
|
|
||||||
{
|
|
||||||
startInfo.RedirectStandardError = true;
|
|
||||||
startInfo.RedirectStandardOutput = true;
|
|
||||||
startInfo.UseShellExecute = false;
|
|
||||||
|
|
||||||
Process process = new()
|
|
||||||
{
|
|
||||||
StartInfo = startInfo,
|
|
||||||
EnableRaisingEvents = true,
|
|
||||||
};
|
|
||||||
|
|
||||||
process.OutputDataReceived += (_, args) => WriteWorkerOutput("stdout", args.Data);
|
|
||||||
process.ErrorDataReceived += (_, args) => WriteWorkerOutput("stderr", args.Data);
|
|
||||||
|
|
||||||
if (!process.Start())
|
|
||||||
{
|
|
||||||
process.Dispose();
|
|
||||||
throw new InvalidOperationException("Worker process failed to start.");
|
|
||||||
}
|
|
||||||
|
|
||||||
process.BeginOutputReadLine();
|
|
||||||
process.BeginErrorReadLine();
|
|
||||||
|
|
||||||
TestWorkerProcess workerProcess = new(process);
|
|
||||||
processes.Add(workerProcess);
|
|
||||||
output.WriteLine($"WorkerProcess started pid={workerProcess.Id} path={startInfo.FileName}");
|
|
||||||
|
|
||||||
return workerProcess;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task WaitForProcessesAsync(TimeSpan timeout)
|
|
||||||
{
|
|
||||||
foreach (TestWorkerProcess process in processes)
|
|
||||||
{
|
|
||||||
if (process.HasExited)
|
|
||||||
{
|
|
||||||
output.WriteLine($"WorkerProcess exited pid={process.Id} exit_code={process.ExitCode}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
using CancellationTokenSource timeoutCancellation = new(timeout);
|
|
||||||
await process.WaitForExitAsync(timeoutCancellation.Token).ConfigureAwait(false);
|
|
||||||
output.WriteLine($"WorkerProcess exited pid={process.Id} exit_code={process.ExitCode}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteWorkerOutput(
|
|
||||||
string streamName,
|
|
||||||
string? line)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(line))
|
|
||||||
{
|
|
||||||
output.WriteLine($"worker_{streamName}: {line}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TestWorkerProcess(Process process) : IWorkerProcess
|
|
||||||
{
|
|
||||||
public int Id => process.Id;
|
|
||||||
|
|
||||||
public bool HasExited => process.HasExited;
|
|
||||||
|
|
||||||
public int? ExitCode => process.HasExited ? process.ExitCode : null;
|
|
||||||
|
|
||||||
public async ValueTask WaitForExitAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Kill(bool entireProcessTree)
|
|
||||||
{
|
|
||||||
process.Kill(entireProcessTree);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
process.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TestOutputLoggerProvider(ITestOutputHelper output) : ILoggerProvider
|
|
||||||
{
|
|
||||||
public ILogger CreateLogger(string categoryName)
|
|
||||||
{
|
|
||||||
return new TestOutputLogger(output, categoryName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class TestOutputLogger(
|
|
||||||
ITestOutputHelper output,
|
|
||||||
string categoryName) : ILogger
|
|
||||||
{
|
|
||||||
public IDisposable? BeginScope<TState>(TState state)
|
|
||||||
where TState : notnull
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsEnabled(LogLevel logLevel)
|
|
||||||
{
|
|
||||||
return logLevel >= LogLevel.Information;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Log<TState>(
|
|
||||||
LogLevel logLevel,
|
|
||||||
EventId eventId,
|
|
||||||
TState state,
|
|
||||||
Exception? exception,
|
|
||||||
Func<TState, Exception?, string> formatter)
|
|
||||||
{
|
|
||||||
if (!IsEnabled(logLevel))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
output.WriteLine($"{logLevel} {categoryName}: {formatter(state, exception)}");
|
|
||||||
if (exception is not null)
|
|
||||||
{
|
|
||||||
output.WriteLine(exception.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
namespace MxGateway.Server.Configuration;
|
|
||||||
|
|
||||||
public enum AuthenticationMode
|
|
||||||
{
|
|
||||||
ApiKey,
|
|
||||||
Disabled
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user