Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a7a9e4d11 |
@@ -1,117 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
internal sealed class CliArguments
|
|
||||||
{
|
|
||||||
private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private readonly HashSet<string> _flags = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
public CliArguments(IEnumerable<string> args)
|
|
||||||
{
|
|
||||||
string? pendingName = null;
|
|
||||||
|
|
||||||
foreach (string arg in args)
|
|
||||||
{
|
|
||||||
if (arg.StartsWith("--", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (pendingName is not null)
|
|
||||||
{
|
|
||||||
_flags.Add(pendingName);
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingName = arg[2..];
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pendingName is null)
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Unexpected argument '{arg}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_values[pendingName] = arg;
|
|
||||||
pendingName = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pendingName is not null)
|
|
||||||
{
|
|
||||||
_flags.Add(pendingName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HasFlag(string name)
|
|
||||||
{
|
|
||||||
return _flags.Contains(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? GetOptional(string name)
|
|
||||||
{
|
|
||||||
return _values.TryGetValue(name, out string? value)
|
|
||||||
? value
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetRequired(string name)
|
|
||||||
{
|
|
||||||
string? value = GetOptional(name);
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
throw new ArgumentException($"Missing required option --{name}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int GetInt32(string name, int? defaultValue = null)
|
|
||||||
{
|
|
||||||
string? value = GetOptional(name);
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
if (defaultValue.HasValue)
|
|
||||||
{
|
|
||||||
return defaultValue.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException($"Missing required option --{name}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return int.Parse(value, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
public uint GetUInt32(string name, uint defaultValue)
|
|
||||||
{
|
|
||||||
string? value = GetOptional(name);
|
|
||||||
return string.IsNullOrWhiteSpace(value)
|
|
||||||
? defaultValue
|
|
||||||
: uint.Parse(value, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ulong GetUInt64(string name, ulong defaultValue)
|
|
||||||
{
|
|
||||||
string? value = GetOptional(name);
|
|
||||||
return string.IsNullOrWhiteSpace(value)
|
|
||||||
? defaultValue
|
|
||||||
: ulong.Parse(value, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TimeSpan GetDuration(string name, TimeSpan defaultValue)
|
|
||||||
{
|
|
||||||
string? value = GetOptional(name);
|
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
|
||||||
{
|
|
||||||
return defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.EndsWith("ms", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return TimeSpan.FromMilliseconds(double.Parse(value[..^2], CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.EndsWith('s'))
|
|
||||||
{
|
|
||||||
return TimeSpan.FromSeconds(double.Parse(value[..^1], CultureInfo.InvariantCulture));
|
|
||||||
}
|
|
||||||
|
|
||||||
return TimeSpan.Parse(value, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
public interface IMxGatewayCliClient : IAsyncDisposable
|
|
||||||
{
|
|
||||||
Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
|
|
||||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CancellationToken cancellationToken);
|
|
||||||
}
|
|
||||||
@@ -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,40 +0,0 @@
|
|||||||
using MxGateway.Client;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
internal sealed class MxGatewayCliClientAdapter(MxGatewayClient client) : IMxGatewayCliClient
|
|
||||||
{
|
|
||||||
public Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return client.OpenSessionRawAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return client.CloseSessionRawAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return client.InvokeAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return client.StreamEventsAsync(request, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
return client.DisposeAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
namespace MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
internal static class MxGatewayCliSecretRedactor
|
|
||||||
{
|
|
||||||
public static string Redact(string value, string? apiKey)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
|
|
||||||
{
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Replace(apiKey, "[redacted]", StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,744 +0,0 @@
|
|||||||
using System.Globalization;
|
|
||||||
using System.Text.Json;
|
|
||||||
using Google.Protobuf;
|
|
||||||
using MxGateway.Client;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
public static class MxGatewayClientCli
|
|
||||||
{
|
|
||||||
private static readonly JsonFormatter ProtobufJsonFormatter = JsonFormatter.Default;
|
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
|
||||||
|
|
||||||
public static int Run(
|
|
||||||
string[] args,
|
|
||||||
TextWriter standardOutput,
|
|
||||||
TextWriter standardError)
|
|
||||||
{
|
|
||||||
return RunAsync(args, standardOutput, standardError)
|
|
||||||
.GetAwaiter()
|
|
||||||
.GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Task<int> RunAsync(
|
|
||||||
string[] args,
|
|
||||||
TextWriter standardOutput,
|
|
||||||
TextWriter standardError,
|
|
||||||
Func<MxGatewayClientOptions, IMxGatewayCliClient>? clientFactory = null)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(args);
|
|
||||||
ArgumentNullException.ThrowIfNull(standardOutput);
|
|
||||||
ArgumentNullException.ThrowIfNull(standardError);
|
|
||||||
|
|
||||||
return RunCoreAsync(
|
|
||||||
args,
|
|
||||||
standardOutput,
|
|
||||||
standardError,
|
|
||||||
clientFactory ?? CreateDefaultClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<int> RunCoreAsync(
|
|
||||||
string[] args,
|
|
||||||
TextWriter standardOutput,
|
|
||||||
TextWriter standardError,
|
|
||||||
Func<MxGatewayClientOptions, IMxGatewayCliClient> clientFactory)
|
|
||||||
{
|
|
||||||
if (args.Length is 0 || IsHelp(args[0]))
|
|
||||||
{
|
|
||||||
WriteUsage(standardOutput);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
string command = args[0].ToLowerInvariant();
|
|
||||||
CliArguments arguments = new(args.Skip(1));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (command is "version")
|
|
||||||
{
|
|
||||||
WriteVersion(arguments, standardOutput);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsKnownGatewayCommand(command))
|
|
||||||
{
|
|
||||||
return WriteUnknownCommand(command, standardError);
|
|
||||||
}
|
|
||||||
|
|
||||||
await using IMxGatewayCliClient client = clientFactory(CreateOptions(arguments));
|
|
||||||
using CancellationTokenSource cancellation = CreateCancellation(arguments);
|
|
||||||
|
|
||||||
return command switch
|
|
||||||
{
|
|
||||||
"open-session" => await OpenSessionAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"close-session" => await CloseSessionAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"ping" => await PingAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"register" => await RegisterAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"add-item" => await AddItemAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"advise" => await AdviseAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"stream-events" => await StreamEventsAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"write" => await WriteAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"write2" => await Write2Async(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
"smoke" => await SmokeAsync(arguments, client, standardOutput, cancellation.Token)
|
|
||||||
.ConfigureAwait(false),
|
|
||||||
_ => WriteUnknownCommand(command, standardError),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
|
||||||
{
|
|
||||||
string? apiKey = arguments.GetOptional("api-key");
|
|
||||||
string message = MxGatewayCliSecretRedactor.Redact(exception.Message, apiKey);
|
|
||||||
|
|
||||||
if (arguments.HasFlag("json"))
|
|
||||||
{
|
|
||||||
standardError.WriteLine(JsonSerializer.Serialize(
|
|
||||||
new { error = message, type = exception.GetType().Name },
|
|
||||||
JsonOptions));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
standardError.WriteLine(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IMxGatewayCliClient CreateDefaultClient(MxGatewayClientOptions options)
|
|
||||||
{
|
|
||||||
return new MxGatewayCliClientAdapter(MxGatewayClient.Create(options));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxGatewayClientOptions CreateOptions(CliArguments arguments)
|
|
||||||
{
|
|
||||||
string endpoint = arguments.GetOptional("endpoint")
|
|
||||||
?? Environment.GetEnvironmentVariable("MXGATEWAY_ENDPOINT")
|
|
||||||
?? "http://localhost:5000";
|
|
||||||
|
|
||||||
string apiKey = ResolveApiKey(arguments);
|
|
||||||
|
|
||||||
return new MxGatewayClientOptions
|
|
||||||
{
|
|
||||||
Endpoint = new Uri(endpoint, UriKind.Absolute),
|
|
||||||
ApiKey = apiKey,
|
|
||||||
UseTls = arguments.HasFlag("tls")
|
|
||||||
|| endpoint.StartsWith("https://", StringComparison.OrdinalIgnoreCase),
|
|
||||||
DefaultCallTimeout = arguments.GetDuration("timeout", TimeSpan.FromSeconds(30)),
|
|
||||||
ConnectTimeout = arguments.GetDuration("connect-timeout", TimeSpan.FromSeconds(10)),
|
|
||||||
CaCertificatePath = arguments.GetOptional("ca-file"),
|
|
||||||
ServerNameOverride = arguments.GetOptional("server-name"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ResolveApiKey(CliArguments arguments)
|
|
||||||
{
|
|
||||||
string? apiKey = arguments.GetOptional("api-key");
|
|
||||||
if (!string.IsNullOrWhiteSpace(apiKey))
|
|
||||||
{
|
|
||||||
return apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
string apiKeyEnvironmentName = arguments.GetOptional("api-key-env")
|
|
||||||
?? "MXGATEWAY_API_KEY";
|
|
||||||
|
|
||||||
apiKey = Environment.GetEnvironmentVariable(apiKeyEnvironmentName);
|
|
||||||
if (!string.IsNullOrWhiteSpace(apiKey))
|
|
||||||
{
|
|
||||||
return apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"Gateway API key is required. Pass --api-key or set {apiKeyEnvironmentName}.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CancellationTokenSource CreateCancellation(CliArguments arguments)
|
|
||||||
{
|
|
||||||
var cancellation = new CancellationTokenSource();
|
|
||||||
TimeSpan timeout = arguments.GetDuration("timeout", TimeSpan.FromSeconds(30));
|
|
||||||
cancellation.CancelAfter(timeout);
|
|
||||||
return cancellation;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> OpenSessionAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return WriteReplyAsync(
|
|
||||||
client.OpenSessionAsync(
|
|
||||||
new OpenSessionRequest
|
|
||||||
{
|
|
||||||
ClientSessionName = arguments.GetOptional("client-name") ?? "mxgw-dotnet-cli",
|
|
||||||
ClientCorrelationId = CreateCorrelationId(),
|
|
||||||
RequestedBackend = arguments.GetOptional("backend") ?? string.Empty,
|
|
||||||
},
|
|
||||||
cancellationToken),
|
|
||||||
arguments,
|
|
||||||
output);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> CloseSessionAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return WriteReplyAsync(
|
|
||||||
client.CloseSessionAsync(
|
|
||||||
new CloseSessionRequest
|
|
||||||
{
|
|
||||||
SessionId = arguments.GetRequired("session-id"),
|
|
||||||
ClientCorrelationId = CreateCorrelationId(),
|
|
||||||
},
|
|
||||||
cancellationToken),
|
|
||||||
arguments,
|
|
||||||
output);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> PingAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Ping,
|
|
||||||
Ping = new PingCommand { Message = arguments.GetOptional("message") ?? "ping" },
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> RegisterAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Register,
|
|
||||||
Register = new RegisterCommand { ClientName = arguments.GetOptional("client-name") ?? "mxgw-dotnet-cli" },
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> AddItemAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.AddItem,
|
|
||||||
AddItem = new AddItemCommand
|
|
||||||
{
|
|
||||||
ServerHandle = arguments.GetInt32("server-handle"),
|
|
||||||
ItemDefinition = arguments.GetRequired("item"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> AdviseAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Advise,
|
|
||||||
Advise = new AdviseCommand
|
|
||||||
{
|
|
||||||
ServerHandle = arguments.GetInt32("server-handle"),
|
|
||||||
ItemHandle = arguments.GetInt32("item-handle"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> WriteAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Write,
|
|
||||||
Write = new WriteCommand
|
|
||||||
{
|
|
||||||
ServerHandle = arguments.GetInt32("server-handle"),
|
|
||||||
ItemHandle = arguments.GetInt32("item-handle"),
|
|
||||||
UserId = arguments.GetInt32("user-id", 0),
|
|
||||||
Value = ParseValue(arguments),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Task<int> Write2Async(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return InvokeAndWriteAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
output,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Write2,
|
|
||||||
Write2 = new Write2Command
|
|
||||||
{
|
|
||||||
ServerHandle = arguments.GetInt32("server-handle"),
|
|
||||||
ItemHandle = arguments.GetInt32("item-handle"),
|
|
||||||
UserId = arguments.GetInt32("user-id", 0),
|
|
||||||
Value = ParseValue(arguments),
|
|
||||||
TimestampValue = ParseTimestampValue(arguments),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<int> StreamEventsAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var events = new List<MxEvent>();
|
|
||||||
uint maxEvents = arguments.GetUInt32("max-events", 0);
|
|
||||||
uint eventCount = 0;
|
|
||||||
var request = new StreamEventsRequest
|
|
||||||
{
|
|
||||||
SessionId = arguments.GetRequired("session-id"),
|
|
||||||
AfterWorkerSequence = arguments.GetUInt64("after-worker-sequence", 0),
|
|
||||||
};
|
|
||||||
|
|
||||||
await foreach (MxEvent gatewayEvent in client.StreamEventsAsync(request, cancellationToken)
|
|
||||||
.WithCancellation(cancellationToken)
|
|
||||||
.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
if (arguments.HasFlag("json"))
|
|
||||||
{
|
|
||||||
events.Add(gatewayEvent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
output.WriteLine(ProtobufJsonFormatter.Format(gatewayEvent));
|
|
||||||
}
|
|
||||||
|
|
||||||
eventCount++;
|
|
||||||
if (maxEvents > 0 && eventCount >= maxEvents)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arguments.HasFlag("json"))
|
|
||||||
{
|
|
||||||
output.WriteLine(JsonSerializer.Serialize(
|
|
||||||
new { events = events.Select(EventToJsonElement).ToArray() },
|
|
||||||
JsonOptions));
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<int> SmokeAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
OpenSessionReply? openReply = null;
|
|
||||||
CloseSessionReply? closeReply = null;
|
|
||||||
var commandReplies = new List<MxCommandReply>();
|
|
||||||
var events = new List<MxEvent>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
openReply = await client.OpenSessionAsync(
|
|
||||||
new OpenSessionRequest
|
|
||||||
{
|
|
||||||
ClientSessionName = arguments.GetOptional("client-name") ?? "mxgw-dotnet-smoke",
|
|
||||||
ClientCorrelationId = CreateCorrelationId(),
|
|
||||||
},
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
int serverHandle = await InvokeForHandleAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
openReply.SessionId,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Register,
|
|
||||||
Register = new RegisterCommand { ClientName = arguments.GetOptional("client-name") ?? "mxgw-dotnet-smoke" },
|
|
||||||
},
|
|
||||||
reply => reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value,
|
|
||||||
commandReplies,
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
int itemHandle = await InvokeForHandleAsync(
|
|
||||||
arguments,
|
|
||||||
client,
|
|
||||||
openReply.SessionId,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.AddItem,
|
|
||||||
AddItem = new AddItemCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemDefinition = arguments.GetRequired("item"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
reply => reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value,
|
|
||||||
commandReplies,
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
commandReplies.Add(await InvokeAndEnsureAsync(
|
|
||||||
client,
|
|
||||||
CreateCommandRequest(
|
|
||||||
openReply.SessionId,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Advise,
|
|
||||||
Advise = new AdviseCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false));
|
|
||||||
|
|
||||||
if (arguments.GetOptional("value") is not null)
|
|
||||||
{
|
|
||||||
commandReplies.Add(await InvokeAndEnsureAsync(
|
|
||||||
client,
|
|
||||||
CreateCommandRequest(
|
|
||||||
openReply.SessionId,
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Write,
|
|
||||||
Write = new WriteCommand
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
UserId = arguments.GetInt32("user-id", 0),
|
|
||||||
Value = ParseValue(arguments),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
using CancellationTokenSource streamCancellation = CancellationTokenSource
|
|
||||||
.CreateLinkedTokenSource(cancellationToken);
|
|
||||||
streamCancellation.CancelAfter(arguments.GetDuration(
|
|
||||||
"event-timeout",
|
|
||||||
TimeSpan.FromSeconds(2)));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await foreach (MxEvent gatewayEvent in client.StreamEventsAsync(
|
|
||||||
new StreamEventsRequest { SessionId = openReply.SessionId },
|
|
||||||
streamCancellation.Token)
|
|
||||||
.WithCancellation(streamCancellation.Token)
|
|
||||||
.ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
events.Add(gatewayEvent);
|
|
||||||
if (events.Count >= arguments.GetUInt32("max-events", 1))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (openReply is not null)
|
|
||||||
{
|
|
||||||
closeReply = await client.CloseSessionAsync(
|
|
||||||
new CloseSessionRequest
|
|
||||||
{
|
|
||||||
SessionId = openReply.SessionId,
|
|
||||||
ClientCorrelationId = CreateCorrelationId(),
|
|
||||||
},
|
|
||||||
CancellationToken.None)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WriteSmokeResult(arguments, output, openReply, closeReply, commandReplies, events);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<int> InvokeAndWriteAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
TextWriter output,
|
|
||||||
MxCommand command,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await InvokeAndEnsureAsync(
|
|
||||||
client,
|
|
||||||
CreateCommandRequest(arguments.GetRequired("session-id"), command),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
|
|
||||||
WriteMessage(arguments, output, reply);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<int> InvokeForHandleAsync(
|
|
||||||
CliArguments arguments,
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
string sessionId,
|
|
||||||
MxCommand command,
|
|
||||||
Func<MxCommandReply, int> handleSelector,
|
|
||||||
List<MxCommandReply> replies,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await InvokeAndEnsureAsync(
|
|
||||||
client,
|
|
||||||
CreateCommandRequest(sessionId, command),
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
replies.Add(reply);
|
|
||||||
return handleSelector(reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<MxCommandReply> InvokeAndEnsureAsync(
|
|
||||||
IMxGatewayCliClient client,
|
|
||||||
MxCommandRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await client.InvokeAsync(request, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxCommandRequest CreateCommandRequest(
|
|
||||||
string sessionId,
|
|
||||||
MxCommand command)
|
|
||||||
{
|
|
||||||
return new MxCommandRequest
|
|
||||||
{
|
|
||||||
SessionId = sessionId,
|
|
||||||
ClientCorrelationId = CreateCorrelationId(),
|
|
||||||
Command = command,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<int> WriteReplyAsync<TReply>(
|
|
||||||
Task<TReply> replyTask,
|
|
||||||
CliArguments arguments,
|
|
||||||
TextWriter output)
|
|
||||||
where TReply : IMessage
|
|
||||||
{
|
|
||||||
TReply reply = await replyTask.ConfigureAwait(false);
|
|
||||||
WriteMessage(arguments, output, reply);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteVersion(CliArguments arguments, TextWriter output)
|
|
||||||
{
|
|
||||||
if (arguments.HasFlag("json"))
|
|
||||||
{
|
|
||||||
output.WriteLine(JsonSerializer.Serialize(
|
|
||||||
new
|
|
||||||
{
|
|
||||||
gatewayProtocolVersion = MxGatewayClientContractInfo.GatewayProtocolVersion,
|
|
||||||
workerProtocolVersion = MxGatewayClientContractInfo.WorkerProtocolVersion,
|
|
||||||
},
|
|
||||||
JsonOptions));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
output.WriteLine(
|
|
||||||
$"gateway-protocol={MxGatewayClientContractInfo.GatewayProtocolVersion}");
|
|
||||||
output.WriteLine(
|
|
||||||
$"worker-protocol={MxGatewayClientContractInfo.WorkerProtocolVersion}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteMessage(
|
|
||||||
CliArguments arguments,
|
|
||||||
TextWriter output,
|
|
||||||
IMessage message)
|
|
||||||
{
|
|
||||||
output.WriteLine(arguments.HasFlag("json")
|
|
||||||
? ProtobufJsonFormatter.Format(message)
|
|
||||||
: message.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteSmokeResult(
|
|
||||||
CliArguments arguments,
|
|
||||||
TextWriter output,
|
|
||||||
OpenSessionReply? openReply,
|
|
||||||
CloseSessionReply? closeReply,
|
|
||||||
IReadOnlyList<MxCommandReply> commandReplies,
|
|
||||||
IReadOnlyList<MxEvent> events)
|
|
||||||
{
|
|
||||||
if (!arguments.HasFlag("json"))
|
|
||||||
{
|
|
||||||
output.WriteLine($"session-id={openReply?.SessionId}");
|
|
||||||
output.WriteLine($"commands={commandReplies.Count}");
|
|
||||||
output.WriteLine($"events={events.Count}");
|
|
||||||
output.WriteLine($"closed={closeReply is not null}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
output.WriteLine(JsonSerializer.Serialize(
|
|
||||||
new
|
|
||||||
{
|
|
||||||
sessionId = openReply?.SessionId,
|
|
||||||
closed = closeReply is not null,
|
|
||||||
commandReplies = commandReplies.Select(CommandReplyToJsonElement).ToArray(),
|
|
||||||
events = events.Select(EventToJsonElement).ToArray(),
|
|
||||||
},
|
|
||||||
JsonOptions));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JsonElement CommandReplyToJsonElement(MxCommandReply reply)
|
|
||||||
{
|
|
||||||
return JsonDocument.Parse(ProtobufJsonFormatter.Format(reply)).RootElement.Clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JsonElement EventToJsonElement(MxEvent gatewayEvent)
|
|
||||||
{
|
|
||||||
return JsonDocument.Parse(ProtobufJsonFormatter.Format(gatewayEvent)).RootElement.Clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxValue ParseValue(CliArguments arguments)
|
|
||||||
{
|
|
||||||
string type = arguments.GetRequired("type").ToLowerInvariant();
|
|
||||||
string value = arguments.GetRequired("value");
|
|
||||||
string[] values = value.Split(',', StringSplitOptions.TrimEntries);
|
|
||||||
|
|
||||||
return type switch
|
|
||||||
{
|
|
||||||
"bool" or "boolean" => bool.Parse(value).ToMxValue(),
|
|
||||||
"bool-array" or "boolean-array" => values.Select(bool.Parse).ToArray().ToMxValue(),
|
|
||||||
"int32" or "integer" => int.Parse(value, CultureInfo.InvariantCulture).ToMxValue(),
|
|
||||||
"int32-array" or "integer-array" => values.Select(item => int.Parse(item, CultureInfo.InvariantCulture)).ToArray().ToMxValue(),
|
|
||||||
"int64" => long.Parse(value, CultureInfo.InvariantCulture).ToMxValue(),
|
|
||||||
"int64-array" => values.Select(item => long.Parse(item, CultureInfo.InvariantCulture)).ToArray().ToMxValue(),
|
|
||||||
"float" => float.Parse(value, CultureInfo.InvariantCulture).ToMxValue(),
|
|
||||||
"float-array" => values.Select(item => float.Parse(item, CultureInfo.InvariantCulture)).ToArray().ToMxValue(),
|
|
||||||
"double" => double.Parse(value, CultureInfo.InvariantCulture).ToMxValue(),
|
|
||||||
"double-array" => values.Select(item => double.Parse(item, CultureInfo.InvariantCulture)).ToArray().ToMxValue(),
|
|
||||||
"string" => value.ToMxValue(),
|
|
||||||
"string-array" => values.ToMxValue(),
|
|
||||||
"time" or "timestamp" => DateTimeOffset.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToMxValue(),
|
|
||||||
"time-array" or "timestamp-array" => values
|
|
||||||
.Select(item => DateTimeOffset.Parse(item, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal))
|
|
||||||
.ToArray()
|
|
||||||
.ToMxValue(),
|
|
||||||
_ => throw new ArgumentException($"Unsupported MX value type '{type}'."),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxValue ParseTimestampValue(CliArguments arguments)
|
|
||||||
{
|
|
||||||
string timestamp = arguments.GetOptional("timestamp")
|
|
||||||
?? DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
return DateTimeOffset.Parse(
|
|
||||||
timestamp,
|
|
||||||
CultureInfo.InvariantCulture,
|
|
||||||
DateTimeStyles.AssumeUniversal)
|
|
||||||
.ToMxValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int WriteUnknownCommand(string command, TextWriter standardError)
|
|
||||||
{
|
|
||||||
standardError.WriteLine($"Unknown command '{command}'.");
|
|
||||||
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 bool IsKnownGatewayCommand(string command)
|
|
||||||
{
|
|
||||||
return command is "open-session"
|
|
||||||
or "close-session"
|
|
||||||
or "ping"
|
|
||||||
or "register"
|
|
||||||
or "add-item"
|
|
||||||
or "advise"
|
|
||||||
or "stream-events"
|
|
||||||
or "write"
|
|
||||||
or "write2"
|
|
||||||
or "smoke";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CreateCorrelationId()
|
|
||||||
{
|
|
||||||
return Guid.NewGuid().ToString("N");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteUsage(TextWriter writer)
|
|
||||||
{
|
|
||||||
writer.WriteLine("mxgw-dotnet version [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet ping --session-id <id> [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet open-session [--client-name <name>] [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet close-session --session-id <id> [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet register --session-id <id> --client-name <name> [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet add-item --session-id <id> --server-handle <n> --item <ref> [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet advise --session-id <id> --server-handle <n> --item-handle <n> [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet stream-events --session-id <id> [--max-events <n>] [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet write --session-id <id> --server-handle <n> --item-handle <n> --type <type> --value <value> [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet write2 --session-id <id> --server-handle <n> --item-handle <n> --type <type> --value <value> [--timestamp <iso>] [--json]");
|
|
||||||
writer.WriteLine("mxgw-dotnet smoke --item <ref> [--value <value> --type <type>] [--json]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
using MxGateway.Client.Cli;
|
|
||||||
|
|
||||||
return await MxGatewayClientCli.RunAsync(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,78 +0,0 @@
|
|||||||
using Google.Protobuf;
|
|
||||||
using MxGateway.Client;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
public sealed class MxCommandReplyExtensionsTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void EnsureSuccess_WithRegisterFixture_ReturnsReply()
|
|
||||||
{
|
|
||||||
MxCommandReply reply = ReadReplyFixture("register.ok.reply.json");
|
|
||||||
|
|
||||||
Assert.Same(reply, reply.EnsureProtocolSuccess());
|
|
||||||
Assert.Same(reply, reply.EnsureMxAccessSuccess());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void EnsureMxAccessSuccess_WithFailureFixture_PreservesHResultAndStatuses()
|
|
||||||
{
|
|
||||||
MxCommandReply reply = ReadReplyFixture("write.mxaccess-failure.reply.json");
|
|
||||||
|
|
||||||
reply.EnsureProtocolSuccess();
|
|
||||||
MxAccessException exception = Assert.Throws<MxAccessException>(
|
|
||||||
reply.EnsureMxAccessSuccess);
|
|
||||||
|
|
||||||
Assert.Equal(-2147220992, exception.HResultCode);
|
|
||||||
Assert.Equal(reply.Statuses.Count, exception.Statuses.Count);
|
|
||||||
Assert.Equal(reply, exception.Reply);
|
|
||||||
Assert.Contains("0x80040200", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void EnsureProtocolSuccess_WithSessionFailure_ThrowsSessionException()
|
|
||||||
{
|
|
||||||
MxCommandReply reply = new()
|
|
||||||
{
|
|
||||||
SessionId = "session-missing",
|
|
||||||
CorrelationId = "correlation",
|
|
||||||
ProtocolStatus = new ProtocolStatus
|
|
||||||
{
|
|
||||||
Code = ProtocolStatusCode.SessionNotFound,
|
|
||||||
Message = "Session was not found.",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
MxGatewaySessionException exception = Assert.Throws<MxGatewaySessionException>(
|
|
||||||
reply.EnsureProtocolSuccess);
|
|
||||||
|
|
||||||
Assert.Equal("session-missing", exception.SessionId);
|
|
||||||
Assert.Equal(ProtocolStatusCode.SessionNotFound, exception.ProtocolStatus?.Code);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxCommandReply ReadReplyFixture(string fileName)
|
|
||||||
{
|
|
||||||
DirectoryInfo directory = new(AppContext.BaseDirectory);
|
|
||||||
while (directory is not null)
|
|
||||||
{
|
|
||||||
string path = Path.Combine(
|
|
||||||
directory.FullName,
|
|
||||||
"clients",
|
|
||||||
"proto",
|
|
||||||
"fixtures",
|
|
||||||
"behavior",
|
|
||||||
"command-replies",
|
|
||||||
fileName);
|
|
||||||
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
return JsonParser.Default.Parse<MxCommandReply>(File.ReadAllText(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = directory.Parent!;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new FileNotFoundException(fileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,241 +0,0 @@
|
|||||||
using MxGateway.Client.Cli;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
|
|
||||||
{
|
|
||||||
using var output = new StringWriter();
|
|
||||||
using var error = new StringWriter();
|
|
||||||
|
|
||||||
int exitCode = await MxGatewayClientCli.RunAsync(["version", "--json"], output, error);
|
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
|
||||||
Assert.Contains("\"gatewayProtocolVersion\":1", output.ToString());
|
|
||||||
Assert.Equal(string.Empty, error.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
|
|
||||||
{
|
|
||||||
using var output = new StringWriter();
|
|
||||||
using var error = new StringWriter();
|
|
||||||
FakeCliClient fakeClient = new();
|
|
||||||
fakeClient.InvokeReplies.Enqueue(new MxCommandReply
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Kind = MxCommandKind.Write,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
});
|
|
||||||
|
|
||||||
int exitCode = await MxGatewayClientCli.RunAsync(
|
|
||||||
[
|
|
||||||
"write",
|
|
||||||
"--endpoint",
|
|
||||||
"http://localhost:5000",
|
|
||||||
"--api-key",
|
|
||||||
"test-api-key",
|
|
||||||
"--session-id",
|
|
||||||
"session-fixture",
|
|
||||||
"--server-handle",
|
|
||||||
"12",
|
|
||||||
"--item-handle",
|
|
||||||
"34",
|
|
||||||
"--type",
|
|
||||||
"int32",
|
|
||||||
"--value",
|
|
||||||
"123",
|
|
||||||
"--json",
|
|
||||||
],
|
|
||||||
output,
|
|
||||||
error,
|
|
||||||
_ => fakeClient);
|
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
|
||||||
MxCommandRequest request = Assert.Single(fakeClient.InvokeRequests);
|
|
||||||
Assert.Equal(MxCommandKind.Write, request.Command.Kind);
|
|
||||||
Assert.Equal(123, request.Command.Write.Value.Int32Value);
|
|
||||||
Assert.Contains("MX_COMMAND_KIND_WRITE", output.ToString());
|
|
||||||
Assert.Equal(string.Empty, error.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RunAsync_ErrorOutput_RedactsApiKey()
|
|
||||||
{
|
|
||||||
using var output = new StringWriter();
|
|
||||||
using var error = new StringWriter();
|
|
||||||
|
|
||||||
int exitCode = await MxGatewayClientCli.RunAsync(
|
|
||||||
[
|
|
||||||
"open-session",
|
|
||||||
"--endpoint",
|
|
||||||
"http://localhost:5000",
|
|
||||||
"--api-key",
|
|
||||||
"secret-api-key",
|
|
||||||
],
|
|
||||||
output,
|
|
||||||
error,
|
|
||||||
_ => throw new InvalidOperationException("boom secret-api-key"));
|
|
||||||
|
|
||||||
Assert.Equal(1, exitCode);
|
|
||||||
Assert.DoesNotContain("secret-api-key", error.ToString());
|
|
||||||
Assert.Contains("[redacted]", error.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
|
|
||||||
{
|
|
||||||
using var output = new StringWriter();
|
|
||||||
using var error = new StringWriter();
|
|
||||||
FakeCliClient fakeClient = new();
|
|
||||||
fakeClient.Events.Add(new MxEvent
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Family = MxEventFamily.OnDataChange,
|
|
||||||
WorkerSequence = 1,
|
|
||||||
});
|
|
||||||
fakeClient.Events.Add(new MxEvent
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Family = MxEventFamily.OnWriteComplete,
|
|
||||||
WorkerSequence = 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
int exitCode = await MxGatewayClientCli.RunAsync(
|
|
||||||
[
|
|
||||||
"stream-events",
|
|
||||||
"--endpoint",
|
|
||||||
"http://localhost:5000",
|
|
||||||
"--api-key",
|
|
||||||
"test-api-key",
|
|
||||||
"--session-id",
|
|
||||||
"session-fixture",
|
|
||||||
"--max-events",
|
|
||||||
"1",
|
|
||||||
],
|
|
||||||
output,
|
|
||||||
error,
|
|
||||||
_ => fakeClient);
|
|
||||||
|
|
||||||
Assert.Equal(0, exitCode);
|
|
||||||
Assert.Contains("workerSequence", output.ToString());
|
|
||||||
Assert.DoesNotContain("ON_WRITE_COMPLETE", output.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
|
|
||||||
{
|
|
||||||
using var output = new StringWriter();
|
|
||||||
using var error = new StringWriter();
|
|
||||||
FakeCliClient fakeClient = new()
|
|
||||||
{
|
|
||||||
InvokeFailure = new InvalidOperationException("register failed"),
|
|
||||||
};
|
|
||||||
|
|
||||||
int exitCode = await MxGatewayClientCli.RunAsync(
|
|
||||||
[
|
|
||||||
"smoke",
|
|
||||||
"--endpoint",
|
|
||||||
"http://localhost:5000",
|
|
||||||
"--api-key",
|
|
||||||
"test-api-key",
|
|
||||||
"--item",
|
|
||||||
"Area001.Pump001.Speed",
|
|
||||||
"--json",
|
|
||||||
],
|
|
||||||
output,
|
|
||||||
error,
|
|
||||||
_ => fakeClient);
|
|
||||||
|
|
||||||
Assert.Equal(1, exitCode);
|
|
||||||
CloseSessionRequest closeRequest = Assert.Single(fakeClient.CloseSessionRequests);
|
|
||||||
Assert.Equal("session-fixture", closeRequest.SessionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FakeCliClient : IMxGatewayCliClient
|
|
||||||
{
|
|
||||||
public Queue<MxCommandReply> InvokeReplies { get; } = new();
|
|
||||||
|
|
||||||
public List<MxCommandRequest> InvokeRequests { get; } = [];
|
|
||||||
|
|
||||||
public List<CloseSessionRequest> CloseSessionRequests { get; } = [];
|
|
||||||
|
|
||||||
public List<MxEvent> Events { get; } = [];
|
|
||||||
|
|
||||||
public Exception? InvokeFailure { get; init; }
|
|
||||||
|
|
||||||
public ValueTask DisposeAsync()
|
|
||||||
{
|
|
||||||
return ValueTask.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<OpenSessionReply> OpenSessionAsync(
|
|
||||||
OpenSessionRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return Task.FromResult(new OpenSessionReply
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
GatewayProtocolVersion = 1,
|
|
||||||
WorkerProtocolVersion = 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
CloseSessionRequests.Add(request);
|
|
||||||
return Task.FromResult(new CloseSessionReply
|
|
||||||
{
|
|
||||||
SessionId = request.SessionId,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
FinalState = SessionState.Closed,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
InvokeRequests.Add(request);
|
|
||||||
if (InvokeFailure is not null)
|
|
||||||
{
|
|
||||||
throw InvokeFailure;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.FromResult(InvokeReplies.Dequeue());
|
|
||||||
}
|
|
||||||
|
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
foreach (MxEvent gatewayEvent in Events)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
await Task.Yield();
|
|
||||||
yield return gatewayEvent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,217 +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 Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
|
|
||||||
{
|
|
||||||
FakeGatewayTransport transport = CreateTransport();
|
|
||||||
transport.AddInvokeReply(new MxCommandReply
|
|
||||||
{
|
|
||||||
SessionId = "session-fixture",
|
|
||||||
Kind = MxCommandKind.Write2,
|
|
||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
|
||||||
});
|
|
||||||
await using MxGatewayClient client = CreateClient(transport);
|
|
||||||
MxGatewaySession session = await client.OpenSessionAsync();
|
|
||||||
MxValue value = 123.ToMxValue();
|
|
||||||
MxValue timestampValue = DateTimeOffset.Parse("2026-01-01T00:00:00Z").ToMxValue();
|
|
||||||
|
|
||||||
MxCommandReply reply = await session.Write2RawAsync(12, 34, value, timestampValue, 56);
|
|
||||||
|
|
||||||
Assert.Equal(MxCommandKind.Write2, reply.Kind);
|
|
||||||
MxCommandRequest request = Assert.Single(transport.InvokeCalls).Request;
|
|
||||||
Assert.Equal(MxCommandKind.Write2, request.Command.Kind);
|
|
||||||
Assert.Equal(12, request.Command.Write2.ServerHandle);
|
|
||||||
Assert.Equal(34, request.Command.Write2.ItemHandle);
|
|
||||||
Assert.Same(value, request.Command.Write2.Value);
|
|
||||||
Assert.Same(timestampValue, request.Command.Write2.TimestampValue);
|
|
||||||
Assert.Equal(56, request.Command.Write2.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,57 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Google.Protobuf;
|
|
||||||
using MxGateway.Client;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
public sealed class MxStatusProxyExtensionsTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void FixtureStatuses_ProjectSuccessAndPreserveRawFields()
|
|
||||||
{
|
|
||||||
using JsonDocument document = JsonDocument.Parse(ReadFixture(
|
|
||||||
"statuses",
|
|
||||||
"status-conversion-cases.json"));
|
|
||||||
|
|
||||||
foreach (JsonElement testCase in document.RootElement.GetProperty("cases").EnumerateArray())
|
|
||||||
{
|
|
||||||
MxStatusProxy status = JsonParser.Default.Parse<MxStatusProxy>(
|
|
||||||
testCase.GetProperty("status").GetRawText());
|
|
||||||
int success = testCase.GetProperty("status").GetProperty("success").GetInt32();
|
|
||||||
|
|
||||||
Assert.Equal(success != 0 && status.Category is MxStatusCategory.Ok, status.IsSuccess());
|
|
||||||
Assert.Equal(
|
|
||||||
testCase.GetProperty("status").GetProperty("rawCategory").GetInt32(),
|
|
||||||
status.RawCategory);
|
|
||||||
Assert.Equal(
|
|
||||||
testCase.GetProperty("status").GetProperty("rawDetectedBy").GetInt32(),
|
|
||||||
status.RawDetectedBy);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadFixture(string category, string fileName)
|
|
||||||
{
|
|
||||||
DirectoryInfo directory = new(AppContext.BaseDirectory);
|
|
||||||
while (directory is not null)
|
|
||||||
{
|
|
||||||
string path = Path.Combine(
|
|
||||||
directory.FullName,
|
|
||||||
"clients",
|
|
||||||
"proto",
|
|
||||||
"fixtures",
|
|
||||||
"behavior",
|
|
||||||
category,
|
|
||||||
fileName);
|
|
||||||
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
return File.ReadAllText(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = directory.Parent!;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new FileNotFoundException(fileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Google.Protobuf;
|
|
||||||
using MxGateway.Client;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
|
||||||
|
|
||||||
public sealed class MxValueExtensionsTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void ToMxValue_WithScalarValues_CreatesTypedProtobufValues()
|
|
||||||
{
|
|
||||||
Assert.Equal(MxValue.KindOneofCase.BoolValue, true.ToMxValue().KindCase);
|
|
||||||
Assert.Equal(MxValue.KindOneofCase.Int32Value, 123.ToMxValue().KindCase);
|
|
||||||
Assert.Equal(MxValue.KindOneofCase.Int64Value, 123L.ToMxValue().KindCase);
|
|
||||||
Assert.Equal(MxValue.KindOneofCase.FloatValue, 1.25F.ToMxValue().KindCase);
|
|
||||||
Assert.Equal(MxValue.KindOneofCase.DoubleValue, 2.5D.ToMxValue().KindCase);
|
|
||||||
Assert.Equal(MxValue.KindOneofCase.StringValue, "alpha".ToMxValue().KindCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ToMxValue_WithArrays_CreatesTypedArrayProtobufValues()
|
|
||||||
{
|
|
||||||
MxValue value = new[] { "alpha", "beta" }.ToMxValue();
|
|
||||||
|
|
||||||
Assert.Equal(MxValue.KindOneofCase.ArrayValue, value.KindCase);
|
|
||||||
Assert.Equal(MxArray.ValuesOneofCase.StringValues, value.ArrayValue.ValuesCase);
|
|
||||||
Assert.Equal(["alpha", "beta"], value.ArrayValue.StringValues.Values);
|
|
||||||
Assert.Equal([2U], value.ArrayValue.Dimensions);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FixtureValues_ProjectExpectedKindsAndPreserveRawMetadata()
|
|
||||||
{
|
|
||||||
using JsonDocument document = JsonDocument.Parse(ReadFixture(
|
|
||||||
"values",
|
|
||||||
"value-conversion-cases.json"));
|
|
||||||
|
|
||||||
foreach (JsonElement testCase in document.RootElement.GetProperty("cases").EnumerateArray())
|
|
||||||
{
|
|
||||||
string expectedKind = testCase.GetProperty("expectedKind").GetString()!;
|
|
||||||
MxValue value = JsonParser.Default.Parse<MxValue>(
|
|
||||||
testCase.GetProperty("value").GetRawText());
|
|
||||||
|
|
||||||
Assert.Equal(expectedKind, value.GetProjectionKind());
|
|
||||||
|
|
||||||
if (testCase.GetProperty("id").GetString() is "raw-fallback.variant")
|
|
||||||
{
|
|
||||||
Assert.Equal(32767, value.RawDataType);
|
|
||||||
Assert.Equal([1, 2, 3, 4, 5], Assert.IsType<byte[]>(value.ToClrValue()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadFixture(string category, string fileName)
|
|
||||||
{
|
|
||||||
DirectoryInfo directory = new(AppContext.BaseDirectory);
|
|
||||||
while (directory is not null)
|
|
||||||
{
|
|
||||||
string path = Path.Combine(
|
|
||||||
directory.FullName,
|
|
||||||
"clients",
|
|
||||||
"proto",
|
|
||||||
"fixtures",
|
|
||||||
"behavior",
|
|
||||||
category,
|
|
||||||
fileName);
|
|
||||||
|
|
||||||
if (File.Exists(path))
|
|
||||||
{
|
|
||||||
return File.ReadAllText(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
directory = directory.Parent!;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new FileNotFoundException(fileName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,117 +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)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await RawClient.OpenSessionAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CloseSessionReply> CloseSessionAsync(
|
|
||||||
CloseSessionRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await RawClient.CloseSessionAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<MxCommandReply> InvokeAsync(
|
|
||||||
MxCommandRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return await RawClient.InvokeAsync(request, callOptions)
|
|
||||||
.ResponseAsync
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
IAsyncStreamReader<MxEvent> responseStream = call.ResponseStream;
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
MxEvent? gatewayEvent;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!await responseStream.MoveNext(effectiveCancellationToken).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
gatewayEvent = responseStream.Current;
|
|
||||||
}
|
|
||||||
catch (RpcException exception)
|
|
||||||
{
|
|
||||||
throw MapRpcException(exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return gatewayEvent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
|
||||||
StreamEventsRequest request,
|
|
||||||
CallOptions callOptions)
|
|
||||||
{
|
|
||||||
return StreamEventsAsync(request, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxGatewayException MapRpcException(RpcException exception)
|
|
||||||
{
|
|
||||||
return exception.StatusCode switch
|
|
||||||
{
|
|
||||||
StatusCode.Unauthenticated => new MxGatewayAuthenticationException(
|
|
||||||
exception.Status.Detail,
|
|
||||||
innerException: exception),
|
|
||||||
StatusCode.PermissionDenied => new MxGatewayAuthorizationException(
|
|
||||||
exception.Status.Detail,
|
|
||||||
innerException: exception),
|
|
||||||
_ => new MxGatewayException(exception.Status.Detail, exception),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,24 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
public sealed class MxAccessException : MxGatewayCommandException
|
|
||||||
{
|
|
||||||
public MxAccessException(
|
|
||||||
string message,
|
|
||||||
MxCommandReply reply,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
reply.SessionId,
|
|
||||||
reply.CorrelationId,
|
|
||||||
reply.ProtocolStatus,
|
|
||||||
reply.HasHresult ? reply.Hresult : null,
|
|
||||||
reply.Statuses.ToArray(),
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
Reply = reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxCommandReply Reply { get; }
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
public static class MxCommandReplyExtensions
|
|
||||||
{
|
|
||||||
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(reply);
|
|
||||||
|
|
||||||
ProtocolStatusCode code = reply.ProtocolStatus?.Code
|
|
||||||
?? ProtocolStatusCode.Unspecified;
|
|
||||||
|
|
||||||
if (code is ProtocolStatusCode.Ok or ProtocolStatusCode.MxaccessFailure)
|
|
||||||
{
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw CreateProtocolException(reply, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(reply);
|
|
||||||
|
|
||||||
bool mxAccessFailure = reply.ProtocolStatus?.Code is ProtocolStatusCode.MxaccessFailure;
|
|
||||||
bool hResultFailure = reply.HasHresult && reply.Hresult != 0;
|
|
||||||
bool statusFailure = reply.Statuses.Any(status => !status.IsSuccess());
|
|
||||||
|
|
||||||
if (!mxAccessFailure && !hResultFailure && !statusFailure)
|
|
||||||
{
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new MxAccessException(CreateMxAccessMessage(reply), reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxGatewayException CreateProtocolException(
|
|
||||||
MxCommandReply reply,
|
|
||||||
ProtocolStatusCode code)
|
|
||||||
{
|
|
||||||
string message = CreateProtocolMessage(reply);
|
|
||||||
int? hResult = reply.HasHresult ? reply.Hresult : null;
|
|
||||||
MxStatusProxy[] statuses = reply.Statuses.ToArray();
|
|
||||||
|
|
||||||
return code switch
|
|
||||||
{
|
|
||||||
ProtocolStatusCode.SessionNotFound or ProtocolStatusCode.SessionNotReady
|
|
||||||
=> new MxGatewaySessionException(
|
|
||||||
message,
|
|
||||||
reply.SessionId,
|
|
||||||
reply.CorrelationId,
|
|
||||||
reply.ProtocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses),
|
|
||||||
ProtocolStatusCode.WorkerUnavailable
|
|
||||||
=> new MxGatewayWorkerException(
|
|
||||||
message,
|
|
||||||
reply.SessionId,
|
|
||||||
reply.CorrelationId,
|
|
||||||
reply.ProtocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses),
|
|
||||||
_
|
|
||||||
=> new MxGatewayCommandException(
|
|
||||||
message,
|
|
||||||
reply.SessionId,
|
|
||||||
reply.CorrelationId,
|
|
||||||
reply.ProtocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CreateProtocolMessage(MxCommandReply reply)
|
|
||||||
{
|
|
||||||
string statusMessage = string.IsNullOrWhiteSpace(reply.ProtocolStatus?.Message)
|
|
||||||
? "Gateway protocol failure."
|
|
||||||
: reply.ProtocolStatus.Message;
|
|
||||||
|
|
||||||
return $"{statusMessage} code={reply.ProtocolStatus?.Code}; session={reply.SessionId}; correlation={reply.CorrelationId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string CreateMxAccessMessage(MxCommandReply reply)
|
|
||||||
{
|
|
||||||
string statusSummary = reply.Statuses.Count is 0
|
|
||||||
? "no MXSTATUS_PROXY entries"
|
|
||||||
: string.Join("; ", reply.Statuses.Select(status => status.ToDiagnosticSummary()));
|
|
||||||
|
|
||||||
string hResult = reply.HasHresult
|
|
||||||
? $"0x{reply.Hresult:X8}"
|
|
||||||
: "none";
|
|
||||||
|
|
||||||
return $"MXAccess command failed. kind={reply.Kind}; hresult={hResult}; statuses={statusSummary}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,25 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
public sealed class MxGatewayAuthenticationException : MxGatewayException
|
|
||||||
{
|
|
||||||
public MxGatewayAuthenticationException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
public sealed class MxGatewayAuthorizationException : MxGatewayException
|
|
||||||
{
|
|
||||||
public MxGatewayAuthorizationException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,25 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
public class MxGatewayCommandException : MxGatewayException
|
|
||||||
{
|
|
||||||
public MxGatewayCommandException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
public class MxGatewayException : Exception
|
|
||||||
{
|
|
||||||
public MxGatewayException(string message)
|
|
||||||
: base(message)
|
|
||||||
{
|
|
||||||
Statuses = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxGatewayException(string message, Exception? innerException)
|
|
||||||
: base(message, innerException)
|
|
||||||
{
|
|
||||||
Statuses = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxGatewayException(
|
|
||||||
string message,
|
|
||||||
string? sessionId,
|
|
||||||
string? correlationId,
|
|
||||||
ProtocolStatus? protocolStatus,
|
|
||||||
int? hResult,
|
|
||||||
IReadOnlyList<MxStatusProxy> statuses,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(message, innerException)
|
|
||||||
{
|
|
||||||
SessionId = sessionId;
|
|
||||||
CorrelationId = correlationId;
|
|
||||||
ProtocolStatus = protocolStatus;
|
|
||||||
HResultCode = hResult;
|
|
||||||
Statuses = statuses;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? SessionId { get; }
|
|
||||||
|
|
||||||
public string? CorrelationId { get; }
|
|
||||||
|
|
||||||
public ProtocolStatus? ProtocolStatus { get; }
|
|
||||||
|
|
||||||
public int? HResultCode { get; }
|
|
||||||
|
|
||||||
public IReadOnlyList<MxStatusProxy> Statuses { get; }
|
|
||||||
}
|
|
||||||
@@ -1,300 +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);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
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);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
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);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await AdviseRawAsync(serverHandle, itemHandle, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await WriteRawAsync(serverHandle, itemHandle, value, userId, cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 async Task Write2Async(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
MxValue value,
|
|
||||||
MxValue timestampValue,
|
|
||||||
int userId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
MxCommandReply reply = await Write2RawAsync(
|
|
||||||
serverHandle,
|
|
||||||
itemHandle,
|
|
||||||
value,
|
|
||||||
timestampValue,
|
|
||||||
userId,
|
|
||||||
cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
|
||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<MxCommandReply> Write2RawAsync(
|
|
||||||
int serverHandle,
|
|
||||||
int itemHandle,
|
|
||||||
MxValue value,
|
|
||||||
MxValue timestampValue,
|
|
||||||
int userId,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
|
||||||
ArgumentNullException.ThrowIfNull(timestampValue);
|
|
||||||
|
|
||||||
return InvokeCommandAsync(
|
|
||||||
new MxCommand
|
|
||||||
{
|
|
||||||
Kind = MxCommandKind.Write2,
|
|
||||||
Write2 = new Write2Command
|
|
||||||
{
|
|
||||||
ServerHandle = serverHandle,
|
|
||||||
ItemHandle = itemHandle,
|
|
||||||
Value = value,
|
|
||||||
TimestampValue = timestampValue,
|
|
||||||
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,25 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
public sealed class MxGatewaySessionException : MxGatewayException
|
|
||||||
{
|
|
||||||
public MxGatewaySessionException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
public sealed class MxGatewayWorkerException : MxGatewayException
|
|
||||||
{
|
|
||||||
public MxGatewayWorkerException(
|
|
||||||
string message,
|
|
||||||
string? sessionId = null,
|
|
||||||
string? correlationId = null,
|
|
||||||
ProtocolStatus? protocolStatus = null,
|
|
||||||
int? hResult = null,
|
|
||||||
IReadOnlyList<MxStatusProxy>? statuses = null,
|
|
||||||
Exception? innerException = null)
|
|
||||||
: base(
|
|
||||||
message,
|
|
||||||
sessionId,
|
|
||||||
correlationId,
|
|
||||||
protocolStatus,
|
|
||||||
hResult,
|
|
||||||
statuses ?? [],
|
|
||||||
innerException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
public static class MxStatusProxyExtensions
|
|
||||||
{
|
|
||||||
public static bool IsSuccess(this MxStatusProxy status)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(status);
|
|
||||||
|
|
||||||
return status.Success != 0
|
|
||||||
&& status.Category is MxStatusCategory.Ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ToDiagnosticSummary(this MxStatusProxy status)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(status);
|
|
||||||
|
|
||||||
string diagnosticText = string.IsNullOrWhiteSpace(status.DiagnosticText)
|
|
||||||
? "no diagnostic text"
|
|
||||||
: status.DiagnosticText;
|
|
||||||
|
|
||||||
return $"{status.Category} by {status.DetectedBy}; detail={status.Detail}; {diagnosticText}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
using Google.Protobuf;
|
|
||||||
using Google.Protobuf.WellKnownTypes;
|
|
||||||
using MxGateway.Contracts.Proto;
|
|
||||||
|
|
||||||
namespace MxGateway.Client;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates and projects gateway MXAccess values without hiding the raw
|
|
||||||
/// protobuf value carried by command replies and events.
|
|
||||||
/// </summary>
|
|
||||||
public static class MxValueExtensions
|
|
||||||
{
|
|
||||||
public static MxValue ToMxValue(this bool value)
|
|
||||||
{
|
|
||||||
return new MxValue
|
|
||||||
{
|
|
||||||
DataType = MxDataType.Boolean,
|
|
||||||
VariantType = "VT_BOOL",
|
|
||||||
BoolValue = value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this int value)
|
|
||||||
{
|
|
||||||
return new MxValue
|
|
||||||
{
|
|
||||||
DataType = MxDataType.Integer,
|
|
||||||
VariantType = "VT_I4",
|
|
||||||
Int32Value = value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this long value)
|
|
||||||
{
|
|
||||||
return new MxValue
|
|
||||||
{
|
|
||||||
DataType = MxDataType.Integer,
|
|
||||||
VariantType = "VT_I8",
|
|
||||||
Int64Value = value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this float value)
|
|
||||||
{
|
|
||||||
return new MxValue
|
|
||||||
{
|
|
||||||
DataType = MxDataType.Float,
|
|
||||||
VariantType = "VT_R4",
|
|
||||||
FloatValue = value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this double value)
|
|
||||||
{
|
|
||||||
return new MxValue
|
|
||||||
{
|
|
||||||
DataType = MxDataType.Double,
|
|
||||||
VariantType = "VT_R8",
|
|
||||||
DoubleValue = value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this string value)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
|
||||||
|
|
||||||
return new MxValue
|
|
||||||
{
|
|
||||||
DataType = MxDataType.String,
|
|
||||||
VariantType = "VT_BSTR",
|
|
||||||
StringValue = value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this DateTimeOffset value)
|
|
||||||
{
|
|
||||||
return new MxValue
|
|
||||||
{
|
|
||||||
DataType = MxDataType.Time,
|
|
||||||
VariantType = "VT_DATE",
|
|
||||||
TimestampValue = Timestamp.FromDateTimeOffset(value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this DateTime value)
|
|
||||||
{
|
|
||||||
return new DateTimeOffset(
|
|
||||||
value.Kind == DateTimeKind.Unspecified
|
|
||||||
? DateTime.SpecifyKind(value, DateTimeKind.Utc)
|
|
||||||
: value.ToUniversalTime())
|
|
||||||
.ToMxValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<bool> values)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
|
||||||
|
|
||||||
var array = new BoolArray();
|
|
||||||
array.Values.Add(values);
|
|
||||||
return CreateArrayValue(MxDataType.Boolean, "VT_ARRAY|VT_BOOL", values.Count, new MxArray
|
|
||||||
{
|
|
||||||
ElementDataType = MxDataType.Boolean,
|
|
||||||
VariantType = "VT_ARRAY|VT_BOOL",
|
|
||||||
BoolValues = array,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<int> values)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
|
||||||
|
|
||||||
var array = new Int32Array();
|
|
||||||
array.Values.Add(values);
|
|
||||||
return CreateArrayValue(MxDataType.Integer, "VT_ARRAY|VT_I4", values.Count, new MxArray
|
|
||||||
{
|
|
||||||
ElementDataType = MxDataType.Integer,
|
|
||||||
VariantType = "VT_ARRAY|VT_I4",
|
|
||||||
Int32Values = array,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<long> values)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
|
||||||
|
|
||||||
var array = new Int64Array();
|
|
||||||
array.Values.Add(values);
|
|
||||||
return CreateArrayValue(MxDataType.Integer, "VT_ARRAY|VT_I8", values.Count, new MxArray
|
|
||||||
{
|
|
||||||
ElementDataType = MxDataType.Integer,
|
|
||||||
VariantType = "VT_ARRAY|VT_I8",
|
|
||||||
Int64Values = array,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<float> values)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
|
||||||
|
|
||||||
var array = new FloatArray();
|
|
||||||
array.Values.Add(values);
|
|
||||||
return CreateArrayValue(MxDataType.Float, "VT_ARRAY|VT_R4", values.Count, new MxArray
|
|
||||||
{
|
|
||||||
ElementDataType = MxDataType.Float,
|
|
||||||
VariantType = "VT_ARRAY|VT_R4",
|
|
||||||
FloatValues = array,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<double> values)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
|
||||||
|
|
||||||
var array = new DoubleArray();
|
|
||||||
array.Values.Add(values);
|
|
||||||
return CreateArrayValue(MxDataType.Double, "VT_ARRAY|VT_R8", values.Count, new MxArray
|
|
||||||
{
|
|
||||||
ElementDataType = MxDataType.Double,
|
|
||||||
VariantType = "VT_ARRAY|VT_R8",
|
|
||||||
DoubleValues = array,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<string> values)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
|
||||||
|
|
||||||
var array = new StringArray();
|
|
||||||
array.Values.Add(values);
|
|
||||||
return CreateArrayValue(MxDataType.String, "VT_ARRAY|VT_BSTR", values.Count, new MxArray
|
|
||||||
{
|
|
||||||
ElementDataType = MxDataType.String,
|
|
||||||
VariantType = "VT_ARRAY|VT_BSTR",
|
|
||||||
StringValues = array,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
|
||||||
|
|
||||||
var array = new TimestampArray();
|
|
||||||
array.Values.Add(values.Select(Timestamp.FromDateTimeOffset));
|
|
||||||
return CreateArrayValue(MxDataType.Time, "VT_ARRAY|VT_DATE", values.Count, new MxArray
|
|
||||||
{
|
|
||||||
ElementDataType = MxDataType.Time,
|
|
||||||
VariantType = "VT_ARRAY|VT_DATE",
|
|
||||||
TimestampValues = array,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetProjectionKind(this MxValue value)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
|
||||||
|
|
||||||
return value.KindCase switch
|
|
||||||
{
|
|
||||||
MxValue.KindOneofCase.BoolValue => "boolValue",
|
|
||||||
MxValue.KindOneofCase.Int32Value => "int32Value",
|
|
||||||
MxValue.KindOneofCase.Int64Value => "int64Value",
|
|
||||||
MxValue.KindOneofCase.FloatValue => "floatValue",
|
|
||||||
MxValue.KindOneofCase.DoubleValue => "doubleValue",
|
|
||||||
MxValue.KindOneofCase.StringValue => "stringValue",
|
|
||||||
MxValue.KindOneofCase.TimestampValue => "timestampValue",
|
|
||||||
MxValue.KindOneofCase.ArrayValue => "arrayValue",
|
|
||||||
MxValue.KindOneofCase.RawValue => "rawValue",
|
|
||||||
_ => value.IsNull ? "nullValue" : "unspecified",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static object? ToClrValue(this MxValue value)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
|
||||||
|
|
||||||
return value.KindCase switch
|
|
||||||
{
|
|
||||||
MxValue.KindOneofCase.BoolValue => value.BoolValue,
|
|
||||||
MxValue.KindOneofCase.Int32Value => value.Int32Value,
|
|
||||||
MxValue.KindOneofCase.Int64Value => value.Int64Value,
|
|
||||||
MxValue.KindOneofCase.FloatValue => value.FloatValue,
|
|
||||||
MxValue.KindOneofCase.DoubleValue => value.DoubleValue,
|
|
||||||
MxValue.KindOneofCase.StringValue => value.StringValue,
|
|
||||||
MxValue.KindOneofCase.TimestampValue => value.TimestampValue.ToDateTimeOffset(),
|
|
||||||
MxValue.KindOneofCase.ArrayValue => value.ArrayValue.ToClrArrayValue(),
|
|
||||||
MxValue.KindOneofCase.RawValue => value.RawValue.ToByteArray(),
|
|
||||||
_ => value.IsNull ? null : value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static object? ToClrArrayValue(this MxArray array)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(array);
|
|
||||||
|
|
||||||
return array.ValuesCase switch
|
|
||||||
{
|
|
||||||
MxArray.ValuesOneofCase.BoolValues => array.BoolValues.Values.ToArray(),
|
|
||||||
MxArray.ValuesOneofCase.Int32Values => array.Int32Values.Values.ToArray(),
|
|
||||||
MxArray.ValuesOneofCase.Int64Values => array.Int64Values.Values.ToArray(),
|
|
||||||
MxArray.ValuesOneofCase.FloatValues => array.FloatValues.Values.ToArray(),
|
|
||||||
MxArray.ValuesOneofCase.DoubleValues => array.DoubleValues.Values.ToArray(),
|
|
||||||
MxArray.ValuesOneofCase.StringValues => array.StringValues.Values.ToArray(),
|
|
||||||
MxArray.ValuesOneofCase.TimestampValues => array.TimestampValues.Values
|
|
||||||
.Select(timestamp => timestamp.ToDateTimeOffset())
|
|
||||||
.ToArray(),
|
|
||||||
MxArray.ValuesOneofCase.RawValues => array.RawValues.Values
|
|
||||||
.Select(value => value.ToByteArray())
|
|
||||||
.ToArray(),
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue ToRawMxValue(
|
|
||||||
byte[] value,
|
|
||||||
string variantType,
|
|
||||||
string rawDiagnostic,
|
|
||||||
int rawDataType = 0)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
|
||||||
|
|
||||||
return new MxValue
|
|
||||||
{
|
|
||||||
DataType = MxDataType.Unknown,
|
|
||||||
VariantType = variantType,
|
|
||||||
RawDiagnostic = rawDiagnostic,
|
|
||||||
RawDataType = rawDataType,
|
|
||||||
RawValue = ByteString.CopyFrom(value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxValue CreateArrayValue(
|
|
||||||
MxDataType dataType,
|
|
||||||
string variantType,
|
|
||||||
int length,
|
|
||||||
MxArray array)
|
|
||||||
{
|
|
||||||
array.Dimensions.Add((uint)length);
|
|
||||||
return new MxValue
|
|
||||||
{
|
|
||||||
DataType = dataType,
|
|
||||||
VariantType = variantType,
|
|
||||||
ArrayValue = array,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("MxGateway.Client.Tests")]
|
|
||||||
@@ -1,111 +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.
|
|
||||||
|
|
||||||
## Values, Status, And Errors
|
|
||||||
|
|
||||||
The client provides extension helpers for generated protobuf values. Use
|
|
||||||
`ToMxValue()` on .NET scalar values and typed arrays to create `MxValue`
|
|
||||||
instances for `Write` and `Write2`. Use `ToClrValue()` and
|
|
||||||
`GetProjectionKind()` when test or diagnostic code needs to inspect generated
|
|
||||||
`MxValue` replies while preserving `rawDiagnostic`, raw data type fields, and
|
|
||||||
raw byte payloads.
|
|
||||||
|
|
||||||
`MxStatusProxy.IsSuccess()` and `ToDiagnosticSummary()` expose MXAccess status
|
|
||||||
arrays without collapsing them into a single gateway success flag. Command
|
|
||||||
reply helpers follow the same split:
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
reply.EnsureProtocolSuccess();
|
|
||||||
reply.EnsureMxAccessSuccess();
|
|
||||||
```
|
|
||||||
|
|
||||||
`EnsureProtocolSuccess()` raises gateway, session, worker, or command
|
|
||||||
exceptions for gateway-level failures. It leaves
|
|
||||||
`PROTOCOL_STATUS_CODE_MXACCESS_FAILURE` to `EnsureMxAccessSuccess()` so callers
|
|
||||||
can keep the full `MxCommandReply`, HRESULT, and status array when MXAccess
|
|
||||||
itself rejects a command. `MxAccessException.Reply` contains the raw generated
|
|
||||||
reply.
|
|
||||||
|
|
||||||
## CLI Usage
|
|
||||||
|
|
||||||
The test CLI supports deterministic JSON output for automation:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- version --json
|
|
||||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- open-session --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --json
|
|
||||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- register --session-id <id> --client-name mxgw-dotnet-cli --json
|
|
||||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- add-item --session-id <id> --server-handle 1 --item Area001.Pump001.Speed --json
|
|
||||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- advise --session-id <id> --server-handle 1 --item-handle 1 --json
|
|
||||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- write --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json
|
|
||||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- write2 --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --timestamp 2026-01-01T00:00:00Z --json
|
|
||||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- stream-events --session-id <id> --max-events 1 --json
|
|
||||||
dotnet run --project clients/dotnet/MxGateway.Client.Cli -- smoke --endpoint http://localhost:5000 --api-key-env MXGATEWAY_API_KEY --item Area001.Pump001.Speed --json
|
|
||||||
```
|
|
||||||
|
|
||||||
`smoke` opens a session, registers a client, adds one item, advises it,
|
|
||||||
optionally writes a value when `--type` and `--value` are supplied, reads a
|
|
||||||
bounded event stream, and closes the session in a `finally` block. CLI error
|
|
||||||
output redacts API keys supplied through `--api-key`.
|
|
||||||
@@ -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,96 +0,0 @@
|
|||||||
# Java Client
|
|
||||||
|
|
||||||
The Java client workspace contains the MXAccess Gateway client library,
|
|
||||||
generated protobuf/gRPC bindings, a Picocli test CLI project, and JUnit tests.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```text
|
|
||||||
clients/java/
|
|
||||||
settings.gradle
|
|
||||||
build.gradle
|
|
||||||
src/main/generated/
|
|
||||||
mxgateway-client/
|
|
||||||
mxgateway-cli/
|
|
||||||
```
|
|
||||||
|
|
||||||
`mxgateway-client` generates Java protobuf and gRPC sources from
|
|
||||||
`../../src/MxGateway.Contracts/Protos`. The Gradle protobuf plugin writes those
|
|
||||||
generated sources under `src/main/generated`, which matches the client proto
|
|
||||||
manifest in `../proto/proto-inputs.json`. Do not edit generated files by hand.
|
|
||||||
|
|
||||||
`mxgateway-client` exposes `MxGatewayClientOptions`, `MxGatewayClient`,
|
|
||||||
`MxGatewaySession`, value/status helpers, typed gateway exceptions, raw
|
|
||||||
generated stubs, and generated protobuf messages for parity tests.
|
|
||||||
|
|
||||||
`mxgateway-cli` depends on `mxgateway-client` and provides the `mxgw-java`
|
|
||||||
application entry point. The CLI supports version, session, command, event
|
|
||||||
streaming, write, and smoke-test commands with deterministic JSON output.
|
|
||||||
|
|
||||||
## Client Usage
|
|
||||||
|
|
||||||
Create a client with explicit transport and auth options:
|
|
||||||
|
|
||||||
```java
|
|
||||||
MxGatewayClientOptions options = MxGatewayClientOptions.builder()
|
|
||||||
.endpoint("localhost:5000")
|
|
||||||
.apiKey(System.getenv("MXGATEWAY_API_KEY"))
|
|
||||||
.plaintext(true)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
try (MxGatewayClient client = MxGatewayClient.connect(options);
|
|
||||||
MxGatewaySession session = client.openSession("java-client")) {
|
|
||||||
int serverHandle = session.register("java-client");
|
|
||||||
int itemHandle = session.addItem(serverHandle, "TestObject.TestInt");
|
|
||||||
session.advise(serverHandle, itemHandle);
|
|
||||||
session.write(serverHandle, itemHandle, MxValues.int32Value(123), 0);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `rawBlockingStub`, `rawFutureStub`, `rawAsyncStub`, `openSessionRaw`,
|
|
||||||
`closeSessionRaw`, `invoke`, and raw session helper methods when tests need the
|
|
||||||
underlying protobuf messages. `MxGatewayCommandException` and
|
|
||||||
`MxAccessException` preserve the raw `MxCommandReply` when the gateway returns a
|
|
||||||
data-bearing MXAccess failure.
|
|
||||||
|
|
||||||
`MxEventStream` implements `Iterator<MxEvent>` and `AutoCloseable`. Closing it
|
|
||||||
cancels the underlying gRPC stream. Canceling or timing out a Java client call
|
|
||||||
only stops the client from waiting; it does not abort an in-flight MXAccess COM
|
|
||||||
call on the worker STA.
|
|
||||||
|
|
||||||
## CLI Usage
|
|
||||||
|
|
||||||
Run the CLI through Gradle:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
gradle :mxgateway-cli:run --args="version --json"
|
|
||||||
gradle :mxgateway-cli:run --args="open-session --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --client-session-name java-cli --json"
|
|
||||||
gradle :mxgateway-cli:run --args="register --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --client-name java-cli --json"
|
|
||||||
gradle :mxgateway-cli:run --args="add-item --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item TestObject.TestInt --json"
|
|
||||||
gradle :mxgateway-cli:run --args="advise --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --json"
|
|
||||||
gradle :mxgateway-cli:run --args="write --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --server-handle 1 --item-handle 1 --type int32 --value 123 --json"
|
|
||||||
gradle :mxgateway-cli:run --args="stream-events --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --session-id <id> --limit 1 --json"
|
|
||||||
gradle :mxgateway-cli:run --args="smoke --endpoint localhost:5000 --api-key-env MXGATEWAY_API_KEY --plaintext --item TestObject.TestInt --json"
|
|
||||||
```
|
|
||||||
|
|
||||||
The CLI accepts `--api-key`, `--api-key-env`, `--plaintext`, `--ca-file`,
|
|
||||||
`--server-name-override`, `--timeout`, and `--json` on gateway commands. JSON
|
|
||||||
output redacts API keys.
|
|
||||||
|
|
||||||
## Build And Test
|
|
||||||
|
|
||||||
Run the Java checks from `clients/java`:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
gradle test
|
|
||||||
```
|
|
||||||
|
|
||||||
The build uses the Java 21 Gradle toolchain, compiles generated protobuf/gRPC
|
|
||||||
code, and runs JUnit 5 tests for the client wrapper, shared behavior fixtures,
|
|
||||||
in-process gRPC behavior, stream cancellation, and CLI parser/output behavior.
|
|
||||||
|
|
||||||
## Related Documentation
|
|
||||||
|
|
||||||
- [Client Proto Generation](../../docs/client-proto-generation.md)
|
|
||||||
- [Java Client Detailed Design](../../docs/clients-java-design.md)
|
|
||||||
- [Java Style Guide](../../docs/style-guides/JavaStyleGuide.md)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'base'
|
|
||||||
}
|
|
||||||
|
|
||||||
ext {
|
|
||||||
guavaVersion = '33.5.0-jre'
|
|
||||||
gsonVersion = '2.13.2'
|
|
||||||
grpcVersion = '1.76.0'
|
|
||||||
junitVersion = '5.14.1'
|
|
||||||
picocliVersion = '4.7.7'
|
|
||||||
protobufVersion = '4.33.1'
|
|
||||||
}
|
|
||||||
|
|
||||||
subprojects {
|
|
||||||
group = 'com.dohertylan.mxgateway'
|
|
||||||
version = '0.1.0'
|
|
||||||
|
|
||||||
pluginManager.withPlugin('java') {
|
|
||||||
java {
|
|
||||||
toolchain {
|
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(JavaCompile).configureEach {
|
|
||||||
options.encoding = 'UTF-8'
|
|
||||||
options.release = 21
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType(Test).configureEach {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
testImplementation platform("org.junit:junit-bom:${junitVersion}")
|
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'application'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation project(':mxgateway-client')
|
|
||||||
implementation "com.google.protobuf:protobuf-java-util:${protobufVersion}"
|
|
||||||
implementation "info.picocli:picocli:${picocliVersion}"
|
|
||||||
}
|
|
||||||
|
|
||||||
application {
|
|
||||||
mainClass = 'com.dohertylan.mxgateway.cli.MxGatewayCli'
|
|
||||||
}
|
|
||||||
-644
@@ -1,644 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.cli;
|
|
||||||
|
|
||||||
import com.dohertylan.mxgateway.client.MxEventStream;
|
|
||||||
import com.dohertylan.mxgateway.client.MxGatewayClient;
|
|
||||||
import com.dohertylan.mxgateway.client.MxGatewayClientOptions;
|
|
||||||
import com.dohertylan.mxgateway.client.MxGatewayClientVersion;
|
|
||||||
import com.dohertylan.mxgateway.client.MxGatewaySecrets;
|
|
||||||
import com.dohertylan.mxgateway.client.MxGatewaySession;
|
|
||||||
import com.dohertylan.mxgateway.client.MxValues;
|
|
||||||
import com.google.protobuf.Message;
|
|
||||||
import com.google.protobuf.util.JsonFormat;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.Callable;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
|
||||||
import picocli.CommandLine;
|
|
||||||
import picocli.CommandLine.Command;
|
|
||||||
import picocli.CommandLine.Mixin;
|
|
||||||
import picocli.CommandLine.Model.CommandSpec;
|
|
||||||
import picocli.CommandLine.Option;
|
|
||||||
import picocli.CommandLine.Spec;
|
|
||||||
|
|
||||||
@Command(
|
|
||||||
name = "mxgw-java",
|
|
||||||
mixinStandardHelpOptions = true,
|
|
||||||
description = "MXAccess Gateway Java test CLI.")
|
|
||||||
public final class MxGatewayCli implements Callable<Integer> {
|
|
||||||
private final MxGatewayCliClientFactory clientFactory;
|
|
||||||
|
|
||||||
@Spec
|
|
||||||
private CommandSpec spec;
|
|
||||||
|
|
||||||
public MxGatewayCli() {
|
|
||||||
this(new GrpcMxGatewayCliClientFactory());
|
|
||||||
}
|
|
||||||
|
|
||||||
MxGatewayCli(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
this.clientFactory = clientFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) {
|
|
||||||
int exitCode = commandLine(new GrpcMxGatewayCliClientFactory()).execute(args);
|
|
||||||
System.exit(exitCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int execute(PrintWriter out, PrintWriter err, String... args) {
|
|
||||||
return execute(new GrpcMxGatewayCliClientFactory(), out, err, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int execute(MxGatewayCliClientFactory clientFactory, PrintWriter out, PrintWriter err, String... args) {
|
|
||||||
CommandLine commandLine = commandLine(clientFactory);
|
|
||||||
commandLine.setOut(out);
|
|
||||||
commandLine.setErr(err);
|
|
||||||
return commandLine.execute(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
spec.commandLine().usage(spec.commandLine().getOut());
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CommandLine commandLine(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
CommandLine commandLine = new CommandLine(new MxGatewayCli(clientFactory));
|
|
||||||
commandLine.addSubcommand("version", new VersionCommand());
|
|
||||||
commandLine.addSubcommand("open-session", new OpenSessionCommand(clientFactory));
|
|
||||||
commandLine.addSubcommand("close-session", new CloseSessionCommand(clientFactory));
|
|
||||||
commandLine.addSubcommand("register", new RegisterCommand(clientFactory));
|
|
||||||
commandLine.addSubcommand("add-item", new AddItemCommand(clientFactory));
|
|
||||||
commandLine.addSubcommand("advise", new AdviseCommand(clientFactory));
|
|
||||||
commandLine.addSubcommand("write", new WriteCommand(clientFactory));
|
|
||||||
commandLine.addSubcommand("stream-events", new StreamEventsCommand(clientFactory));
|
|
||||||
commandLine.addSubcommand("smoke", new SmokeCommand(clientFactory));
|
|
||||||
return commandLine;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command(name = "version", description = "Prints the Java client version.")
|
|
||||||
public static final class VersionCommand implements Callable<Integer> {
|
|
||||||
@Spec
|
|
||||||
private CommandSpec spec;
|
|
||||||
|
|
||||||
@Option(names = "--json", description = "Write JSON output.")
|
|
||||||
private boolean json;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
Map<String, Object> values = new LinkedHashMap<>();
|
|
||||||
values.put("clientVersion", MxGatewayClientVersion.clientVersion());
|
|
||||||
values.put("gatewayProtocolVersion", MxGatewayClientVersion.gatewayProtocolVersion());
|
|
||||||
values.put("workerProtocolVersion", MxGatewayClientVersion.workerProtocolVersion());
|
|
||||||
if (json) {
|
|
||||||
spec.commandLine().getOut().println(jsonObject(values));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
spec.commandLine()
|
|
||||||
.getOut()
|
|
||||||
.printf(
|
|
||||||
"mxgateway-java %s gatewayProtocolVersion=%d workerProtocolVersion=%d%n",
|
|
||||||
MxGatewayClientVersion.clientVersion(),
|
|
||||||
MxGatewayClientVersion.gatewayProtocolVersion(),
|
|
||||||
MxGatewayClientVersion.workerProtocolVersion());
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract static class GatewayCommand implements Callable<Integer> {
|
|
||||||
final MxGatewayCliClientFactory clientFactory;
|
|
||||||
|
|
||||||
@Mixin
|
|
||||||
CommonOptions common = new CommonOptions();
|
|
||||||
|
|
||||||
@Option(names = "--json", description = "Write JSON output.")
|
|
||||||
boolean json;
|
|
||||||
|
|
||||||
GatewayCommand(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
this.clientFactory = clientFactory;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command(name = "open-session", description = "Opens a gateway session.")
|
|
||||||
static final class OpenSessionCommand extends GatewayCommand {
|
|
||||||
@Option(names = "--client-session-name", description = "Client session name.")
|
|
||||||
String clientSessionName = "";
|
|
||||||
|
|
||||||
@Option(names = "--backend", description = "Requested gateway backend.")
|
|
||||||
String backend = "";
|
|
||||||
|
|
||||||
OpenSessionCommand(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
super(clientFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
|
||||||
var reply = client.openSession(OpenSessionRequest.newBuilder()
|
|
||||||
.setClientSessionName(clientSessionName)
|
|
||||||
.setRequestedBackend(backend)
|
|
||||||
.build());
|
|
||||||
writeOutput("open-session", common, json, reply, () -> reply.getSessionId());
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command(name = "close-session", description = "Closes a gateway session.")
|
|
||||||
static final class CloseSessionCommand extends GatewayCommand {
|
|
||||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
|
||||||
String sessionId;
|
|
||||||
|
|
||||||
CloseSessionCommand(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
super(clientFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
|
||||||
var reply = client.closeSession(CloseSessionRequest.newBuilder()
|
|
||||||
.setSessionId(sessionId)
|
|
||||||
.build());
|
|
||||||
writeOutput("close-session", common, json, reply, () -> reply.getFinalState().name());
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command(name = "register", description = "Invokes MXAccess Register.")
|
|
||||||
static final class RegisterCommand extends GatewayCommand {
|
|
||||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
|
||||||
String sessionId;
|
|
||||||
|
|
||||||
@Option(names = "--client-name", required = true, description = "MXAccess client name.")
|
|
||||||
String clientName;
|
|
||||||
|
|
||||||
RegisterCommand(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
super(clientFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
|
||||||
MxCommandReply reply = client.session(sessionId).registerRaw(clientName);
|
|
||||||
writeOutput("register", common, json, reply, () -> reply.getKind().name());
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command(name = "add-item", description = "Invokes MXAccess AddItem.")
|
|
||||||
static final class AddItemCommand extends GatewayCommand {
|
|
||||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
|
||||||
String sessionId;
|
|
||||||
|
|
||||||
@Option(names = "--server-handle", required = true, description = "MXAccess server handle.")
|
|
||||||
int serverHandle;
|
|
||||||
|
|
||||||
@Option(names = "--item", required = true, description = "Item definition.")
|
|
||||||
String item;
|
|
||||||
|
|
||||||
AddItemCommand(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
super(clientFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
|
||||||
MxCommandReply reply = client.session(sessionId).addItemRaw(serverHandle, item);
|
|
||||||
writeOutput("add-item", common, json, reply, () -> reply.getKind().name());
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command(name = "advise", description = "Invokes MXAccess Advise.")
|
|
||||||
static final class AdviseCommand extends GatewayCommand {
|
|
||||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
|
||||||
String sessionId;
|
|
||||||
|
|
||||||
@Option(names = "--server-handle", required = true, description = "MXAccess server handle.")
|
|
||||||
int serverHandle;
|
|
||||||
|
|
||||||
@Option(names = "--item-handle", required = true, description = "MXAccess item handle.")
|
|
||||||
int itemHandle;
|
|
||||||
|
|
||||||
AdviseCommand(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
super(clientFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
|
||||||
MxCommandReply reply = client.session(sessionId).adviseRaw(serverHandle, itemHandle);
|
|
||||||
writeOutput("advise", common, json, reply, () -> reply.getKind().name());
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command(name = "write", description = "Invokes MXAccess Write.")
|
|
||||||
static final class WriteCommand extends GatewayCommand {
|
|
||||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
|
||||||
String sessionId;
|
|
||||||
|
|
||||||
@Option(names = "--server-handle", required = true, description = "MXAccess server handle.")
|
|
||||||
int serverHandle;
|
|
||||||
|
|
||||||
@Option(names = "--item-handle", required = true, description = "MXAccess item handle.")
|
|
||||||
int itemHandle;
|
|
||||||
|
|
||||||
@Option(names = "--type", defaultValue = "string", description = "Value type.")
|
|
||||||
String type;
|
|
||||||
|
|
||||||
@Option(names = "--value", required = true, description = "Value text.")
|
|
||||||
String value;
|
|
||||||
|
|
||||||
@Option(names = "--user-id", defaultValue = "0", description = "MXAccess user id.")
|
|
||||||
int userId;
|
|
||||||
|
|
||||||
WriteCommand(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
super(clientFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
|
||||||
MxCommandReply reply =
|
|
||||||
client.session(sessionId).writeRaw(serverHandle, itemHandle, parseValue(type, value), userId);
|
|
||||||
writeOutput("write", common, json, reply, () -> reply.getKind().name());
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command(name = "stream-events", description = "Streams gateway events.")
|
|
||||||
static final class StreamEventsCommand extends GatewayCommand {
|
|
||||||
@Option(names = "--session-id", required = true, description = "Gateway session id.")
|
|
||||||
String sessionId;
|
|
||||||
|
|
||||||
@Option(names = "--after-worker-sequence", defaultValue = "0", description = "Starting worker sequence.")
|
|
||||||
long afterWorkerSequence;
|
|
||||||
|
|
||||||
@Option(names = "--limit", defaultValue = "0", description = "Maximum events to print.")
|
|
||||||
int limit;
|
|
||||||
|
|
||||||
StreamEventsCommand(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
super(clientFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved());
|
|
||||||
MxEventStream events = client.session(sessionId).streamEventsAfter(afterWorkerSequence)) {
|
|
||||||
int count = 0;
|
|
||||||
while (events.hasNext()) {
|
|
||||||
MxEvent event = events.next();
|
|
||||||
if (json) {
|
|
||||||
client.out().println(protoJson(event));
|
|
||||||
} else {
|
|
||||||
client.out().printf("%d %s%n", event.getWorkerSequence(), event.getFamily());
|
|
||||||
}
|
|
||||||
count++;
|
|
||||||
if (limit > 0 && count >= limit) {
|
|
||||||
events.close();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Command(name = "smoke", description = "Runs a bounded open/register/add/advise flow.")
|
|
||||||
static final class SmokeCommand extends GatewayCommand {
|
|
||||||
@Option(names = "--client-name", defaultValue = "mxgw-java-smoke", description = "MXAccess client name.")
|
|
||||||
String clientName;
|
|
||||||
|
|
||||||
@Option(names = "--item", required = true, description = "Item definition.")
|
|
||||||
String item;
|
|
||||||
|
|
||||||
SmokeCommand(MxGatewayCliClientFactory clientFactory) {
|
|
||||||
super(clientFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer call() {
|
|
||||||
try (MxGatewayCliClient client = clientFactory.connect(common.resolved())) {
|
|
||||||
var session = client.openSession(OpenSessionRequest.newBuilder()
|
|
||||||
.setClientSessionName(clientName)
|
|
||||||
.build());
|
|
||||||
MxGatewayCliSession cliSession = client.session(session.getSessionId());
|
|
||||||
int serverHandle = cliSession.register(clientName);
|
|
||||||
int itemHandle = cliSession.addItem(serverHandle, item);
|
|
||||||
cliSession.advise(serverHandle, itemHandle);
|
|
||||||
if (json) {
|
|
||||||
Map<String, Object> output = new LinkedHashMap<>();
|
|
||||||
output.put("command", "smoke");
|
|
||||||
output.put("options", common.redactedJsonMap());
|
|
||||||
output.put("sessionId", session.getSessionId());
|
|
||||||
output.put("serverHandle", serverHandle);
|
|
||||||
output.put("itemHandle", itemHandle);
|
|
||||||
client.out().println(jsonObject(output));
|
|
||||||
} else {
|
|
||||||
client.out().printf(
|
|
||||||
"session=%s server=%d item=%d%n", session.getSessionId(), serverHandle, itemHandle);
|
|
||||||
}
|
|
||||||
client.closeSession(CloseSessionRequest.newBuilder()
|
|
||||||
.setSessionId(session.getSessionId())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class CommonOptions {
|
|
||||||
@Spec
|
|
||||||
CommandSpec spec;
|
|
||||||
|
|
||||||
@Option(names = "--endpoint", defaultValue = "localhost:5000", description = "Gateway endpoint.")
|
|
||||||
String endpoint;
|
|
||||||
|
|
||||||
@Option(names = "--api-key", description = "Gateway API key.")
|
|
||||||
String apiKey = "";
|
|
||||||
|
|
||||||
@Option(names = "--api-key-env", defaultValue = "MXGATEWAY_API_KEY", description = "API key environment variable.")
|
|
||||||
String apiKeyEnv;
|
|
||||||
|
|
||||||
@Option(names = "--plaintext", description = "Use plaintext transport.")
|
|
||||||
boolean plaintext;
|
|
||||||
|
|
||||||
@Option(names = "--ca-file", description = "CA certificate file.")
|
|
||||||
Path caFile;
|
|
||||||
|
|
||||||
@Option(names = "--server-name-override", description = "TLS server name override.")
|
|
||||||
String serverNameOverride = "";
|
|
||||||
|
|
||||||
@Option(names = "--timeout", defaultValue = "30s", description = "Per-call timeout.")
|
|
||||||
String timeout;
|
|
||||||
|
|
||||||
private String resolvedApiKey = "";
|
|
||||||
private Duration resolvedTimeout = Duration.ofSeconds(30);
|
|
||||||
|
|
||||||
CommonOptions resolved() {
|
|
||||||
resolvedApiKey = apiKey == null || apiKey.isBlank() ? System.getenv(apiKeyEnv) : apiKey;
|
|
||||||
if (resolvedApiKey == null) {
|
|
||||||
resolvedApiKey = "";
|
|
||||||
}
|
|
||||||
resolvedTimeout = parseDuration(timeout);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
MxGatewayClientOptions toClientOptions() {
|
|
||||||
return MxGatewayClientOptions.builder()
|
|
||||||
.endpoint(endpoint)
|
|
||||||
.apiKey(resolvedApiKey)
|
|
||||||
.plaintext(plaintext)
|
|
||||||
.caCertificatePath(caFile)
|
|
||||||
.serverNameOverride(serverNameOverride)
|
|
||||||
.callTimeout(resolvedTimeout)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> redactedJsonMap() {
|
|
||||||
Map<String, Object> values = new LinkedHashMap<>();
|
|
||||||
values.put("endpoint", endpoint);
|
|
||||||
values.put("apiKey", MxGatewaySecrets.redactApiKey(resolvedApiKey));
|
|
||||||
values.put("apiKeyEnv", apiKeyEnv);
|
|
||||||
values.put("plaintext", plaintext);
|
|
||||||
values.put("caFile", caFile == null ? "" : caFile.toString());
|
|
||||||
values.put("serverNameOverride", serverNameOverride);
|
|
||||||
values.put("timeout", timeout);
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MxGatewayCliClientFactory {
|
|
||||||
MxGatewayCliClient connect(CommonOptions options);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MxGatewayCliClient extends AutoCloseable {
|
|
||||||
PrintWriter out();
|
|
||||||
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply openSession(OpenSessionRequest request);
|
|
||||||
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply closeSession(CloseSessionRequest request);
|
|
||||||
|
|
||||||
MxGatewayCliSession session(String sessionId);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void close();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MxGatewayCliSession {
|
|
||||||
int register(String clientName);
|
|
||||||
|
|
||||||
MxCommandReply registerRaw(String clientName);
|
|
||||||
|
|
||||||
int addItem(int serverHandle, String itemDefinition);
|
|
||||||
|
|
||||||
MxCommandReply addItemRaw(int serverHandle, String itemDefinition);
|
|
||||||
|
|
||||||
void advise(int serverHandle, int itemHandle);
|
|
||||||
|
|
||||||
MxCommandReply adviseRaw(int serverHandle, int itemHandle);
|
|
||||||
|
|
||||||
MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId);
|
|
||||||
|
|
||||||
MxEventStream streamEventsAfter(long afterWorkerSequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class GrpcMxGatewayCliClientFactory implements MxGatewayCliClientFactory {
|
|
||||||
@Override
|
|
||||||
public MxGatewayCliClient connect(CommonOptions options) {
|
|
||||||
return new GrpcMxGatewayCliClient(MxGatewayClient.connect(options.toClientOptions()), options.spec.commandLine().getOut());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static final class GrpcMxGatewayCliClient implements MxGatewayCliClient {
|
|
||||||
private final MxGatewayClient client;
|
|
||||||
private final PrintWriter out;
|
|
||||||
|
|
||||||
GrpcMxGatewayCliClient(MxGatewayClient client, PrintWriter out) {
|
|
||||||
this.client = client;
|
|
||||||
this.out = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PrintWriter out() {
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply openSession(OpenSessionRequest request) {
|
|
||||||
return client.openSessionRaw(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply closeSession(CloseSessionRequest request) {
|
|
||||||
return client.closeSessionRaw(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxGatewayCliSession session(String sessionId) {
|
|
||||||
return new GrpcMxGatewayCliSession(MxGatewaySession.forSessionId(client, sessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
client.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
record GrpcMxGatewayCliSession(MxGatewaySession session) implements MxGatewayCliSession {
|
|
||||||
@Override
|
|
||||||
public int register(String clientName) {
|
|
||||||
return session.register(clientName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxCommandReply registerRaw(String clientName) {
|
|
||||||
return session.registerRaw(clientName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int addItem(int serverHandle, String itemDefinition) {
|
|
||||||
return session.addItem(serverHandle, itemDefinition);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
|
|
||||||
return session.addItemRaw(serverHandle, itemDefinition);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void advise(int serverHandle, int itemHandle) {
|
|
||||||
session.advise(serverHandle, itemHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
|
|
||||||
return session.adviseRaw(serverHandle, itemHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
|
||||||
return session.writeRaw(serverHandle, itemHandle, value, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxEventStream streamEventsAfter(long afterWorkerSequence) {
|
|
||||||
return session.streamEventsAfter(afterWorkerSequence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextSupplier {
|
|
||||||
String get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void writeOutput(
|
|
||||||
String command, CommonOptions common, boolean json, Message reply, TextSupplier textSupplier) {
|
|
||||||
PrintWriter out = common.spec.commandLine().getOut();
|
|
||||||
if (json) {
|
|
||||||
Map<String, Object> output = new LinkedHashMap<>();
|
|
||||||
output.put("command", command);
|
|
||||||
output.put("options", common.redactedJsonMap());
|
|
||||||
output.put("reply", new RawJson(protoJson(reply)));
|
|
||||||
out.println(jsonObject(output));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
out.println(textSupplier.get());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MxValue parseValue(String type, String text) {
|
|
||||||
return switch (type) {
|
|
||||||
case "bool" -> MxValues.boolValue(Boolean.parseBoolean(text));
|
|
||||||
case "int32" -> MxValues.int32Value(Integer.parseInt(text));
|
|
||||||
case "int64" -> MxValues.int64Value(Long.parseLong(text));
|
|
||||||
case "float" -> MxValues.floatValue(Float.parseFloat(text));
|
|
||||||
case "double" -> MxValues.doubleValue(Double.parseDouble(text));
|
|
||||||
case "string" -> MxValues.stringValue(text);
|
|
||||||
default -> throw new IllegalArgumentException("unsupported value type " + type);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Duration parseDuration(String value) {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
return Duration.ofSeconds(30);
|
|
||||||
}
|
|
||||||
if (value.startsWith("P")) {
|
|
||||||
return Duration.parse(value);
|
|
||||||
}
|
|
||||||
if (value.endsWith("ms")) {
|
|
||||||
return Duration.ofMillis(Long.parseLong(value.substring(0, value.length() - 2)));
|
|
||||||
}
|
|
||||||
if (value.endsWith("s")) {
|
|
||||||
return Duration.ofSeconds(Long.parseLong(value.substring(0, value.length() - 1)));
|
|
||||||
}
|
|
||||||
if (value.endsWith("m")) {
|
|
||||||
return Duration.ofMinutes(Long.parseLong(value.substring(0, value.length() - 1)));
|
|
||||||
}
|
|
||||||
return Duration.parse(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String protoJson(Message message) {
|
|
||||||
try {
|
|
||||||
return JsonFormat.printer().omittingInsignificantWhitespace().print(message);
|
|
||||||
} catch (Exception error) {
|
|
||||||
throw new IllegalStateException("failed to write protobuf JSON", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String jsonObject(Map<String, Object> values) {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
builder.append('{');
|
|
||||||
boolean first = true;
|
|
||||||
for (Map.Entry<String, Object> entry : values.entrySet()) {
|
|
||||||
if (!first) {
|
|
||||||
builder.append(',');
|
|
||||||
}
|
|
||||||
first = false;
|
|
||||||
builder.append(jsonString(entry.getKey())).append(':').append(jsonValue(entry.getValue()));
|
|
||||||
}
|
|
||||||
builder.append('}');
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static String jsonValue(Object value) {
|
|
||||||
if (value == null) {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
if (value instanceof RawJson rawJson) {
|
|
||||||
return rawJson.value();
|
|
||||||
}
|
|
||||||
if (value instanceof String string) {
|
|
||||||
return jsonString(string);
|
|
||||||
}
|
|
||||||
if (value instanceof Number || value instanceof Boolean) {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
if (value instanceof Map<?, ?> map) {
|
|
||||||
return jsonObject((Map<String, Object>) map);
|
|
||||||
}
|
|
||||||
return jsonString(value.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String jsonString(String value) {
|
|
||||||
return '"'
|
|
||||||
+ value.replace("\\", "\\\\")
|
|
||||||
.replace("\"", "\\\"")
|
|
||||||
.replace("\r", "\\r")
|
|
||||||
.replace("\n", "\\n")
|
|
||||||
+ '"';
|
|
||||||
}
|
|
||||||
|
|
||||||
private record RawJson(String value) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-241
@@ -1,241 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.cli;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.AddItemReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.RegisterReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.SessionState;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
final class MxGatewayCliTests {
|
|
||||||
@Test
|
|
||||||
void versionCommandPrintsProtocolVersions() {
|
|
||||||
CliRun run = execute(new FakeClientFactory(), "version");
|
|
||||||
|
|
||||||
assertEquals(0, run.exitCode());
|
|
||||||
assertEquals("", run.errors());
|
|
||||||
assertTrue(run.output().contains("mxgateway-java 0.1.0"));
|
|
||||||
assertTrue(run.output().contains("gatewayProtocolVersion=1"));
|
|
||||||
assertTrue(run.output().contains("workerProtocolVersion=1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void versionCommandPrintsJson() {
|
|
||||||
CliRun run = execute(new FakeClientFactory(), "version", "--json");
|
|
||||||
|
|
||||||
assertEquals(0, run.exitCode());
|
|
||||||
assertTrue(run.output().contains("\"clientVersion\":\"0.1.0\""));
|
|
||||||
assertTrue(run.output().contains("\"gatewayProtocolVersion\":1"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void openSessionJsonRedactsApiKey() {
|
|
||||||
CliRun run = execute(
|
|
||||||
new FakeClientFactory(),
|
|
||||||
"open-session",
|
|
||||||
"--endpoint",
|
|
||||||
"localhost:5000",
|
|
||||||
"--api-key",
|
|
||||||
"mxgw_visible_secret",
|
|
||||||
"--plaintext",
|
|
||||||
"--client-session-name",
|
|
||||||
"java-cli",
|
|
||||||
"--json");
|
|
||||||
|
|
||||||
assertEquals(0, run.exitCode());
|
|
||||||
assertTrue(run.output().contains("\"command\":\"open-session\""));
|
|
||||||
assertTrue(run.output().contains("\"sessionId\":\"session-cli\""));
|
|
||||||
assertTrue(run.output().contains("mxgw***********cret"));
|
|
||||||
assertFalse(run.output().contains("visible_secret"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void writeBuildsTypedValueFromParserOptions() {
|
|
||||||
FakeClientFactory factory = new FakeClientFactory();
|
|
||||||
CliRun run = execute(
|
|
||||||
factory,
|
|
||||||
"write",
|
|
||||||
"--session-id",
|
|
||||||
"session-cli",
|
|
||||||
"--server-handle",
|
|
||||||
"12",
|
|
||||||
"--item-handle",
|
|
||||||
"34",
|
|
||||||
"--type",
|
|
||||||
"int32",
|
|
||||||
"--value",
|
|
||||||
"123",
|
|
||||||
"--json");
|
|
||||||
|
|
||||||
assertEquals(0, run.exitCode());
|
|
||||||
assertEquals(123, factory.client.session.lastWriteValue.getInt32Value());
|
|
||||||
assertTrue(run.output().contains("\"kind\":\"MX_COMMAND_KIND_WRITE\""));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void smokeCommandRunsOpenRegisterAddAdviseAndClose() {
|
|
||||||
FakeClientFactory factory = new FakeClientFactory();
|
|
||||||
CliRun run = execute(factory, "smoke", "--item", "TestObject.TestInt", "--json");
|
|
||||||
|
|
||||||
assertEquals(0, run.exitCode());
|
|
||||||
assertTrue(factory.client.session.registerCalled);
|
|
||||||
assertTrue(factory.client.session.addItemCalled);
|
|
||||||
assertTrue(factory.client.session.adviseCalled);
|
|
||||||
assertTrue(factory.client.closeCalled);
|
|
||||||
assertTrue(run.output().contains("\"serverHandle\":42"));
|
|
||||||
assertTrue(run.output().contains("\"itemHandle\":7"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CliRun execute(MxGatewayCli.MxGatewayCliClientFactory factory, String... args) {
|
|
||||||
StringWriter output = new StringWriter();
|
|
||||||
StringWriter errors = new StringWriter();
|
|
||||||
int exitCode = MxGatewayCli.execute(
|
|
||||||
factory,
|
|
||||||
new PrintWriter(output, true),
|
|
||||||
new PrintWriter(errors, true),
|
|
||||||
args);
|
|
||||||
return new CliRun(exitCode, output.toString(), errors.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private record CliRun(int exitCode, String output, String errors) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class FakeClientFactory implements MxGatewayCli.MxGatewayCliClientFactory {
|
|
||||||
private FakeClient client;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxGatewayCli.MxGatewayCliClient connect(MxGatewayCli.CommonOptions options) {
|
|
||||||
client = new FakeClient(options.spec.commandLine().getOut());
|
|
||||||
return client;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class FakeClient implements MxGatewayCli.MxGatewayCliClient {
|
|
||||||
private final PrintWriter out;
|
|
||||||
private final FakeSession session = new FakeSession();
|
|
||||||
private boolean closeCalled;
|
|
||||||
|
|
||||||
private FakeClient(PrintWriter out) {
|
|
||||||
this.out = out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PrintWriter out() {
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OpenSessionReply openSession(OpenSessionRequest request) {
|
|
||||||
return OpenSessionReply.newBuilder()
|
|
||||||
.setSessionId("session-cli")
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public CloseSessionReply closeSession(CloseSessionRequest request) {
|
|
||||||
closeCalled = true;
|
|
||||||
return CloseSessionReply.newBuilder()
|
|
||||||
.setSessionId(request.getSessionId())
|
|
||||||
.setFinalState(SessionState.SESSION_STATE_CLOSED)
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxGatewayCli.MxGatewayCliSession session(String sessionId) {
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class FakeSession implements MxGatewayCli.MxGatewayCliSession {
|
|
||||||
private boolean registerCalled;
|
|
||||||
private boolean addItemCalled;
|
|
||||||
private boolean adviseCalled;
|
|
||||||
private MxValue lastWriteValue;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int register(String clientName) {
|
|
||||||
registerCalled = true;
|
|
||||||
return 42;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxCommandReply registerRaw(String clientName) {
|
|
||||||
registerCalled = true;
|
|
||||||
return MxCommandReply.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.setRegister(RegisterReply.newBuilder().setServerHandle(42))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int addItem(int serverHandle, String itemDefinition) {
|
|
||||||
addItemCalled = true;
|
|
||||||
return 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
|
|
||||||
addItemCalled = true;
|
|
||||||
return MxCommandReply.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.setAddItem(AddItemReply.newBuilder().setItemHandle(7))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void advise(int serverHandle, int itemHandle) {
|
|
||||||
adviseCalled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
|
|
||||||
adviseCalled = true;
|
|
||||||
return MxCommandReply.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
|
||||||
lastWriteValue = value;
|
|
||||||
return MxCommandReply.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public com.dohertylan.mxgateway.client.MxEventStream streamEventsAfter(long afterWorkerSequence) {
|
|
||||||
throw new UnsupportedOperationException("stream-events is covered by client tests");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ProtocolStatus ok() {
|
|
||||||
return ProtocolStatus.newBuilder()
|
|
||||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'java-library'
|
|
||||||
id 'com.google.protobuf'
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
api "com.google.protobuf:protobuf-java-util:${protobufVersion}"
|
|
||||||
api "com.google.protobuf:protobuf-java:${protobufVersion}"
|
|
||||||
api "io.grpc:grpc-protobuf:${grpcVersion}"
|
|
||||||
api "io.grpc:grpc-stub:${grpcVersion}"
|
|
||||||
|
|
||||||
implementation "com.google.guava:guava:${guavaVersion}"
|
|
||||||
implementation "io.grpc:grpc-netty-shaded:${grpcVersion}"
|
|
||||||
|
|
||||||
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
|
|
||||||
|
|
||||||
testImplementation "com.google.code.gson:gson:${gsonVersion}"
|
|
||||||
testImplementation "io.grpc:grpc-inprocess:${grpcVersion}"
|
|
||||||
testImplementation "io.grpc:grpc-testing:${grpcVersion}"
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
main {
|
|
||||||
proto {
|
|
||||||
srcDir rootProject.file('../../src/MxGateway.Contracts/Protos')
|
|
||||||
include 'mxaccess_gateway.proto'
|
|
||||||
include 'mxaccess_worker.proto'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protobuf {
|
|
||||||
protoc {
|
|
||||||
artifact = "com.google.protobuf:protoc:${protobufVersion}"
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
grpc {
|
|
||||||
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
generatedFilesBaseDir = rootProject.file('src/main/generated').absolutePath
|
|
||||||
|
|
||||||
generateProtoTasks {
|
|
||||||
all().configureEach {
|
|
||||||
plugins {
|
|
||||||
grpc {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-14
@@ -1,14 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
|
||||||
|
|
||||||
public final class MxAccessException extends MxGatewayCommandException {
|
|
||||||
public MxAccessException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
|
||||||
super(operation, protocolStatus, reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxAccessException(String operation, MxCommandReply reply) {
|
|
||||||
super(operation, reply == null ? null : reply.getProtocolStatus(), reply);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-117
@@ -1,117 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import io.grpc.Status;
|
|
||||||
import io.grpc.StatusRuntimeException;
|
|
||||||
import io.grpc.stub.ClientCallStreamObserver;
|
|
||||||
import io.grpc.stub.ClientResponseObserver;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.ArrayBlockingQueue;
|
|
||||||
import java.util.concurrent.BlockingQueue;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
|
||||||
|
|
||||||
public final class MxEventStream implements Iterator<MxEvent>, AutoCloseable {
|
|
||||||
private static final Object END = new Object();
|
|
||||||
|
|
||||||
private final BlockingQueue<Object> queue;
|
|
||||||
private volatile ClientCallStreamObserver<StreamEventsRequest> requestStream;
|
|
||||||
private volatile boolean closed;
|
|
||||||
private Object next;
|
|
||||||
|
|
||||||
MxEventStream(int capacity) {
|
|
||||||
queue = new ArrayBlockingQueue<>(capacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClientResponseObserver<StreamEventsRequest, MxEvent> observer() {
|
|
||||||
return new ClientResponseObserver<>() {
|
|
||||||
@Override
|
|
||||||
public void beforeStart(ClientCallStreamObserver<StreamEventsRequest> requestStream) {
|
|
||||||
MxEventStream.this.requestStream = requestStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNext(MxEvent value) {
|
|
||||||
offer(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Throwable error) {
|
|
||||||
if (Status.fromThrowable(error).getCode() == Status.Code.CANCELLED && closed) {
|
|
||||||
offer(END);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
offer(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCompleted() {
|
|
||||||
offer(END);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasNext() {
|
|
||||||
if (next == END) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (next == null) {
|
|
||||||
next = take();
|
|
||||||
}
|
|
||||||
if (next instanceof RuntimeException runtimeException) {
|
|
||||||
next = END;
|
|
||||||
throw runtimeException;
|
|
||||||
}
|
|
||||||
if (next instanceof Throwable throwable) {
|
|
||||||
next = END;
|
|
||||||
throw new MxGatewayException("gateway stream events failed: " + throwable.getMessage(), throwable);
|
|
||||||
}
|
|
||||||
return next != END;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MxEvent next() {
|
|
||||||
if (!hasNext()) {
|
|
||||||
throw new NoSuchElementException();
|
|
||||||
}
|
|
||||||
Object value = next;
|
|
||||||
next = null;
|
|
||||||
return (MxEvent) value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
closed = true;
|
|
||||||
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream;
|
|
||||||
if (stream != null) {
|
|
||||||
stream.cancel("client cancelled event stream", null);
|
|
||||||
}
|
|
||||||
offer(END);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object take() {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
return queue.take();
|
|
||||||
} catch (InterruptedException error) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
return new StatusRuntimeException(Status.CANCELLED.withDescription("interrupted while reading events"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void offer(Object value) {
|
|
||||||
Objects.requireNonNull(value, "value");
|
|
||||||
if (value == END) {
|
|
||||||
queue.offer(value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
queue.put(value);
|
|
||||||
} catch (InterruptedException error) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import io.grpc.CallOptions;
|
|
||||||
import io.grpc.Channel;
|
|
||||||
import io.grpc.ClientCall;
|
|
||||||
import io.grpc.ClientInterceptor;
|
|
||||||
import io.grpc.ForwardingClientCall;
|
|
||||||
import io.grpc.Metadata;
|
|
||||||
import io.grpc.MethodDescriptor;
|
|
||||||
|
|
||||||
public final class MxGatewayAuthInterceptor implements ClientInterceptor {
|
|
||||||
static final Metadata.Key<String> AUTHORIZATION_HEADER =
|
|
||||||
Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER);
|
|
||||||
|
|
||||||
private final String apiKey;
|
|
||||||
|
|
||||||
public MxGatewayAuthInterceptor(String apiKey) {
|
|
||||||
this.apiKey = apiKey == null ? "" : apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
|
|
||||||
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
|
|
||||||
ClientCall<ReqT, RespT> call = next.newCall(method, callOptions);
|
|
||||||
if (apiKey.isBlank()) {
|
|
||||||
return call;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ForwardingClientCall.SimpleForwardingClientCall<>(call) {
|
|
||||||
@Override
|
|
||||||
public void start(Listener<RespT> responseListener, Metadata headers) {
|
|
||||||
headers.put(AUTHORIZATION_HEADER, "Bearer " + apiKey);
|
|
||||||
super.start(responseListener, headers);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
public final class MxGatewayAuthenticationException extends MxGatewayException {
|
|
||||||
public MxGatewayAuthenticationException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
public final class MxGatewayAuthorizationException extends MxGatewayException {
|
|
||||||
public MxGatewayAuthorizationException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-228
@@ -1,228 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
|
||||||
import com.google.common.util.concurrent.Futures;
|
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
|
||||||
import com.google.protobuf.Duration;
|
|
||||||
import io.grpc.Channel;
|
|
||||||
import io.grpc.ClientInterceptors;
|
|
||||||
import io.grpc.ManagedChannel;
|
|
||||||
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
|
|
||||||
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
|
|
||||||
import io.grpc.stub.StreamObserver;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import javax.net.ssl.SSLException;
|
|
||||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
|
||||||
|
|
||||||
public final class MxGatewayClient implements AutoCloseable {
|
|
||||||
private final ManagedChannel ownedChannel;
|
|
||||||
private final MxGatewayClientOptions options;
|
|
||||||
private final MxAccessGatewayGrpc.MxAccessGatewayBlockingStub blockingStub;
|
|
||||||
private final MxAccessGatewayGrpc.MxAccessGatewayFutureStub futureStub;
|
|
||||||
private final MxAccessGatewayGrpc.MxAccessGatewayStub asyncStub;
|
|
||||||
|
|
||||||
private MxGatewayClient(ManagedChannel channel, MxGatewayClientOptions options) {
|
|
||||||
this.ownedChannel = channel;
|
|
||||||
this.options = options;
|
|
||||||
Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
|
|
||||||
blockingStub = MxAccessGatewayGrpc.newBlockingStub(intercepted);
|
|
||||||
futureStub = MxAccessGatewayGrpc.newFutureStub(intercepted);
|
|
||||||
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxGatewayClient(Channel channel, MxGatewayClientOptions options) {
|
|
||||||
this.ownedChannel = null;
|
|
||||||
this.options = Objects.requireNonNull(options, "options");
|
|
||||||
Channel intercepted = ClientInterceptors.intercept(channel, new MxGatewayAuthInterceptor(options.apiKey()));
|
|
||||||
blockingStub = MxAccessGatewayGrpc.newBlockingStub(intercepted);
|
|
||||||
futureStub = MxAccessGatewayGrpc.newFutureStub(intercepted);
|
|
||||||
asyncStub = MxAccessGatewayGrpc.newStub(intercepted);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxGatewayClient connect(MxGatewayClientOptions options) {
|
|
||||||
return new MxGatewayClient(createChannel(options), options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxAccessGatewayGrpc.MxAccessGatewayBlockingStub rawBlockingStub() {
|
|
||||||
return withDeadline(blockingStub);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxAccessGatewayGrpc.MxAccessGatewayFutureStub rawFutureStub() {
|
|
||||||
return withDeadline(futureStub);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxAccessGatewayGrpc.MxAccessGatewayStub rawAsyncStub() {
|
|
||||||
return withDeadline(asyncStub);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxGatewaySession openSession(OpenSessionRequest request) {
|
|
||||||
OpenSessionReply reply = openSessionRaw(request);
|
|
||||||
return new MxGatewaySession(this, reply);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxGatewaySession openSession(String clientSessionName) {
|
|
||||||
return openSession(OpenSessionRequest.newBuilder()
|
|
||||||
.setClientSessionName(clientSessionName)
|
|
||||||
.setCommandTimeout(Duration.newBuilder()
|
|
||||||
.setSeconds(options.callTimeout().toSeconds())
|
|
||||||
.setNanos(options.callTimeout().toNanosPart())
|
|
||||||
.build())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public OpenSessionReply openSessionRaw(OpenSessionRequest request) {
|
|
||||||
try {
|
|
||||||
OpenSessionReply reply = rawBlockingStub().openSession(request);
|
|
||||||
MxGatewayErrors.ensureProtocolSuccess("open session", reply.getProtocolStatus(), null);
|
|
||||||
return reply;
|
|
||||||
} catch (RuntimeException error) {
|
|
||||||
if (error instanceof MxGatewayException) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw MxGatewayErrors.fromGrpc("open session", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<OpenSessionReply> openSessionAsync(OpenSessionRequest request) {
|
|
||||||
CompletableFuture<OpenSessionReply> future = toCompletable(rawFutureStub().openSession(request));
|
|
||||||
return future.thenApply(reply -> {
|
|
||||||
MxGatewayErrors.ensureProtocolSuccess("open session", reply.getProtocolStatus(), null);
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxCommandReply invoke(MxCommandRequest request) {
|
|
||||||
try {
|
|
||||||
MxCommandReply reply = rawBlockingStub().invoke(request);
|
|
||||||
MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
|
|
||||||
MxGatewayErrors.ensureMxAccessSuccess("invoke", reply);
|
|
||||||
return reply;
|
|
||||||
} catch (RuntimeException error) {
|
|
||||||
if (error instanceof MxGatewayException) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw MxGatewayErrors.fromGrpc("invoke", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompletableFuture<MxCommandReply> invokeAsync(MxCommandRequest request) {
|
|
||||||
CompletableFuture<MxCommandReply> future = toCompletable(rawFutureStub().invoke(request));
|
|
||||||
return future.thenApply(reply -> {
|
|
||||||
MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
|
|
||||||
MxGatewayErrors.ensureMxAccessSuccess("invoke", reply);
|
|
||||||
return reply;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public CloseSessionReply closeSessionRaw(CloseSessionRequest request) {
|
|
||||||
try {
|
|
||||||
CloseSessionReply reply = rawBlockingStub().closeSession(request);
|
|
||||||
MxGatewayErrors.ensureProtocolSuccess("close session", reply.getProtocolStatus(), null);
|
|
||||||
return reply;
|
|
||||||
} catch (RuntimeException error) {
|
|
||||||
if (error instanceof MxGatewayException) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
throw MxGatewayErrors.fromGrpc("close session", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxEventStream streamEvents(StreamEventsRequest request) {
|
|
||||||
MxEventStream stream = new MxEventStream(16);
|
|
||||||
rawAsyncStub().streamEvents(request, stream.observer());
|
|
||||||
return stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxGatewayEventSubscription streamEventsAsync(
|
|
||||||
StreamEventsRequest request, StreamObserver<MxEvent> observer) {
|
|
||||||
MxGatewayEventSubscription subscription = new MxGatewayEventSubscription();
|
|
||||||
rawAsyncStub().streamEvents(request, subscription.wrap(observer));
|
|
||||||
return subscription;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
if (ownedChannel != null) {
|
|
||||||
ownedChannel.shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void closeAndAwaitTermination() throws InterruptedException {
|
|
||||||
if (ownedChannel != null) {
|
|
||||||
ownedChannel.shutdown();
|
|
||||||
ownedChannel.awaitTermination(options.connectTimeout().toMillis(), TimeUnit.MILLISECONDS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ManagedChannel createChannel(MxGatewayClientOptions options) {
|
|
||||||
NettyChannelBuilder builder = NettyChannelBuilder.forTarget(options.endpoint())
|
|
||||||
.maxInboundMessageSize(16 * 1024 * 1024);
|
|
||||||
if (!options.connectTimeout().isNegative()) {
|
|
||||||
builder.withOption(
|
|
||||||
io.grpc.netty.shaded.io.netty.channel.ChannelOption.CONNECT_TIMEOUT_MILLIS,
|
|
||||||
Math.toIntExact(options.connectTimeout().toMillis()));
|
|
||||||
}
|
|
||||||
if (options.plaintext()) {
|
|
||||||
builder.usePlaintext();
|
|
||||||
} else if (options.caCertificatePath() != null) {
|
|
||||||
try {
|
|
||||||
builder.sslContext(GrpcSslContexts.forClient()
|
|
||||||
.trustManager(options.caCertificatePath().toFile())
|
|
||||||
.build());
|
|
||||||
} catch (SSLException error) {
|
|
||||||
throw new MxGatewayException("failed to configure gateway TLS", error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
builder.useTransportSecurity();
|
|
||||||
}
|
|
||||||
if (!options.serverNameOverride().isBlank()) {
|
|
||||||
builder.overrideAuthority(options.serverNameOverride());
|
|
||||||
}
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private <T extends io.grpc.stub.AbstractStub<T>> T withDeadline(T stub) {
|
|
||||||
if (options.callTimeout().isNegative()) {
|
|
||||||
return stub;
|
|
||||||
}
|
|
||||||
return stub.withDeadlineAfter(options.callTimeout().toNanos(), TimeUnit.NANOSECONDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <T> CompletableFuture<T> toCompletable(com.google.common.util.concurrent.ListenableFuture<T> source) {
|
|
||||||
CompletableFuture<T> target = new CompletableFuture<>();
|
|
||||||
Futures.addCallback(
|
|
||||||
source,
|
|
||||||
new FutureCallback<>() {
|
|
||||||
@Override
|
|
||||||
public void onSuccess(T result) {
|
|
||||||
target.complete(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(Throwable error) {
|
|
||||||
if (error instanceof RuntimeException runtimeException) {
|
|
||||||
target.completeExceptionally(MxGatewayErrors.fromGrpc("async call", runtimeException));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
target.completeExceptionally(error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MoreExecutors.directExecutor());
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ProtocolStatusCode okStatusCode() {
|
|
||||||
return ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-146
@@ -1,146 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public final class MxGatewayClientOptions {
|
|
||||||
private static final Duration DEFAULT_CONNECT_TIMEOUT = Duration.ofSeconds(10);
|
|
||||||
private static final Duration DEFAULT_CALL_TIMEOUT = Duration.ofSeconds(30);
|
|
||||||
|
|
||||||
private final String endpoint;
|
|
||||||
private final String apiKey;
|
|
||||||
private final boolean plaintext;
|
|
||||||
private final Path caCertificatePath;
|
|
||||||
private final String serverNameOverride;
|
|
||||||
private final Duration connectTimeout;
|
|
||||||
private final Duration callTimeout;
|
|
||||||
|
|
||||||
private MxGatewayClientOptions(Builder builder) {
|
|
||||||
endpoint = requireText(builder.endpoint, "endpoint");
|
|
||||||
apiKey = builder.apiKey == null ? "" : builder.apiKey;
|
|
||||||
plaintext = builder.plaintext;
|
|
||||||
caCertificatePath = builder.caCertificatePath;
|
|
||||||
serverNameOverride = builder.serverNameOverride == null ? "" : builder.serverNameOverride;
|
|
||||||
connectTimeout = builder.connectTimeout == null ? DEFAULT_CONNECT_TIMEOUT : builder.connectTimeout;
|
|
||||||
callTimeout = builder.callTimeout == null ? DEFAULT_CALL_TIMEOUT : builder.callTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Builder builder() {
|
|
||||||
return new Builder();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String endpoint() {
|
|
||||||
return endpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String apiKey() {
|
|
||||||
return apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String redactedApiKey() {
|
|
||||||
return MxGatewaySecrets.redactApiKey(apiKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean plaintext() {
|
|
||||||
return plaintext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Path caCertificatePath() {
|
|
||||||
return caCertificatePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String serverNameOverride() {
|
|
||||||
return serverNameOverride;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Duration connectTimeout() {
|
|
||||||
return connectTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Duration callTimeout() {
|
|
||||||
return callTimeout;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "MxGatewayClientOptions{"
|
|
||||||
+ "endpoint='"
|
|
||||||
+ endpoint
|
|
||||||
+ '\''
|
|
||||||
+ ", apiKey='"
|
|
||||||
+ redactedApiKey()
|
|
||||||
+ '\''
|
|
||||||
+ ", plaintext="
|
|
||||||
+ plaintext
|
|
||||||
+ ", caCertificatePath="
|
|
||||||
+ caCertificatePath
|
|
||||||
+ ", serverNameOverride='"
|
|
||||||
+ serverNameOverride
|
|
||||||
+ '\''
|
|
||||||
+ ", connectTimeout="
|
|
||||||
+ connectTimeout
|
|
||||||
+ ", callTimeout="
|
|
||||||
+ callTimeout
|
|
||||||
+ '}';
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String requireText(String value, String name) {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
throw new IllegalArgumentException(name + " is required");
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class Builder {
|
|
||||||
private String endpoint;
|
|
||||||
private String apiKey;
|
|
||||||
private boolean plaintext;
|
|
||||||
private Path caCertificatePath;
|
|
||||||
private String serverNameOverride;
|
|
||||||
private Duration connectTimeout;
|
|
||||||
private Duration callTimeout;
|
|
||||||
|
|
||||||
private Builder() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder endpoint(String value) {
|
|
||||||
endpoint = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder apiKey(String value) {
|
|
||||||
apiKey = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder plaintext(boolean value) {
|
|
||||||
plaintext = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder caCertificatePath(Path value) {
|
|
||||||
caCertificatePath = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder serverNameOverride(String value) {
|
|
||||||
serverNameOverride = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder connectTimeout(Duration value) {
|
|
||||||
connectTimeout = Objects.requireNonNull(value, "connectTimeout");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder callTimeout(Duration value) {
|
|
||||||
callTimeout = Objects.requireNonNull(value, "callTimeout");
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxGatewayClientOptions build() {
|
|
||||||
return new MxGatewayClientOptions(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
public final class MxGatewayClientVersion {
|
|
||||||
private static final int GATEWAY_PROTOCOL_VERSION = 1;
|
|
||||||
private static final int WORKER_PROTOCOL_VERSION = 1;
|
|
||||||
private static final String CLIENT_VERSION = "0.1.0";
|
|
||||||
|
|
||||||
private MxGatewayClientVersion() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String clientVersion() {
|
|
||||||
return CLIENT_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int gatewayProtocolVersion() {
|
|
||||||
return GATEWAY_PROTOCOL_VERSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int workerProtocolVersion() {
|
|
||||||
return WORKER_PROTOCOL_VERSION;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-23
@@ -1,23 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
|
||||||
|
|
||||||
public class MxGatewayCommandException extends MxGatewayException {
|
|
||||||
private final ProtocolStatus protocolStatus;
|
|
||||||
private final MxCommandReply reply;
|
|
||||||
|
|
||||||
public MxGatewayCommandException(String operation, ProtocolStatus protocolStatus, MxCommandReply reply) {
|
|
||||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
|
||||||
this.protocolStatus = protocolStatus;
|
|
||||||
this.reply = reply;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProtocolStatus protocolStatus() {
|
|
||||||
return protocolStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxCommandReply reply() {
|
|
||||||
return reply;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-72
@@ -1,72 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import io.grpc.Status;
|
|
||||||
import io.grpc.StatusRuntimeException;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
|
||||||
|
|
||||||
final class MxGatewayErrors {
|
|
||||||
private MxGatewayErrors() {
|
|
||||||
}
|
|
||||||
|
|
||||||
static RuntimeException fromGrpc(String operation, RuntimeException error) {
|
|
||||||
if (error instanceof StatusRuntimeException statusError) {
|
|
||||||
Status status = statusError.getStatus();
|
|
||||||
String message = MxGatewaySecrets.redactCredentials(status.getDescription());
|
|
||||||
return switch (status.getCode()) {
|
|
||||||
case UNAUTHENTICATED -> new MxGatewayAuthenticationException(
|
|
||||||
"authentication failed: " + message, statusError);
|
|
||||||
case PERMISSION_DENIED -> new MxGatewayAuthorizationException(
|
|
||||||
"authorization failed: " + message, statusError);
|
|
||||||
case DEADLINE_EXCEEDED -> new MxGatewayException("gateway call timed out: " + message, statusError);
|
|
||||||
case CANCELLED -> new MxGatewayException("gateway call cancelled: " + message, statusError);
|
|
||||||
default -> new MxGatewayException("gateway " + operation + " failed: " + message, statusError);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return new MxGatewayException("gateway " + operation + " failed: " + error.getMessage(), error);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ensureProtocolSuccess(String operation, ProtocolStatus status, MxCommandReply reply) {
|
|
||||||
if (status == null || status.getCode() == ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw switch (status.getCode()) {
|
|
||||||
case PROTOCOL_STATUS_CODE_SESSION_NOT_FOUND, PROTOCOL_STATUS_CODE_SESSION_NOT_READY ->
|
|
||||||
new MxGatewaySessionException(operation, status);
|
|
||||||
case PROTOCOL_STATUS_CODE_WORKER_UNAVAILABLE, PROTOCOL_STATUS_CODE_PROTOCOL_VIOLATION ->
|
|
||||||
new MxGatewayWorkerException(operation, status);
|
|
||||||
case PROTOCOL_STATUS_CODE_MXACCESS_FAILURE -> new MxAccessException(operation, status, reply);
|
|
||||||
default -> new MxGatewayCommandException(operation, status, reply);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static void ensureMxAccessSuccess(String operation, MxCommandReply reply) {
|
|
||||||
if (reply == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (reply.hasHresult() && reply.getHresult() != 0) {
|
|
||||||
throw new MxAccessException(operation, reply);
|
|
||||||
}
|
|
||||||
for (var status : reply.getStatusesList()) {
|
|
||||||
if (!MxStatuses.succeeded(status)) {
|
|
||||||
throw new MxAccessException(operation, reply);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static String protocolStatusMessage(String operation, ProtocolStatus status) {
|
|
||||||
if (status == null) {
|
|
||||||
return "mxgateway " + operation + " failed with missing protocol status";
|
|
||||||
}
|
|
||||||
if (status.getMessage().isBlank()) {
|
|
||||||
return "mxgateway " + operation + " failed with protocol status " + status.getCode();
|
|
||||||
}
|
|
||||||
return "mxgateway " + operation + " failed with protocol status "
|
|
||||||
+ status.getCode()
|
|
||||||
+ ": "
|
|
||||||
+ status.getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import io.grpc.stub.ClientCallStreamObserver;
|
|
||||||
import io.grpc.stub.ClientResponseObserver;
|
|
||||||
import io.grpc.stub.StreamObserver;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
|
||||||
|
|
||||||
public final class MxGatewayEventSubscription implements AutoCloseable {
|
|
||||||
private final AtomicReference<ClientCallStreamObserver<StreamEventsRequest>> requestStream = new AtomicReference<>();
|
|
||||||
|
|
||||||
ClientResponseObserver<StreamEventsRequest, MxEvent> wrap(StreamObserver<MxEvent> observer) {
|
|
||||||
return new ClientResponseObserver<>() {
|
|
||||||
@Override
|
|
||||||
public void beforeStart(ClientCallStreamObserver<StreamEventsRequest> stream) {
|
|
||||||
requestStream.set(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNext(MxEvent value) {
|
|
||||||
observer.onNext(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Throwable error) {
|
|
||||||
observer.onError(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCompleted() {
|
|
||||||
observer.onCompleted();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancel() {
|
|
||||||
ClientCallStreamObserver<StreamEventsRequest> stream = requestStream.get();
|
|
||||||
if (stream != null) {
|
|
||||||
stream.cancel("client cancelled event stream", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
public class MxGatewayException extends RuntimeException {
|
|
||||||
public MxGatewayException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxGatewayException(String message, Throwable cause) {
|
|
||||||
super(message, cause);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-33
@@ -1,33 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
public final class MxGatewaySecrets {
|
|
||||||
private MxGatewaySecrets() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String redactApiKey(String apiKey) {
|
|
||||||
if (apiKey == null || apiKey.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (apiKey.length() <= 8) {
|
|
||||||
return "<redacted>";
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiKey.substring(0, 4)
|
|
||||||
+ "*".repeat(apiKey.length() - 8)
|
|
||||||
+ apiKey.substring(apiKey.length() - 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String redactCredentials(String value) {
|
|
||||||
if (value == null || value.isBlank()) {
|
|
||||||
return value == null ? "" : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] parts = value.split("\\s+");
|
|
||||||
for (int index = 0; index < parts.length; index++) {
|
|
||||||
if (parts[index].startsWith("mxgw_") || parts[index].equalsIgnoreCase("bearer")) {
|
|
||||||
parts[index] = "<redacted>";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return String.join(" ", parts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-184
@@ -1,184 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.HexFormat;
|
|
||||||
import java.util.Objects;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.AddItem2Command;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.AddItemCommand;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.AdviseCommand;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommand;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.RegisterCommand;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.UnregisterCommand;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.Write2Command;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.WriteCommand;
|
|
||||||
|
|
||||||
public final class MxGatewaySession implements AutoCloseable {
|
|
||||||
private static final SecureRandom RANDOM = new SecureRandom();
|
|
||||||
|
|
||||||
private final MxGatewayClient client;
|
|
||||||
private final OpenSessionReply openReply;
|
|
||||||
private CloseSessionReply closeReply;
|
|
||||||
|
|
||||||
MxGatewaySession(MxGatewayClient client, OpenSessionReply openReply) {
|
|
||||||
this.client = Objects.requireNonNull(client, "client");
|
|
||||||
this.openReply = Objects.requireNonNull(openReply, "openReply");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxGatewaySession forSessionId(MxGatewayClient client, String sessionId) {
|
|
||||||
return new MxGatewaySession(
|
|
||||||
client, OpenSessionReply.newBuilder().setSessionId(sessionId).build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String sessionId() {
|
|
||||||
return openReply.getSessionId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public OpenSessionReply openReply() {
|
|
||||||
return openReply;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized CloseSessionReply closeRaw() {
|
|
||||||
if (closeReply == null) {
|
|
||||||
closeReply = client.closeSessionRaw(CloseSessionRequest.newBuilder()
|
|
||||||
.setSessionId(sessionId())
|
|
||||||
.setClientCorrelationId(newCorrelationId())
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
return closeReply;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
closeRaw();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int register(String clientName) {
|
|
||||||
MxCommandReply reply = registerRaw(clientName);
|
|
||||||
if (reply.hasRegister()) {
|
|
||||||
return reply.getRegister().getServerHandle();
|
|
||||||
}
|
|
||||||
return reply.getReturnValue().getInt32Value();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxCommandReply registerRaw(String clientName) {
|
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_REGISTER)
|
|
||||||
.setRegister(RegisterCommand.newBuilder().setClientName(clientName))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregister(int serverHandle) {
|
|
||||||
invokeCommand(MxCommand.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_UNREGISTER)
|
|
||||||
.setUnregister(UnregisterCommand.newBuilder().setServerHandle(serverHandle))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public int addItem(int serverHandle, String itemDefinition) {
|
|
||||||
MxCommandReply reply = addItemRaw(serverHandle, itemDefinition);
|
|
||||||
if (reply.hasAddItem()) {
|
|
||||||
return reply.getAddItem().getItemHandle();
|
|
||||||
}
|
|
||||||
return reply.getReturnValue().getInt32Value();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxCommandReply addItemRaw(int serverHandle, String itemDefinition) {
|
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM)
|
|
||||||
.setAddItem(AddItemCommand.newBuilder()
|
|
||||||
.setServerHandle(serverHandle)
|
|
||||||
.setItemDefinition(itemDefinition))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public int addItem2(int serverHandle, String itemDefinition, String itemContext) {
|
|
||||||
MxCommandReply reply = addItem2Raw(serverHandle, itemDefinition, itemContext);
|
|
||||||
if (reply.hasAddItem2()) {
|
|
||||||
return reply.getAddItem2().getItemHandle();
|
|
||||||
}
|
|
||||||
return reply.getReturnValue().getInt32Value();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxCommandReply addItem2Raw(int serverHandle, String itemDefinition, String itemContext) {
|
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADD_ITEM2)
|
|
||||||
.setAddItem2(AddItem2Command.newBuilder()
|
|
||||||
.setServerHandle(serverHandle)
|
|
||||||
.setItemDefinition(itemDefinition)
|
|
||||||
.setItemContext(itemContext))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void advise(int serverHandle, int itemHandle) {
|
|
||||||
adviseRaw(serverHandle, itemHandle);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxCommandReply adviseRaw(int serverHandle, int itemHandle) {
|
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_ADVISE)
|
|
||||||
.setAdvise(AdviseCommand.newBuilder()
|
|
||||||
.setServerHandle(serverHandle)
|
|
||||||
.setItemHandle(itemHandle))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write(int serverHandle, int itemHandle, MxValue value, int userId) {
|
|
||||||
writeRaw(serverHandle, itemHandle, value, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxCommandReply writeRaw(int serverHandle, int itemHandle, MxValue value, int userId) {
|
|
||||||
return invokeCommand(MxCommand.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
|
|
||||||
.setWrite(WriteCommand.newBuilder()
|
|
||||||
.setServerHandle(serverHandle)
|
|
||||||
.setItemHandle(itemHandle)
|
|
||||||
.setValue(value)
|
|
||||||
.setUserId(userId))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void write2(int serverHandle, int itemHandle, MxValue value, MxValue timestampValue, int userId) {
|
|
||||||
invokeCommand(MxCommand.newBuilder()
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE2)
|
|
||||||
.setWrite2(Write2Command.newBuilder()
|
|
||||||
.setServerHandle(serverHandle)
|
|
||||||
.setItemHandle(itemHandle)
|
|
||||||
.setValue(value)
|
|
||||||
.setTimestampValue(timestampValue)
|
|
||||||
.setUserId(userId))
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxEventStream streamEvents() {
|
|
||||||
return streamEventsAfter(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxEventStream streamEventsAfter(long afterWorkerSequence) {
|
|
||||||
return client.streamEvents(StreamEventsRequest.newBuilder()
|
|
||||||
.setSessionId(sessionId())
|
|
||||||
.setAfterWorkerSequence(afterWorkerSequence)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxCommandReply invokeCommand(MxCommand command) {
|
|
||||||
return client.invoke(MxCommandRequest.newBuilder()
|
|
||||||
.setSessionId(sessionId())
|
|
||||||
.setClientCorrelationId(newCorrelationId())
|
|
||||||
.setCommand(command)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String newCorrelationId() {
|
|
||||||
byte[] bytes = new byte[16];
|
|
||||||
RANDOM.nextBytes(bytes);
|
|
||||||
return HexFormat.of().formatHex(bytes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
|
||||||
|
|
||||||
public final class MxGatewaySessionException extends MxGatewayException {
|
|
||||||
private final ProtocolStatus protocolStatus;
|
|
||||||
|
|
||||||
public MxGatewaySessionException(String operation, ProtocolStatus protocolStatus) {
|
|
||||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
|
||||||
this.protocolStatus = protocolStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProtocolStatus protocolStatus() {
|
|
||||||
return protocolStatus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
|
||||||
|
|
||||||
public final class MxGatewayWorkerException extends MxGatewayException {
|
|
||||||
private final ProtocolStatus protocolStatus;
|
|
||||||
|
|
||||||
public MxGatewayWorkerException(String operation, ProtocolStatus protocolStatus) {
|
|
||||||
super(MxGatewayErrors.protocolStatusMessage(operation, protocolStatus));
|
|
||||||
this.protocolStatus = protocolStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProtocolStatus protocolStatus() {
|
|
||||||
return protocolStatus;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusSource;
|
|
||||||
|
|
||||||
public final class MxStatuses {
|
|
||||||
private MxStatuses() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean succeeded(MxStatusProxy status) {
|
|
||||||
return status == null || status.getSuccess() != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxStatusView view(MxStatusProxy status) {
|
|
||||||
return new MxStatusView(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public record MxStatusView(MxStatusProxy raw) {
|
|
||||||
public int success() {
|
|
||||||
return raw.getSuccess();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxStatusCategory category() {
|
|
||||||
return raw.getCategory();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MxStatusSource detectedBy() {
|
|
||||||
return raw.getDetectedBy();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int detail() {
|
|
||||||
return raw.getDetail();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int rawCategory() {
|
|
||||||
return raw.getRawCategory();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int rawDetectedBy() {
|
|
||||||
return raw.getRawDetectedBy();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String diagnosticText() {
|
|
||||||
return raw.getDiagnosticText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-170
@@ -1,170 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import com.google.protobuf.ByteString;
|
|
||||||
import com.google.protobuf.Timestamp;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.BoolArray;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.DoubleArray;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.FloatArray;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.Int32Array;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.Int64Array;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxArray;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxDataType;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.RawArray;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StringArray;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.TimestampArray;
|
|
||||||
|
|
||||||
public final class MxValues {
|
|
||||||
private MxValues() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue boolValue(boolean value) {
|
|
||||||
return MxValue.newBuilder()
|
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_BOOLEAN)
|
|
||||||
.setVariantType("VT_BOOL")
|
|
||||||
.setBoolValue(value)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue int32Value(int value) {
|
|
||||||
return MxValue.newBuilder()
|
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
|
||||||
.setVariantType("VT_I4")
|
|
||||||
.setInt32Value(value)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue int64Value(long value) {
|
|
||||||
return MxValue.newBuilder()
|
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
|
||||||
.setVariantType("VT_I8")
|
|
||||||
.setInt64Value(value)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue floatValue(float value) {
|
|
||||||
return MxValue.newBuilder()
|
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_FLOAT)
|
|
||||||
.setVariantType("VT_R4")
|
|
||||||
.setFloatValue(value)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue doubleValue(double value) {
|
|
||||||
return MxValue.newBuilder()
|
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_DOUBLE)
|
|
||||||
.setVariantType("VT_R8")
|
|
||||||
.setDoubleValue(value)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue stringValue(String value) {
|
|
||||||
return MxValue.newBuilder()
|
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_STRING)
|
|
||||||
.setVariantType("VT_BSTR")
|
|
||||||
.setStringValue(value)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxValue timestampValue(Instant value) {
|
|
||||||
return MxValue.newBuilder()
|
|
||||||
.setDataType(MxDataType.MX_DATA_TYPE_TIME)
|
|
||||||
.setVariantType("VT_DATE")
|
|
||||||
.setTimestampValue(Timestamp.newBuilder()
|
|
||||||
.setSeconds(value.getEpochSecond())
|
|
||||||
.setNanos(value.getNano())
|
|
||||||
.build())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Object nativeValue(MxValue value) {
|
|
||||||
if (value == null || value.getIsNull()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return switch (value.getKindCase()) {
|
|
||||||
case BOOL_VALUE -> value.getBoolValue();
|
|
||||||
case INT32_VALUE -> value.getInt32Value();
|
|
||||||
case INT64_VALUE -> value.getInt64Value();
|
|
||||||
case FLOAT_VALUE -> value.getFloatValue();
|
|
||||||
case DOUBLE_VALUE -> value.getDoubleValue();
|
|
||||||
case STRING_VALUE -> value.getStringValue();
|
|
||||||
case TIMESTAMP_VALUE -> instant(value.getTimestampValue());
|
|
||||||
case ARRAY_VALUE -> nativeArray(value.getArrayValue());
|
|
||||||
case RAW_VALUE -> value.getRawValue().toByteArray();
|
|
||||||
case KIND_NOT_SET -> null;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Object nativeArray(MxArray array) {
|
|
||||||
if (array == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return switch (array.getValuesCase()) {
|
|
||||||
case BOOL_VALUES -> List.copyOf(array.getBoolValues().getValuesList());
|
|
||||||
case INT32_VALUES -> List.copyOf(array.getInt32Values().getValuesList());
|
|
||||||
case INT64_VALUES -> List.copyOf(array.getInt64Values().getValuesList());
|
|
||||||
case FLOAT_VALUES -> List.copyOf(array.getFloatValues().getValuesList());
|
|
||||||
case DOUBLE_VALUES -> List.copyOf(array.getDoubleValues().getValuesList());
|
|
||||||
case STRING_VALUES -> List.copyOf(array.getStringValues().getValuesList());
|
|
||||||
case TIMESTAMP_VALUES -> timestampValues(array.getTimestampValues());
|
|
||||||
case RAW_VALUES -> rawValues(array.getRawValues());
|
|
||||||
case VALUES_NOT_SET -> List.of();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxArray stringArray(List<String> values) {
|
|
||||||
return MxArray.newBuilder()
|
|
||||||
.setElementDataType(MxDataType.MX_DATA_TYPE_STRING)
|
|
||||||
.setVariantType("VT_ARRAY|VT_BSTR")
|
|
||||||
.addDimensions(values.size())
|
|
||||||
.setStringValues(StringArray.newBuilder().addAllValues(values))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static MxArray int32Array(List<Integer> values) {
|
|
||||||
return MxArray.newBuilder()
|
|
||||||
.setElementDataType(MxDataType.MX_DATA_TYPE_INTEGER)
|
|
||||||
.setVariantType("VT_ARRAY|VT_I4")
|
|
||||||
.addDimensions(values.size())
|
|
||||||
.setInt32Values(Int32Array.newBuilder().addAllValues(values))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String kindName(MxValue value) {
|
|
||||||
return value == null ? "KIND_NOT_SET" : value.getKindCase().name();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Instant instant(Timestamp timestamp) {
|
|
||||||
return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Instant> timestampValues(TimestampArray array) {
|
|
||||||
List<Instant> values = new ArrayList<>();
|
|
||||||
for (Timestamp timestamp : array.getValuesList()) {
|
|
||||||
values.add(instant(timestamp));
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<byte[]> rawValues(RawArray array) {
|
|
||||||
List<byte[]> values = new ArrayList<>();
|
|
||||||
for (ByteString rawValue : array.getValuesList()) {
|
|
||||||
values.add(rawValue.toByteArray());
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static void generatedTypeReferences(
|
|
||||||
BoolArray boolArray,
|
|
||||||
Int64Array int64Array,
|
|
||||||
FloatArray floatArray,
|
|
||||||
DoubleArray doubleArray) {
|
|
||||||
// Keeps generated repeated-value imports visible for javadocs and IDE navigation.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-29
@@ -1,29 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
|
|
||||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
|
||||||
import mxaccess_worker.v1.MxaccessWorker.WorkerEnvelope;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
final class GeneratedContractSmokeTests {
|
|
||||||
@Test
|
|
||||||
void generatedGatewayAndWorkerContractsCompile() {
|
|
||||||
OpenSessionRequest request = OpenSessionRequest.newBuilder()
|
|
||||||
.setClientSessionName("junit")
|
|
||||||
.build();
|
|
||||||
WorkerEnvelope envelope = WorkerEnvelope.newBuilder()
|
|
||||||
.setProtocolVersion(MxGatewayClientVersion.workerProtocolVersion())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
assertEquals("junit", request.getClientSessionName());
|
|
||||||
assertEquals("mxaccess_gateway.v1.MxAccessGateway", MxAccessGatewayGrpc.SERVICE_NAME);
|
|
||||||
assertEquals(1, envelope.getProtocolVersion());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void javaTwentyOneToolchainRunsTests() {
|
|
||||||
assertEquals(21, Runtime.version().feature());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-243
@@ -1,243 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import io.grpc.Context;
|
|
||||||
import io.grpc.ManagedChannel;
|
|
||||||
import io.grpc.Metadata;
|
|
||||||
import io.grpc.Server;
|
|
||||||
import io.grpc.ServerCall;
|
|
||||||
import io.grpc.ServerCallHandler;
|
|
||||||
import io.grpc.ServerInterceptor;
|
|
||||||
import io.grpc.inprocess.InProcessChannelBuilder;
|
|
||||||
import io.grpc.inprocess.InProcessServerBuilder;
|
|
||||||
import io.grpc.stub.ServerCallStreamObserver;
|
|
||||||
import io.grpc.stub.StreamObserver;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
import mxaccess_gateway.v1.MxAccessGatewayGrpc;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.AddItemReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandKind;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxEvent;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatus;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.RegisterReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.SessionState;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
final class MxGatewayClientSessionTests {
|
|
||||||
@Test
|
|
||||||
void unaryCallsCarryAuthMetadataAndDeadline() throws Exception {
|
|
||||||
AtomicReference<String> authorization = new AtomicReference<>();
|
|
||||||
AtomicReference<MxCommandRequest> commandRequest = new AtomicReference<>();
|
|
||||||
AtomicReference<Boolean> deadlineSeen = new AtomicReference<>(false);
|
|
||||||
|
|
||||||
TestGatewayService service = new TestGatewayService() {
|
|
||||||
@Override
|
|
||||||
public void openSession(OpenSessionRequest request, StreamObserver<OpenSessionReply> responseObserver) {
|
|
||||||
deadlineSeen.set(Context.current().getDeadline() != null);
|
|
||||||
responseObserver.onNext(OpenSessionReply.newBuilder()
|
|
||||||
.setSessionId("session-java")
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.build());
|
|
||||||
responseObserver.onCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
|
||||||
commandRequest.set(request);
|
|
||||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
|
||||||
.setSessionId(request.getSessionId())
|
|
||||||
.setKind(request.getCommand().getKind())
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.setRegister(RegisterReply.newBuilder().setServerHandle(42))
|
|
||||||
.build());
|
|
||||||
responseObserver.onCompleted();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try (InProcessGateway gateway = InProcessGateway.start(service, authorization);
|
|
||||||
MxGatewayClient client = gateway.client("mxgw_visible_secret", Duration.ofSeconds(5))) {
|
|
||||||
MxGatewaySession session = client.openSession("junit-session");
|
|
||||||
|
|
||||||
int serverHandle = session.register("java-test-client");
|
|
||||||
|
|
||||||
assertEquals(42, serverHandle);
|
|
||||||
assertEquals("Bearer mxgw_visible_secret", authorization.get());
|
|
||||||
assertEquals("session-java", commandRequest.get().getSessionId());
|
|
||||||
assertEquals(MxCommandKind.MX_COMMAND_KIND_REGISTER, commandRequest.get().getCommand().getKind());
|
|
||||||
assertTrue(deadlineSeen.get());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void methodHelpersReturnTypedHandlesAndRawReplies() throws Exception {
|
|
||||||
TestGatewayService service = new TestGatewayService() {
|
|
||||||
@Override
|
|
||||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
|
||||||
MxCommandReply.Builder reply = MxCommandReply.newBuilder()
|
|
||||||
.setSessionId(request.getSessionId())
|
|
||||||
.setKind(request.getCommand().getKind())
|
|
||||||
.setProtocolStatus(ok());
|
|
||||||
if (request.getCommand().getKind() == MxCommandKind.MX_COMMAND_KIND_ADD_ITEM) {
|
|
||||||
reply.setAddItem(AddItemReply.newBuilder().setItemHandle(7));
|
|
||||||
}
|
|
||||||
responseObserver.onNext(reply.build());
|
|
||||||
responseObserver.onCompleted();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
|
||||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
|
||||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "existing-session");
|
|
||||||
|
|
||||||
int itemHandle = session.addItem(12, "TestObject.TestInt");
|
|
||||||
MxCommandReply raw = session.adviseRaw(12, itemHandle);
|
|
||||||
|
|
||||||
assertEquals(7, itemHandle);
|
|
||||||
assertEquals(MxCommandKind.MX_COMMAND_KIND_ADVISE, raw.getKind());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void streamCancellationCancelsServerCall() throws Exception {
|
|
||||||
CountDownLatch cancelled = new CountDownLatch(1);
|
|
||||||
TestGatewayService service = new TestGatewayService() {
|
|
||||||
@Override
|
|
||||||
public void streamEvents(StreamEventsRequest request, StreamObserver<MxEvent> responseObserver) {
|
|
||||||
ServerCallStreamObserver<MxEvent> serverObserver =
|
|
||||||
(ServerCallStreamObserver<MxEvent>) responseObserver;
|
|
||||||
serverObserver.setOnCancelHandler(cancelled::countDown);
|
|
||||||
responseObserver.onNext(MxEvent.newBuilder()
|
|
||||||
.setSessionId(request.getSessionId())
|
|
||||||
.setWorkerSequence(1)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
|
||||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
|
||||||
MxEventStream events = MxGatewaySession.forSessionId(client, "stream-session").streamEvents();
|
|
||||||
|
|
||||||
assertTrue(events.hasNext());
|
|
||||||
assertEquals(1, events.next().getWorkerSequence());
|
|
||||||
events.close();
|
|
||||||
|
|
||||||
assertTrue(cancelled.await(5, TimeUnit.SECONDS));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void commandFailureKeepsRawReply() throws Exception {
|
|
||||||
TestGatewayService service = new TestGatewayService() {
|
|
||||||
@Override
|
|
||||||
public void invoke(MxCommandRequest request, StreamObserver<MxCommandReply> responseObserver) {
|
|
||||||
responseObserver.onNext(MxCommandReply.newBuilder()
|
|
||||||
.setSessionId(request.getSessionId())
|
|
||||||
.setKind(MxCommandKind.MX_COMMAND_KIND_WRITE)
|
|
||||||
.setProtocolStatus(ProtocolStatus.newBuilder()
|
|
||||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE)
|
|
||||||
.setMessage("MXAccess rejected the write."))
|
|
||||||
.setHresult(-2147220992)
|
|
||||||
.build());
|
|
||||||
responseObserver.onCompleted();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
try (InProcessGateway gateway = InProcessGateway.start(service, new AtomicReference<>());
|
|
||||||
MxGatewayClient client = gateway.client("", Duration.ofSeconds(5))) {
|
|
||||||
MxGatewaySession session = MxGatewaySession.forSessionId(client, "failure-session");
|
|
||||||
|
|
||||||
MxAccessException error = assertThrows(
|
|
||||||
MxAccessException.class,
|
|
||||||
() -> session.write(1, 2, MxValues.int32Value(123), 0));
|
|
||||||
|
|
||||||
assertNotNull(error.reply());
|
|
||||||
assertEquals(-2147220992, error.reply().getHresult());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ProtocolStatus ok() {
|
|
||||||
return ProtocolStatus.newBuilder()
|
|
||||||
.setCode(ProtocolStatusCode.PROTOCOL_STATUS_CODE_OK)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private abstract static class TestGatewayService extends MxAccessGatewayGrpc.MxAccessGatewayImplBase {
|
|
||||||
@Override
|
|
||||||
public void openSession(OpenSessionRequest request, StreamObserver<OpenSessionReply> responseObserver) {
|
|
||||||
responseObserver.onNext(OpenSessionReply.newBuilder()
|
|
||||||
.setSessionId("session-java")
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.build());
|
|
||||||
responseObserver.onCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void closeSession(CloseSessionRequest request, StreamObserver<CloseSessionReply> responseObserver) {
|
|
||||||
responseObserver.onNext(CloseSessionReply.newBuilder()
|
|
||||||
.setSessionId(request.getSessionId())
|
|
||||||
.setFinalState(SessionState.SESSION_STATE_CLOSED)
|
|
||||||
.setProtocolStatus(ok())
|
|
||||||
.build());
|
|
||||||
responseObserver.onCompleted();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private record InProcessGateway(Server server, ManagedChannel channel) implements AutoCloseable {
|
|
||||||
static InProcessGateway start(
|
|
||||||
MxAccessGatewayGrpc.MxAccessGatewayImplBase service, AtomicReference<String> authorization)
|
|
||||||
throws Exception {
|
|
||||||
String serverName = "mxgw-java-" + UUID.randomUUID();
|
|
||||||
ServerInterceptor interceptor = new ServerInterceptor() {
|
|
||||||
@Override
|
|
||||||
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
|
|
||||||
ServerCall<ReqT, RespT> call,
|
|
||||||
Metadata headers,
|
|
||||||
ServerCallHandler<ReqT, RespT> next) {
|
|
||||||
authorization.set(headers.get(MxGatewayAuthInterceptor.AUTHORIZATION_HEADER));
|
|
||||||
return next.startCall(call, headers);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Server server = InProcessServerBuilder.forName(serverName)
|
|
||||||
.directExecutor()
|
|
||||||
.addService(io.grpc.ServerInterceptors.intercept(service, interceptor))
|
|
||||||
.build()
|
|
||||||
.start();
|
|
||||||
ManagedChannel channel = InProcessChannelBuilder.forName(serverName)
|
|
||||||
.directExecutor()
|
|
||||||
.build();
|
|
||||||
return new InProcessGateway(server, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
MxGatewayClient client(String apiKey, Duration callTimeout) {
|
|
||||||
return new MxGatewayClient(
|
|
||||||
channel,
|
|
||||||
MxGatewayClientOptions.builder()
|
|
||||||
.endpoint("in-process")
|
|
||||||
.apiKey(apiKey)
|
|
||||||
.plaintext(true)
|
|
||||||
.callTimeout(callTimeout)
|
|
||||||
.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() {
|
|
||||||
channel.shutdownNow();
|
|
||||||
server.shutdownNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-136
@@ -1,136 +0,0 @@
|
|||||||
package com.dohertylan.mxgateway.client;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
import com.google.protobuf.util.JsonFormat;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.List;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxCommandReply;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusCategory;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxStatusProxy;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.MxValue;
|
|
||||||
import mxaccess_gateway.v1.MxaccessGateway.ProtocolStatusCode;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
|
|
||||||
final class MxGatewayFixtureTests {
|
|
||||||
@Test
|
|
||||||
void valueFixtureCasesExposeNativeProjectionAndRawMetadata() throws Exception {
|
|
||||||
JsonArray cases = readFixture("values/value-conversion-cases.json").getAsJsonArray("cases");
|
|
||||||
|
|
||||||
for (var element : cases) {
|
|
||||||
JsonObject testCase = element.getAsJsonObject();
|
|
||||||
MxValue.Builder builder = MxValue.newBuilder();
|
|
||||||
JsonFormat.parser().merge(testCase.getAsJsonObject("value").toString(), builder);
|
|
||||||
MxValue value = builder.build();
|
|
||||||
|
|
||||||
assertEquals(testCase.get("expectedKind").getAsString(), lowerCamelKind(value));
|
|
||||||
if ("timestamp.utc".equals(testCase.get("id").getAsString())) {
|
|
||||||
assertEquals(Instant.parse("2026-01-01T00:00:04Z"), MxValues.nativeValue(value));
|
|
||||||
}
|
|
||||||
if ("string-array".equals(testCase.get("id").getAsString())) {
|
|
||||||
assertEquals(List.of("alpha", "beta"), MxValues.nativeValue(value));
|
|
||||||
}
|
|
||||||
if ("raw-fallback.variant".equals(testCase.get("id").getAsString())) {
|
|
||||||
assertEquals("No lossless typed projection exists for this VARIANT.", value.getRawDiagnostic());
|
|
||||||
assertArrayEquals(new byte[] {1, 2, 3, 4, 5}, (byte[]) MxValues.nativeValue(value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void statusFixtureCasesPreserveRawFields() throws Exception {
|
|
||||||
JsonArray cases = readFixture("statuses/status-conversion-cases.json").getAsJsonArray("cases");
|
|
||||||
|
|
||||||
for (var element : cases) {
|
|
||||||
JsonObject testCase = element.getAsJsonObject();
|
|
||||||
MxStatusProxy.Builder builder = MxStatusProxy.newBuilder();
|
|
||||||
JsonFormat.parser().merge(testCase.getAsJsonObject("status").toString(), builder);
|
|
||||||
MxStatusProxy status = builder.build();
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
testCase.getAsJsonObject("status").get("rawCategory").getAsInt(),
|
|
||||||
status.getRawCategory());
|
|
||||||
if ("ok.responding-lmx".equals(testCase.get("id").getAsString())) {
|
|
||||||
assertTrue(MxStatuses.succeeded(status));
|
|
||||||
}
|
|
||||||
if ("security-error.requesting-lmx".equals(testCase.get("id").getAsString())) {
|
|
||||||
assertFalse(MxStatuses.succeeded(status));
|
|
||||||
assertEquals(MxStatusCategory.MX_STATUS_CATEGORY_SECURITY_ERROR, MxStatuses.view(status).category());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void mxAccessFailureFixtureMapsToRichCommandException() throws Exception {
|
|
||||||
MxCommandReply.Builder builder = MxCommandReply.newBuilder();
|
|
||||||
JsonFormat.parser().merge(
|
|
||||||
Files.readString(fixtureRoot().resolve("command-replies/write.mxaccess-failure.reply.json")),
|
|
||||||
builder);
|
|
||||||
MxCommandReply reply = builder.build();
|
|
||||||
|
|
||||||
try {
|
|
||||||
MxGatewayErrors.ensureProtocolSuccess("invoke", reply.getProtocolStatus(), reply);
|
|
||||||
} catch (MxAccessException error) {
|
|
||||||
assertEquals(ProtocolStatusCode.PROTOCOL_STATUS_CODE_MXACCESS_FAILURE, error.protocolStatus().getCode());
|
|
||||||
assertEquals(-2147220992, error.reply().getHresult());
|
|
||||||
assertEquals(2, error.reply().getStatusesCount());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new AssertionError("expected MxAccessException");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void grpcAuthErrorsAreClassifiedAndRedacted() {
|
|
||||||
RuntimeException authError = MxGatewayErrors.fromGrpc(
|
|
||||||
"open session",
|
|
||||||
new io.grpc.StatusRuntimeException(io.grpc.Status.UNAUTHENTICATED.withDescription(
|
|
||||||
"invalid API key mxgw_visible_secret")));
|
|
||||||
RuntimeException permissionError = MxGatewayErrors.fromGrpc(
|
|
||||||
"write",
|
|
||||||
new io.grpc.StatusRuntimeException(io.grpc.Status.PERMISSION_DENIED.withDescription(
|
|
||||||
"missing scope mxaccess.write")));
|
|
||||||
|
|
||||||
assertInstanceOf(MxGatewayAuthenticationException.class, authError);
|
|
||||||
assertInstanceOf(MxGatewayAuthorizationException.class, permissionError);
|
|
||||||
assertTrue(authError.getMessage().contains("<redacted>"));
|
|
||||||
assertFalse(authError.getMessage().contains("visible_secret"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static JsonObject readFixture(String relativePath) throws Exception {
|
|
||||||
return JsonParser.parseString(Files.readString(fixtureRoot().resolve(relativePath))).getAsJsonObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path fixtureRoot() {
|
|
||||||
Path current = Path.of(System.getProperty("user.dir")).toAbsolutePath();
|
|
||||||
for (Path path = current; path != null; path = path.getParent()) {
|
|
||||||
Path candidate = path.resolve("clients/proto/fixtures/behavior");
|
|
||||||
if (Files.exists(candidate)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
candidate = path.resolve("../proto/fixtures/behavior").normalize();
|
|
||||||
if (Files.exists(candidate)) {
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new IllegalStateException("could not locate behavior fixtures from " + current);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String lowerCamelKind(MxValue value) {
|
|
||||||
String[] parts = value.getKindCase().name().toLowerCase().split("_");
|
|
||||||
StringBuilder result = new StringBuilder(parts[0]);
|
|
||||||
for (int index = 1; index < parts.length; index++) {
|
|
||||||
result.append(Character.toUpperCase(parts[index].charAt(0))).append(parts[index].substring(1));
|
|
||||||
}
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
gradlePluginPortal()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
id 'com.google.protobuf' version '0.9.5'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = 'mxaccessgw-java'
|
|
||||||
|
|
||||||
include 'mxgateway-client'
|
|
||||||
include 'mxgateway-cli'
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
-588
@@ -1,588 +0,0 @@
|
|||||||
package mxaccess_gateway.v1;
|
|
||||||
|
|
||||||
import static io.grpc.MethodDescriptor.generateFullMethodName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <pre>
|
|
||||||
* Public client API for MXAccess sessions hosted by the gateway.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@io.grpc.stub.annotations.GrpcGenerated
|
|
||||||
public final class MxAccessGatewayGrpc {
|
|
||||||
|
|
||||||
private MxAccessGatewayGrpc() {}
|
|
||||||
|
|
||||||
public static final java.lang.String SERVICE_NAME = "mxaccess_gateway.v1.MxAccessGateway";
|
|
||||||
|
|
||||||
// Static method descriptors that strictly reflect the proto.
|
|
||||||
private static volatile io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply> getOpenSessionMethod;
|
|
||||||
|
|
||||||
@io.grpc.stub.annotations.RpcMethod(
|
|
||||||
fullMethodName = SERVICE_NAME + '/' + "OpenSession",
|
|
||||||
requestType = mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest.class,
|
|
||||||
responseType = mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply.class,
|
|
||||||
methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
|
|
||||||
public static io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply> getOpenSessionMethod() {
|
|
||||||
io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest, mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply> getOpenSessionMethod;
|
|
||||||
if ((getOpenSessionMethod = MxAccessGatewayGrpc.getOpenSessionMethod) == null) {
|
|
||||||
synchronized (MxAccessGatewayGrpc.class) {
|
|
||||||
if ((getOpenSessionMethod = MxAccessGatewayGrpc.getOpenSessionMethod) == null) {
|
|
||||||
MxAccessGatewayGrpc.getOpenSessionMethod = getOpenSessionMethod =
|
|
||||||
io.grpc.MethodDescriptor.<mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest, mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply>newBuilder()
|
|
||||||
.setType(io.grpc.MethodDescriptor.MethodType.UNARY)
|
|
||||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "OpenSession"))
|
|
||||||
.setSampledToLocalTracing(true)
|
|
||||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest.getDefaultInstance()))
|
|
||||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply.getDefaultInstance()))
|
|
||||||
.setSchemaDescriptor(new MxAccessGatewayMethodDescriptorSupplier("OpenSession"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getOpenSessionMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static volatile io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply> getCloseSessionMethod;
|
|
||||||
|
|
||||||
@io.grpc.stub.annotations.RpcMethod(
|
|
||||||
fullMethodName = SERVICE_NAME + '/' + "CloseSession",
|
|
||||||
requestType = mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest.class,
|
|
||||||
responseType = mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply.class,
|
|
||||||
methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
|
|
||||||
public static io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply> getCloseSessionMethod() {
|
|
||||||
io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest, mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply> getCloseSessionMethod;
|
|
||||||
if ((getCloseSessionMethod = MxAccessGatewayGrpc.getCloseSessionMethod) == null) {
|
|
||||||
synchronized (MxAccessGatewayGrpc.class) {
|
|
||||||
if ((getCloseSessionMethod = MxAccessGatewayGrpc.getCloseSessionMethod) == null) {
|
|
||||||
MxAccessGatewayGrpc.getCloseSessionMethod = getCloseSessionMethod =
|
|
||||||
io.grpc.MethodDescriptor.<mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest, mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply>newBuilder()
|
|
||||||
.setType(io.grpc.MethodDescriptor.MethodType.UNARY)
|
|
||||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "CloseSession"))
|
|
||||||
.setSampledToLocalTracing(true)
|
|
||||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest.getDefaultInstance()))
|
|
||||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply.getDefaultInstance()))
|
|
||||||
.setSchemaDescriptor(new MxAccessGatewayMethodDescriptorSupplier("CloseSession"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getCloseSessionMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static volatile io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxCommandReply> getInvokeMethod;
|
|
||||||
|
|
||||||
@io.grpc.stub.annotations.RpcMethod(
|
|
||||||
fullMethodName = SERVICE_NAME + '/' + "Invoke",
|
|
||||||
requestType = mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest.class,
|
|
||||||
responseType = mxaccess_gateway.v1.MxaccessGateway.MxCommandReply.class,
|
|
||||||
methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
|
|
||||||
public static io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxCommandReply> getInvokeMethod() {
|
|
||||||
io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest, mxaccess_gateway.v1.MxaccessGateway.MxCommandReply> getInvokeMethod;
|
|
||||||
if ((getInvokeMethod = MxAccessGatewayGrpc.getInvokeMethod) == null) {
|
|
||||||
synchronized (MxAccessGatewayGrpc.class) {
|
|
||||||
if ((getInvokeMethod = MxAccessGatewayGrpc.getInvokeMethod) == null) {
|
|
||||||
MxAccessGatewayGrpc.getInvokeMethod = getInvokeMethod =
|
|
||||||
io.grpc.MethodDescriptor.<mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest, mxaccess_gateway.v1.MxaccessGateway.MxCommandReply>newBuilder()
|
|
||||||
.setType(io.grpc.MethodDescriptor.MethodType.UNARY)
|
|
||||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "Invoke"))
|
|
||||||
.setSampledToLocalTracing(true)
|
|
||||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest.getDefaultInstance()))
|
|
||||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxCommandReply.getDefaultInstance()))
|
|
||||||
.setSchemaDescriptor(new MxAccessGatewayMethodDescriptorSupplier("Invoke"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getInvokeMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static volatile io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxEvent> getStreamEventsMethod;
|
|
||||||
|
|
||||||
@io.grpc.stub.annotations.RpcMethod(
|
|
||||||
fullMethodName = SERVICE_NAME + '/' + "StreamEvents",
|
|
||||||
requestType = mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest.class,
|
|
||||||
responseType = mxaccess_gateway.v1.MxaccessGateway.MxEvent.class,
|
|
||||||
methodType = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
|
||||||
public static io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxEvent> getStreamEventsMethod() {
|
|
||||||
io.grpc.MethodDescriptor<mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest, mxaccess_gateway.v1.MxaccessGateway.MxEvent> getStreamEventsMethod;
|
|
||||||
if ((getStreamEventsMethod = MxAccessGatewayGrpc.getStreamEventsMethod) == null) {
|
|
||||||
synchronized (MxAccessGatewayGrpc.class) {
|
|
||||||
if ((getStreamEventsMethod = MxAccessGatewayGrpc.getStreamEventsMethod) == null) {
|
|
||||||
MxAccessGatewayGrpc.getStreamEventsMethod = getStreamEventsMethod =
|
|
||||||
io.grpc.MethodDescriptor.<mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest, mxaccess_gateway.v1.MxaccessGateway.MxEvent>newBuilder()
|
|
||||||
.setType(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
|
||||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "StreamEvents"))
|
|
||||||
.setSampledToLocalTracing(true)
|
|
||||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest.getDefaultInstance()))
|
|
||||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxEvent.getDefaultInstance()))
|
|
||||||
.setSchemaDescriptor(new MxAccessGatewayMethodDescriptorSupplier("StreamEvents"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getStreamEventsMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new async stub that supports all call types for the service
|
|
||||||
*/
|
|
||||||
public static MxAccessGatewayStub newStub(io.grpc.Channel channel) {
|
|
||||||
io.grpc.stub.AbstractStub.StubFactory<MxAccessGatewayStub> factory =
|
|
||||||
new io.grpc.stub.AbstractStub.StubFactory<MxAccessGatewayStub>() {
|
|
||||||
@java.lang.Override
|
|
||||||
public MxAccessGatewayStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new MxAccessGatewayStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return MxAccessGatewayStub.newStub(factory, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new blocking-style stub that supports all types of calls on the service
|
|
||||||
*/
|
|
||||||
public static MxAccessGatewayBlockingV2Stub newBlockingV2Stub(
|
|
||||||
io.grpc.Channel channel) {
|
|
||||||
io.grpc.stub.AbstractStub.StubFactory<MxAccessGatewayBlockingV2Stub> factory =
|
|
||||||
new io.grpc.stub.AbstractStub.StubFactory<MxAccessGatewayBlockingV2Stub>() {
|
|
||||||
@java.lang.Override
|
|
||||||
public MxAccessGatewayBlockingV2Stub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new MxAccessGatewayBlockingV2Stub(channel, callOptions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return MxAccessGatewayBlockingV2Stub.newStub(factory, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new blocking-style stub that supports unary and streaming output calls on the service
|
|
||||||
*/
|
|
||||||
public static MxAccessGatewayBlockingStub newBlockingStub(
|
|
||||||
io.grpc.Channel channel) {
|
|
||||||
io.grpc.stub.AbstractStub.StubFactory<MxAccessGatewayBlockingStub> factory =
|
|
||||||
new io.grpc.stub.AbstractStub.StubFactory<MxAccessGatewayBlockingStub>() {
|
|
||||||
@java.lang.Override
|
|
||||||
public MxAccessGatewayBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new MxAccessGatewayBlockingStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return MxAccessGatewayBlockingStub.newStub(factory, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new ListenableFuture-style stub that supports unary calls on the service
|
|
||||||
*/
|
|
||||||
public static MxAccessGatewayFutureStub newFutureStub(
|
|
||||||
io.grpc.Channel channel) {
|
|
||||||
io.grpc.stub.AbstractStub.StubFactory<MxAccessGatewayFutureStub> factory =
|
|
||||||
new io.grpc.stub.AbstractStub.StubFactory<MxAccessGatewayFutureStub>() {
|
|
||||||
@java.lang.Override
|
|
||||||
public MxAccessGatewayFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new MxAccessGatewayFutureStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return MxAccessGatewayFutureStub.newStub(factory, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <pre>
|
|
||||||
* Public client API for MXAccess sessions hosted by the gateway.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public interface AsyncService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
default void openSession(mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest request,
|
|
||||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply> responseObserver) {
|
|
||||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getOpenSessionMethod(), responseObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
default void closeSession(mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest request,
|
|
||||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply> responseObserver) {
|
|
||||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getCloseSessionMethod(), responseObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
default void invoke(mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest request,
|
|
||||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.MxCommandReply> responseObserver) {
|
|
||||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getInvokeMethod(), responseObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
default void streamEvents(mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest request,
|
|
||||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.MxEvent> responseObserver) {
|
|
||||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getStreamEventsMethod(), responseObserver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for the server implementation of the service MxAccessGateway.
|
|
||||||
* <pre>
|
|
||||||
* Public client API for MXAccess sessions hosted by the gateway.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static abstract class MxAccessGatewayImplBase
|
|
||||||
implements io.grpc.BindableService, AsyncService {
|
|
||||||
|
|
||||||
@java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
|
|
||||||
return MxAccessGatewayGrpc.bindService(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A stub to allow clients to do asynchronous rpc calls to service MxAccessGateway.
|
|
||||||
* <pre>
|
|
||||||
* Public client API for MXAccess sessions hosted by the gateway.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static final class MxAccessGatewayStub
|
|
||||||
extends io.grpc.stub.AbstractAsyncStub<MxAccessGatewayStub> {
|
|
||||||
private MxAccessGatewayStub(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
super(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
protected MxAccessGatewayStub build(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new MxAccessGatewayStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public void openSession(mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest request,
|
|
||||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply> responseObserver) {
|
|
||||||
io.grpc.stub.ClientCalls.asyncUnaryCall(
|
|
||||||
getChannel().newCall(getOpenSessionMethod(), getCallOptions()), request, responseObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public void closeSession(mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest request,
|
|
||||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply> responseObserver) {
|
|
||||||
io.grpc.stub.ClientCalls.asyncUnaryCall(
|
|
||||||
getChannel().newCall(getCloseSessionMethod(), getCallOptions()), request, responseObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public void invoke(mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest request,
|
|
||||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.MxCommandReply> responseObserver) {
|
|
||||||
io.grpc.stub.ClientCalls.asyncUnaryCall(
|
|
||||||
getChannel().newCall(getInvokeMethod(), getCallOptions()), request, responseObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public void streamEvents(mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest request,
|
|
||||||
io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.MxEvent> responseObserver) {
|
|
||||||
io.grpc.stub.ClientCalls.asyncServerStreamingCall(
|
|
||||||
getChannel().newCall(getStreamEventsMethod(), getCallOptions()), request, responseObserver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A stub to allow clients to do synchronous rpc calls to service MxAccessGateway.
|
|
||||||
* <pre>
|
|
||||||
* Public client API for MXAccess sessions hosted by the gateway.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static final class MxAccessGatewayBlockingV2Stub
|
|
||||||
extends io.grpc.stub.AbstractBlockingStub<MxAccessGatewayBlockingV2Stub> {
|
|
||||||
private MxAccessGatewayBlockingV2Stub(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
super(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
protected MxAccessGatewayBlockingV2Stub build(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new MxAccessGatewayBlockingV2Stub(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply openSession(mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest request) throws io.grpc.StatusException {
|
|
||||||
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
|
|
||||||
getChannel(), getOpenSessionMethod(), getCallOptions(), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply closeSession(mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest request) throws io.grpc.StatusException {
|
|
||||||
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
|
|
||||||
getChannel(), getCloseSessionMethod(), getCallOptions(), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public mxaccess_gateway.v1.MxaccessGateway.MxCommandReply invoke(mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest request) throws io.grpc.StatusException {
|
|
||||||
return io.grpc.stub.ClientCalls.blockingV2UnaryCall(
|
|
||||||
getChannel(), getInvokeMethod(), getCallOptions(), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
@io.grpc.ExperimentalApi("https://github.com/grpc/grpc-java/issues/10918")
|
|
||||||
public io.grpc.stub.BlockingClientCall<?, mxaccess_gateway.v1.MxaccessGateway.MxEvent>
|
|
||||||
streamEvents(mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest request) {
|
|
||||||
return io.grpc.stub.ClientCalls.blockingV2ServerStreamingCall(
|
|
||||||
getChannel(), getStreamEventsMethod(), getCallOptions(), request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A stub to allow clients to do limited synchronous rpc calls to service MxAccessGateway.
|
|
||||||
* <pre>
|
|
||||||
* Public client API for MXAccess sessions hosted by the gateway.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static final class MxAccessGatewayBlockingStub
|
|
||||||
extends io.grpc.stub.AbstractBlockingStub<MxAccessGatewayBlockingStub> {
|
|
||||||
private MxAccessGatewayBlockingStub(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
super(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
protected MxAccessGatewayBlockingStub build(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new MxAccessGatewayBlockingStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply openSession(mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest request) {
|
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
|
||||||
getChannel(), getOpenSessionMethod(), getCallOptions(), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply closeSession(mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest request) {
|
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
|
||||||
getChannel(), getCloseSessionMethod(), getCallOptions(), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public mxaccess_gateway.v1.MxaccessGateway.MxCommandReply invoke(mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest request) {
|
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
|
||||||
getChannel(), getInvokeMethod(), getCallOptions(), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public java.util.Iterator<mxaccess_gateway.v1.MxaccessGateway.MxEvent> streamEvents(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest request) {
|
|
||||||
return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
|
|
||||||
getChannel(), getStreamEventsMethod(), getCallOptions(), request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A stub to allow clients to do ListenableFuture-style rpc calls to service MxAccessGateway.
|
|
||||||
* <pre>
|
|
||||||
* Public client API for MXAccess sessions hosted by the gateway.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static final class MxAccessGatewayFutureStub
|
|
||||||
extends io.grpc.stub.AbstractFutureStub<MxAccessGatewayFutureStub> {
|
|
||||||
private MxAccessGatewayFutureStub(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
super(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
protected MxAccessGatewayFutureStub build(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new MxAccessGatewayFutureStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public com.google.common.util.concurrent.ListenableFuture<mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply> openSession(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest request) {
|
|
||||||
return io.grpc.stub.ClientCalls.futureUnaryCall(
|
|
||||||
getChannel().newCall(getOpenSessionMethod(), getCallOptions()), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public com.google.common.util.concurrent.ListenableFuture<mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply> closeSession(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest request) {
|
|
||||||
return io.grpc.stub.ClientCalls.futureUnaryCall(
|
|
||||||
getChannel().newCall(getCloseSessionMethod(), getCallOptions()), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public com.google.common.util.concurrent.ListenableFuture<mxaccess_gateway.v1.MxaccessGateway.MxCommandReply> invoke(
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest request) {
|
|
||||||
return io.grpc.stub.ClientCalls.futureUnaryCall(
|
|
||||||
getChannel().newCall(getInvokeMethod(), getCallOptions()), request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int METHODID_OPEN_SESSION = 0;
|
|
||||||
private static final int METHODID_CLOSE_SESSION = 1;
|
|
||||||
private static final int METHODID_INVOKE = 2;
|
|
||||||
private static final int METHODID_STREAM_EVENTS = 3;
|
|
||||||
|
|
||||||
private static final class MethodHandlers<Req, Resp> implements
|
|
||||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
|
||||||
io.grpc.stub.ServerCalls.ServerStreamingMethod<Req, Resp>,
|
|
||||||
io.grpc.stub.ServerCalls.ClientStreamingMethod<Req, Resp>,
|
|
||||||
io.grpc.stub.ServerCalls.BidiStreamingMethod<Req, Resp> {
|
|
||||||
private final AsyncService serviceImpl;
|
|
||||||
private final int methodId;
|
|
||||||
|
|
||||||
MethodHandlers(AsyncService serviceImpl, int methodId) {
|
|
||||||
this.serviceImpl = serviceImpl;
|
|
||||||
this.methodId = methodId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
@java.lang.SuppressWarnings("unchecked")
|
|
||||||
public void invoke(Req request, io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
|
||||||
switch (methodId) {
|
|
||||||
case METHODID_OPEN_SESSION:
|
|
||||||
serviceImpl.openSession((mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest) request,
|
|
||||||
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply>) responseObserver);
|
|
||||||
break;
|
|
||||||
case METHODID_CLOSE_SESSION:
|
|
||||||
serviceImpl.closeSession((mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest) request,
|
|
||||||
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply>) responseObserver);
|
|
||||||
break;
|
|
||||||
case METHODID_INVOKE:
|
|
||||||
serviceImpl.invoke((mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest) request,
|
|
||||||
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.MxCommandReply>) responseObserver);
|
|
||||||
break;
|
|
||||||
case METHODID_STREAM_EVENTS:
|
|
||||||
serviceImpl.streamEvents((mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest) request,
|
|
||||||
(io.grpc.stub.StreamObserver<mxaccess_gateway.v1.MxaccessGateway.MxEvent>) responseObserver);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
@java.lang.SuppressWarnings("unchecked")
|
|
||||||
public io.grpc.stub.StreamObserver<Req> invoke(
|
|
||||||
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
|
||||||
switch (methodId) {
|
|
||||||
default:
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final io.grpc.ServerServiceDefinition bindService(AsyncService service) {
|
|
||||||
return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
|
|
||||||
.addMethod(
|
|
||||||
getOpenSessionMethod(),
|
|
||||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
|
||||||
new MethodHandlers<
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.OpenSessionRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.OpenSessionReply>(
|
|
||||||
service, METHODID_OPEN_SESSION)))
|
|
||||||
.addMethod(
|
|
||||||
getCloseSessionMethod(),
|
|
||||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
|
||||||
new MethodHandlers<
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.CloseSessionRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.CloseSessionReply>(
|
|
||||||
service, METHODID_CLOSE_SESSION)))
|
|
||||||
.addMethod(
|
|
||||||
getInvokeMethod(),
|
|
||||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
|
||||||
new MethodHandlers<
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxCommandRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxCommandReply>(
|
|
||||||
service, METHODID_INVOKE)))
|
|
||||||
.addMethod(
|
|
||||||
getStreamEventsMethod(),
|
|
||||||
io.grpc.stub.ServerCalls.asyncServerStreamingCall(
|
|
||||||
new MethodHandlers<
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.StreamEventsRequest,
|
|
||||||
mxaccess_gateway.v1.MxaccessGateway.MxEvent>(
|
|
||||||
service, METHODID_STREAM_EVENTS)))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static abstract class MxAccessGatewayBaseDescriptorSupplier
|
|
||||||
implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier {
|
|
||||||
MxAccessGatewayBaseDescriptorSupplier() {}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() {
|
|
||||||
return mxaccess_gateway.v1.MxaccessGateway.getDescriptor();
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() {
|
|
||||||
return getFileDescriptor().findServiceByName("MxAccessGateway");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class MxAccessGatewayFileDescriptorSupplier
|
|
||||||
extends MxAccessGatewayBaseDescriptorSupplier {
|
|
||||||
MxAccessGatewayFileDescriptorSupplier() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class MxAccessGatewayMethodDescriptorSupplier
|
|
||||||
extends MxAccessGatewayBaseDescriptorSupplier
|
|
||||||
implements io.grpc.protobuf.ProtoMethodDescriptorSupplier {
|
|
||||||
private final java.lang.String methodName;
|
|
||||||
|
|
||||||
MxAccessGatewayMethodDescriptorSupplier(java.lang.String methodName) {
|
|
||||||
this.methodName = methodName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() {
|
|
||||||
return getServiceDescriptor().findMethodByName(methodName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static volatile io.grpc.ServiceDescriptor serviceDescriptor;
|
|
||||||
|
|
||||||
public static io.grpc.ServiceDescriptor getServiceDescriptor() {
|
|
||||||
io.grpc.ServiceDescriptor result = serviceDescriptor;
|
|
||||||
if (result == null) {
|
|
||||||
synchronized (MxAccessGatewayGrpc.class) {
|
|
||||||
result = serviceDescriptor;
|
|
||||||
if (result == null) {
|
|
||||||
serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME)
|
|
||||||
.setSchemaDescriptor(new MxAccessGatewayFileDescriptorSupplier())
|
|
||||||
.addMethod(getOpenSessionMethod())
|
|
||||||
.addMethod(getCloseSessionMethod())
|
|
||||||
.addMethod(getInvokeMethod())
|
|
||||||
.addMethod(getStreamEventsMethod())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user