Add XML documentation across gateway, worker, and .NET client
This commit is contained in:
@@ -2,11 +2,14 @@ using System.Globalization;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Cli;
|
namespace MxGateway.Client.Cli;
|
||||||
|
|
||||||
|
/// <summary>Parses command-line arguments into flags and named values.</summary>
|
||||||
internal sealed class CliArguments
|
internal sealed class CliArguments
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);
|
private readonly Dictionary<string, string> _values = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private readonly HashSet<string> _flags = new(StringComparer.OrdinalIgnoreCase);
|
private readonly HashSet<string> _flags = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance by parsing the given command-line arguments.</summary>
|
||||||
|
/// <param name="args">Unparsed command-line arguments; flags prefixed with '--' and values follow their flag.</param>
|
||||||
public CliArguments(IEnumerable<string> args)
|
public CliArguments(IEnumerable<string> args)
|
||||||
{
|
{
|
||||||
string? pendingName = null;
|
string? pendingName = null;
|
||||||
@@ -39,11 +42,15 @@ internal sealed class CliArguments
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns whether the named flag was present in the arguments.</summary>
|
||||||
|
/// <param name="name">The flag name (without '--' prefix).</param>
|
||||||
public bool HasFlag(string name)
|
public bool HasFlag(string name)
|
||||||
{
|
{
|
||||||
return _flags.Contains(name);
|
return _flags.Contains(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the value for a named argument, or <c>null</c> if absent.</summary>
|
||||||
|
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||||
public string? GetOptional(string name)
|
public string? GetOptional(string name)
|
||||||
{
|
{
|
||||||
return _values.TryGetValue(name, out string? value)
|
return _values.TryGetValue(name, out string? value)
|
||||||
@@ -51,6 +58,8 @@ internal sealed class CliArguments
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the value for a required named argument, or throws if absent.</summary>
|
||||||
|
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||||
public string GetRequired(string name)
|
public string GetRequired(string name)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -62,6 +71,9 @@ internal sealed class CliArguments
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses and returns an int32 argument, or the default value if absent.</summary>
|
||||||
|
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||||
|
/// <param name="defaultValue">The default value if the argument is absent; if <c>null</c>, the argument is required.</param>
|
||||||
public int GetInt32(string name, int? defaultValue = null)
|
public int GetInt32(string name, int? defaultValue = null)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -78,6 +90,9 @@ internal sealed class CliArguments
|
|||||||
return int.Parse(value, CultureInfo.InvariantCulture);
|
return int.Parse(value, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses and returns a uint32 argument, or the default value if absent.</summary>
|
||||||
|
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||||
|
/// <param name="defaultValue">The default value if the argument is absent.</param>
|
||||||
public uint GetUInt32(string name, uint defaultValue)
|
public uint GetUInt32(string name, uint defaultValue)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -86,6 +101,9 @@ internal sealed class CliArguments
|
|||||||
: uint.Parse(value, CultureInfo.InvariantCulture);
|
: uint.Parse(value, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses and returns a uint64 argument, or the default value if absent.</summary>
|
||||||
|
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||||
|
/// <param name="defaultValue">The default value if the argument is absent.</param>
|
||||||
public ulong GetUInt64(string name, ulong defaultValue)
|
public ulong GetUInt64(string name, ulong defaultValue)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
@@ -94,6 +112,9 @@ internal sealed class CliArguments
|
|||||||
: ulong.Parse(value, CultureInfo.InvariantCulture);
|
: ulong.Parse(value, CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Parses and returns a TimeSpan argument, or the default value if absent. Supports "ms", "s", and standard TimeSpan format.</summary>
|
||||||
|
/// <param name="name">The argument name (without '--' prefix).</param>
|
||||||
|
/// <param name="defaultValue">The default value if the argument is absent.</param>
|
||||||
public TimeSpan GetDuration(string name, TimeSpan defaultValue)
|
public TimeSpan GetDuration(string name, TimeSpan defaultValue)
|
||||||
{
|
{
|
||||||
string? value = GetOptional(name);
|
string? value = GetOptional(name);
|
||||||
|
|||||||
@@ -5,34 +5,82 @@ namespace MxGateway.Client.Cli;
|
|||||||
|
|
||||||
public interface IMxGatewayCliClient : IAsyncDisposable
|
public interface IMxGatewayCliClient : IAsyncDisposable
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Opens a new gateway session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Session open request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The session open reply.</returns>
|
||||||
Task<OpenSessionReply> OpenSessionAsync(
|
Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes an open gateway session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Session close request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The session close reply.</returns>
|
||||||
Task<CloseSessionReply> CloseSessionAsync(
|
Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes an MXAccess command on the session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The command request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The command reply.</returns>
|
||||||
Task<MxCommandReply> InvokeAsync(
|
Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams events from the gateway session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The stream events request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>An async enumerable of events.</returns>
|
||||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests connection to the Galaxy Repository.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The connection test request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The connection test reply.</returns>
|
||||||
Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the last deployment time from the Galaxy Repository.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The last deploy time request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The last deploy time reply.</returns>
|
||||||
Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discovers the Galaxy Repository hierarchy.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The discover hierarchy request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The discover hierarchy reply.</returns>
|
||||||
Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Watches for deployment events from the Galaxy Repository.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The watch deploy events request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>An async enumerable of deployment events.</returns>
|
||||||
IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
private readonly MxGatewayClient _client;
|
private readonly MxGatewayClient _client;
|
||||||
private readonly Lazy<GalaxyRepositoryClient> _galaxyClient;
|
private readonly Lazy<GalaxyRepositoryClient> _galaxyClient;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MxGatewayCliClientAdapter"/> that bridges the CLI to the gateway client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The gateway client to adapt.</param>
|
||||||
public MxGatewayCliClientAdapter(MxGatewayClient client)
|
public MxGatewayCliClientAdapter(MxGatewayClient client)
|
||||||
{
|
{
|
||||||
_client = client;
|
_client = client;
|
||||||
@@ -16,6 +20,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
() => GalaxyRepositoryClient.Create(_client.Options));
|
() => GalaxyRepositoryClient.Create(_client.Options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<OpenSessionReply> OpenSessionAsync(
|
public Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -23,6 +28,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _client.OpenSessionRawAsync(request, cancellationToken);
|
return _client.OpenSessionRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<CloseSessionReply> CloseSessionAsync(
|
public Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -30,6 +36,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _client.CloseSessionRawAsync(request, cancellationToken);
|
return _client.CloseSessionRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -37,6 +44,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _client.InvokeAsync(request, cancellationToken);
|
return _client.InvokeAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -44,6 +52,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _client.StreamEventsAsync(request, cancellationToken);
|
return _client.StreamEventsAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -51,6 +60,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.TestConnectionRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.TestConnectionRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -58,6 +68,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.GetLastDeployTimeRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.GetLastDeployTimeRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -65,6 +76,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.DiscoverHierarchyRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.DiscoverHierarchyRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
public IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -72,6 +84,7 @@ internal sealed class MxGatewayCliClientAdapter : IMxGatewayCliClient
|
|||||||
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
|
return _galaxyClient.Value.WatchDeployEventsRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_galaxyClient.IsValueCreated)
|
if (_galaxyClient.IsValueCreated)
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
namespace MxGateway.Client.Cli;
|
namespace MxGateway.Client.Cli;
|
||||||
|
|
||||||
|
/// <summary>Utility to redact API keys from error messages for safe output.</summary>
|
||||||
internal static class MxGatewayCliSecretRedactor
|
internal static class MxGatewayCliSecretRedactor
|
||||||
{
|
{
|
||||||
|
/// <summary>Replaces occurrences of the API key in the value with a redacted placeholder.</summary>
|
||||||
|
/// <param name="value">The message text to redact.</param>
|
||||||
|
/// <param name="apiKey">The API key to remove; no redaction if null or empty.</param>
|
||||||
public static string Redact(string value, string? apiKey)
|
public static string Redact(string value, string? apiKey)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
|
if (string.IsNullOrEmpty(value) || string.IsNullOrEmpty(apiKey))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Cli;
|
namespace MxGateway.Client.Cli;
|
||||||
|
|
||||||
|
/// <summary>Command-line interface for the MXAccess Gateway client, supporting session and command operations.</summary>
|
||||||
public static class MxGatewayClientCli
|
public static class MxGatewayClientCli
|
||||||
{
|
{
|
||||||
private const uint MaxAggregateEvents = 10_000;
|
private const uint MaxAggregateEvents = 10_000;
|
||||||
@@ -15,6 +16,10 @@ public static class MxGatewayClientCli
|
|||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
/// <summary>Runs the CLI synchronously with the given arguments, writing output and errors.</summary>
|
||||||
|
/// <param name="args">Command-line arguments (command name followed by options).</param>
|
||||||
|
/// <param name="standardOutput">TextWriter for command output.</param>
|
||||||
|
/// <param name="standardError">TextWriter for error messages.</param>
|
||||||
public static int Run(
|
public static int Run(
|
||||||
string[] args,
|
string[] args,
|
||||||
TextWriter standardOutput,
|
TextWriter standardOutput,
|
||||||
@@ -25,6 +30,11 @@ public static class MxGatewayClientCli
|
|||||||
.GetResult();
|
.GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Runs the CLI asynchronously with the given arguments, writing output and errors.</summary>
|
||||||
|
/// <param name="args">Command-line arguments (command name followed by options).</param>
|
||||||
|
/// <param name="standardOutput">TextWriter for command output.</param>
|
||||||
|
/// <param name="standardError">TextWriter for error messages.</param>
|
||||||
|
/// <param name="clientFactory">Optional factory to create the gateway client; defaults to MxGatewayClient.Create.</param>
|
||||||
public static Task<int> RunAsync(
|
public static Task<int> RunAsync(
|
||||||
string[] args,
|
string[] args,
|
||||||
TextWriter standardOutput,
|
TextWriter standardOutput,
|
||||||
|
|||||||
@@ -3,30 +3,71 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fake Galaxy Repository client transport for testing.
|
||||||
|
/// </summary>
|
||||||
internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport
|
internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions options) : IGalaxyRepositoryClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway client options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the raw gRPC client; always null for the fake.
|
||||||
|
/// </summary>
|
||||||
public GalaxyRepository.GalaxyRepositoryClient? RawClient => null;
|
public GalaxyRepository.GalaxyRepositoryClient? RawClient => null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of TestConnection RPC calls made by the client.
|
||||||
|
/// </summary>
|
||||||
public List<(TestConnectionRequest Request, CallOptions CallOptions)> TestConnectionCalls { get; } = [];
|
public List<(TestConnectionRequest Request, CallOptions CallOptions)> TestConnectionCalls { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of GetLastDeployTime RPC calls made by the client.
|
||||||
|
/// </summary>
|
||||||
public List<(GetLastDeployTimeRequest Request, CallOptions CallOptions)> GetLastDeployTimeCalls { get; } = [];
|
public List<(GetLastDeployTimeRequest Request, CallOptions CallOptions)> GetLastDeployTimeCalls { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of DiscoverHierarchy RPC calls made by the client.
|
||||||
|
/// </summary>
|
||||||
public List<(DiscoverHierarchyRequest Request, CallOptions CallOptions)> DiscoverHierarchyCalls { get; } = [];
|
public List<(DiscoverHierarchyRequest Request, CallOptions CallOptions)> DiscoverHierarchyCalls { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the reply to return from TestConnection; defaults to successful response.
|
||||||
|
/// </summary>
|
||||||
public TestConnectionReply TestConnectionReply { get; set; } = new() { Ok = true };
|
public TestConnectionReply TestConnectionReply { get; set; } = new() { Ok = true };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the reply to return from GetLastDeployTime; defaults to no deploy time present.
|
||||||
|
/// </summary>
|
||||||
public GetLastDeployTimeReply GetLastDeployTimeReply { get; set; } = new() { Present = false };
|
public GetLastDeployTimeReply GetLastDeployTimeReply { get; set; } = new() { Present = false };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the reply to return from DiscoverHierarchy; defaults to empty response.
|
||||||
|
/// </summary>
|
||||||
public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new();
|
public DiscoverHierarchyReply DiscoverHierarchyReply { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the queue of exceptions to throw from TestConnection; dequeued in FIFO order.
|
||||||
|
/// </summary>
|
||||||
public Queue<Exception> TestConnectionExceptions { get; } = new();
|
public Queue<Exception> TestConnectionExceptions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the queue of exceptions to throw from GetLastDeployTime; dequeued in FIFO order.
|
||||||
|
/// </summary>
|
||||||
public Queue<Exception> GetLastDeployTimeExceptions { get; } = new();
|
public Queue<Exception> GetLastDeployTimeExceptions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the queue of exceptions to throw from DiscoverHierarchy; dequeued in FIFO order.
|
||||||
|
/// </summary>
|
||||||
public Queue<Exception> DiscoverHierarchyExceptions { get; } = new();
|
public Queue<Exception> DiscoverHierarchyExceptions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records the request and either throws a queued exception or returns the configured reply.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The TestConnectionRequest to process.</param>
|
||||||
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||||
public Task<TestConnectionReply> TestConnectionAsync(
|
public Task<TestConnectionReply> TestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -40,6 +81,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
return Task.FromResult(TestConnectionReply);
|
return Task.FromResult(TestConnectionReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records the request and either throws a queued exception or returns the configured reply.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The GetLastDeployTimeRequest to process.</param>
|
||||||
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||||
public Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
public Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -53,6 +99,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
return Task.FromResult(GetLastDeployTimeReply);
|
return Task.FromResult(GetLastDeployTimeReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records the request and either throws a queued exception or returns the configured reply.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The DiscoverHierarchyRequest to process.</param>
|
||||||
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||||
public Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
public Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -66,10 +117,19 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
return Task.FromResult(DiscoverHierarchyReply);
|
return Task.FromResult(DiscoverHierarchyReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of WatchDeployEvents RPC calls made by the client.
|
||||||
|
/// </summary>
|
||||||
public List<(WatchDeployEventsRequest Request, CallOptions CallOptions)> WatchDeployEventsCalls { get; } = [];
|
public List<(WatchDeployEventsRequest Request, CallOptions CallOptions)> WatchDeployEventsCalls { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the list of events to stream from WatchDeployEvents.
|
||||||
|
/// </summary>
|
||||||
public List<DeployEvent> WatchDeployEvents { get; } = [];
|
public List<DeployEvent> WatchDeployEvents { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the exception to throw from WatchDeployEvents, if any.
|
||||||
|
/// </summary>
|
||||||
public Exception? WatchDeployEventsException { get; set; }
|
public Exception? WatchDeployEventsException { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,6 +138,11 @@ internal sealed class FakeGalaxyRepositoryTransport(MxGatewayClientOptions optio
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Func<CancellationToken, Task>? WatchDeployEventsBeforeYield { get; set; }
|
public Func<CancellationToken, Task>? WatchDeployEventsBeforeYield { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records the request and streams events, checking for queued exceptions and calling WatchDeployEventsBeforeYield before each event.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The WatchDeployEventsRequest to process.</param>
|
||||||
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||||
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
|
|||||||
@@ -3,23 +3,47 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fake implementation of IMxGatewayClientTransport for testing.
|
||||||
|
/// </summary>
|
||||||
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
|
internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMxGatewayClientTransport
|
||||||
{
|
{
|
||||||
private readonly Queue<MxCommandReply> _invokeReplies = new();
|
private readonly Queue<MxCommandReply> _invokeReplies = new();
|
||||||
private readonly List<MxEvent> _events = [];
|
private readonly List<MxEvent> _events = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway client options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets null, since this is a test fake without a real gRPC client.
|
||||||
|
/// </summary>
|
||||||
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
|
public MxAccessGateway.MxAccessGatewayClient? RawClient => null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of captured OpenSessionAsync calls.
|
||||||
|
/// </summary>
|
||||||
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
|
public List<(OpenSessionRequest Request, CallOptions CallOptions)> OpenSessionCalls { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of captured CloseSessionAsync calls.
|
||||||
|
/// </summary>
|
||||||
public List<(CloseSessionRequest Request, CallOptions CallOptions)> CloseSessionCalls { get; } = [];
|
public List<(CloseSessionRequest Request, CallOptions CallOptions)> CloseSessionCalls { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of captured InvokeAsync calls.
|
||||||
|
/// </summary>
|
||||||
public List<(MxCommandRequest Request, CallOptions CallOptions)> InvokeCalls { get; } = [];
|
public List<(MxCommandRequest Request, CallOptions CallOptions)> InvokeCalls { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of captured StreamEventsAsync calls.
|
||||||
|
/// </summary>
|
||||||
public List<(StreamEventsRequest Request, CallOptions CallOptions)> StreamEventsCalls { get; } = [];
|
public List<(StreamEventsRequest Request, CallOptions CallOptions)> StreamEventsCalls { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the reply to return from OpenSessionAsync.
|
||||||
|
/// </summary>
|
||||||
public OpenSessionReply OpenSessionReply { get; set; } = new()
|
public OpenSessionReply OpenSessionReply { get; set; } = new()
|
||||||
{
|
{
|
||||||
SessionId = "session-fixture",
|
SessionId = "session-fixture",
|
||||||
@@ -29,6 +53,9 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the reply to return from CloseSessionAsync.
|
||||||
|
/// </summary>
|
||||||
public CloseSessionReply CloseSessionReply { get; set; } = new()
|
public CloseSessionReply CloseSessionReply { get; set; } = new()
|
||||||
{
|
{
|
||||||
SessionId = "session-fixture",
|
SessionId = "session-fixture",
|
||||||
@@ -36,12 +63,26 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the queue of exceptions to throw from OpenSessionAsync.
|
||||||
|
/// </summary>
|
||||||
public Queue<Exception> OpenSessionExceptions { get; } = new();
|
public Queue<Exception> OpenSessionExceptions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the queue of exceptions to throw from CloseSessionAsync.
|
||||||
|
/// </summary>
|
||||||
public Queue<Exception> CloseSessionExceptions { get; } = new();
|
public Queue<Exception> CloseSessionExceptions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the queue of exceptions to throw from InvokeAsync.
|
||||||
|
/// </summary>
|
||||||
public Queue<Exception> InvokeExceptions { get; } = new();
|
public Queue<Exception> InvokeExceptions { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the OpenSessionAsync call is recorded and returns the configured reply.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The OpenSessionRequest to process.</param>
|
||||||
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||||
public Task<OpenSessionReply> OpenSessionAsync(
|
public Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -55,6 +96,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
return Task.FromResult(OpenSessionReply);
|
return Task.FromResult(OpenSessionReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the CloseSessionAsync call is recorded and returns the configured reply.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The CloseSessionRequest to process.</param>
|
||||||
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||||
public Task<CloseSessionReply> CloseSessionAsync(
|
public Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -68,6 +114,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
return Task.FromResult(CloseSessionReply);
|
return Task.FromResult(CloseSessionReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the InvokeAsync call is recorded and returns the next enqueued reply.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The MxCommandRequest to process.</param>
|
||||||
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -81,6 +132,11 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
return Task.FromResult(_invokeReplies.Dequeue());
|
return Task.FromResult(_invokeReplies.Dequeue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that the StreamEventsAsync call is recorded and yields all enqueued events.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The StreamEventsRequest to process.</param>
|
||||||
|
/// <param name="callOptions">Call options specifying RPC behavior.</param>
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -95,11 +151,19 @@ internal sealed class FakeGatewayTransport(MxGatewayClientOptions options) : IMx
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enqueues a reply to be returned from the next InvokeAsync call.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reply">The reply to enqueue.</param>
|
||||||
public void AddInvokeReply(MxCommandReply reply)
|
public void AddInvokeReply(MxCommandReply reply)
|
||||||
{
|
{
|
||||||
_invokeReplies.Enqueue(reply);
|
_invokeReplies.Enqueue(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enqueues an event to be yielded from StreamEventsAsync.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="gatewayEvent">The event to enqueue.</param>
|
||||||
public void AddEvent(MxEvent gatewayEvent)
|
public void AddEvent(MxEvent gatewayEvent)
|
||||||
{
|
{
|
||||||
_events.Add(gatewayEvent);
|
_events.Add(gatewayEvent);
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class GalaxyRepositoryClientTests
|
public sealed class GalaxyRepositoryClientTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that TestConnectionAsync attaches the API key in request metadata and returns the Ok flag.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
|
public async Task TestConnectionAsync_AttachesApiKeyMetadataAndReturnsOkFlag()
|
||||||
{
|
{
|
||||||
@@ -21,6 +24,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
|
Assert.Equal("Bearer test-api-key", call.CallOptions.Headers?.GetValue("authorization"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that TestConnectionAsync returns false when the server reports NotOk.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
|
public async Task TestConnectionAsync_ReturnsFalseWhenServerReportsNotOk()
|
||||||
{
|
{
|
||||||
@@ -33,6 +39,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.False(ok);
|
Assert.False(ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that GetLastDeployTimeAsync returns null when the server reports not present.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
|
public async Task GetLastDeployTimeAsync_ReturnsNullWhenNotPresent()
|
||||||
{
|
{
|
||||||
@@ -46,6 +55,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Single(transport.GetLastDeployTimeCalls);
|
Assert.Single(transport.GetLastDeployTimeCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that GetLastDeployTimeAsync returns the timestamp when the server reports it present.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
|
public async Task GetLastDeployTimeAsync_ReturnsTimestampWhenPresent()
|
||||||
{
|
{
|
||||||
@@ -64,6 +76,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(expected, deployTime!.Value);
|
Assert.Equal(expected, deployTime!.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that DiscoverHierarchyAsync returns the objects from the server reply.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
public async Task DiscoverHierarchyAsync_ReturnsObjectsFromReply()
|
||||||
{
|
{
|
||||||
@@ -104,6 +119,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal("DelmiaReceiver_001.DownloadPath", attribute.FullTagReference);
|
Assert.Equal("DelmiaReceiver_001.DownloadPath", attribute.FullTagReference);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that DiscoverHierarchyAsync propagates cancellation tokens to the transport.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
|
public async Task DiscoverHierarchyAsync_PropagatesCancellationToTransport()
|
||||||
{
|
{
|
||||||
@@ -121,6 +139,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
|
Assert.False(call.CallOptions.CancellationToken.IsCancellationRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that TestConnectionAsync retries on transient gRPC failures.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
|
public async Task TestConnectionAsync_RetriesOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -135,6 +156,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(2, transport.TestConnectionCalls.Count);
|
Assert.Equal(2, transport.TestConnectionCalls.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that DiscoverHierarchyAsync retries on transient gRPC failures.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
|
public async Task DiscoverHierarchyAsync_RetriesOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -148,6 +172,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
|
Assert.Equal(2, transport.DiscoverHierarchyCalls.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that WatchDeployEventsAsync delivers the bootstrap event.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
|
public async Task WatchDeployEventsAsync_DeliversBootstrapEvent()
|
||||||
{
|
{
|
||||||
@@ -181,6 +208,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Null(call.Request.LastSeenDeployTime);
|
Assert.Null(call.Request.LastSeenDeployTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that WatchDeployEventsAsync delivers multiple events in order.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
|
public async Task WatchDeployEventsAsync_DeliversMultipleEventsInOrder()
|
||||||
{
|
{
|
||||||
@@ -216,6 +246,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(t0, call.Request.LastSeenDeployTime!.ToDateTime());
|
Assert.Equal(t0, call.Request.LastSeenDeployTime!.ToDateTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that WatchDeployEventsAsync stops iteration cleanly when cancelled.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
|
public async Task WatchDeployEventsAsync_CancellationStopsIterationCleanly()
|
||||||
{
|
{
|
||||||
@@ -257,6 +290,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
Assert.Equal(1ul, received[0].Sequence);
|
Assert.Equal(1ul, received[0].Sequence);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that WatchDeployEventsAsync throws ObjectDisposedException after the client is disposed.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
|
public async Task WatchDeployEventsAsync_ThrowsAfterDisposal()
|
||||||
{
|
{
|
||||||
@@ -269,6 +305,9 @@ public sealed class GalaxyRepositoryClientTests
|
|||||||
client.WatchDeployEventsAsync());
|
client.WatchDeployEventsAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that TestConnectionAsync throws ObjectDisposedException after the client is disposed.
|
||||||
|
/// </summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task TestConnectionAsync_ThrowsAfterDisposal()
|
public async Task TestConnectionAsync_ThrowsAfterDisposal()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxCommandReplyExtensionsTests
|
public sealed class MxCommandReplyExtensionsTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that successful replies pass both protocol and MxAccess success checks.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EnsureSuccess_WithRegisterFixture_ReturnsReply()
|
public void EnsureSuccess_WithRegisterFixture_ReturnsReply()
|
||||||
{
|
{
|
||||||
@@ -15,6 +16,7 @@ public sealed class MxCommandReplyExtensionsTests
|
|||||||
Assert.Same(reply, reply.EnsureMxAccessSuccess());
|
Assert.Same(reply, reply.EnsureMxAccessSuccess());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that MxAccess failures throw with preserved HResult and status details.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EnsureMxAccessSuccess_WithFailureFixture_PreservesHResultAndStatuses()
|
public void EnsureMxAccessSuccess_WithFailureFixture_PreservesHResultAndStatuses()
|
||||||
{
|
{
|
||||||
@@ -30,6 +32,7 @@ public sealed class MxCommandReplyExtensionsTests
|
|||||||
Assert.Contains("0x80040200", exception.Message);
|
Assert.Contains("0x80040200", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that session-not-found protocol failures throw the correct gateway exception.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EnsureProtocolSuccess_WithSessionFailure_ThrowsSessionException()
|
public void EnsureProtocolSuccess_WithSessionFailure_ThrowsSessionException()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
/// <summary>Tests for the CLI command interface.</summary>
|
||||||
public sealed class MxGatewayClientCliTests
|
public sealed class MxGatewayClientCliTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that the version command prints compiled protocol versions.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Run_Version_PrintsCompiledProtocolVersions()
|
public void Run_Version_PrintsCompiledProtocolVersions()
|
||||||
{
|
{
|
||||||
@@ -21,6 +23,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that the version command with --json flag prints JSON protocol versions.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
|
public async Task RunAsync_VersionJson_PrintsJsonProtocolVersions()
|
||||||
{
|
{
|
||||||
@@ -34,6 +37,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that the write command builds a write request and prints JSON reply.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
|
public async Task RunAsync_Write_BuildsWriteCommandAndPrintsJsonReply()
|
||||||
{
|
{
|
||||||
@@ -78,6 +82,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that error output redacts sensitive API key values.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_ErrorOutput_RedactsApiKey()
|
public async Task RunAsync_ErrorOutput_RedactsApiKey()
|
||||||
{
|
{
|
||||||
@@ -101,6 +106,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Contains("[redacted]", error.ToString());
|
Assert.Contains("[redacted]", error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that stream-events with max-events limit stops output in non-JSON format.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
|
public async Task RunAsync_StreamEvents_WithMaxEventsStopsNonJsonOutput()
|
||||||
{
|
{
|
||||||
@@ -142,6 +148,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Verifies that smoke command closes opened session when a command fails.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
|
public async Task RunAsync_Smoke_WhenCommandFails_ClosesOpenedSession()
|
||||||
{
|
{
|
||||||
@@ -172,6 +179,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal("session-fixture", closeRequest.SessionId);
|
Assert.Equal("session-fixture", closeRequest.SessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that galaxy-test-connection command prints JSON reply.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply()
|
public async Task RunAsync_GalaxyTestConnection_PrintsJsonReply()
|
||||||
{
|
{
|
||||||
@@ -201,6 +209,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that galaxy-discover command prints hierarchy summary.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary()
|
public async Task RunAsync_GalaxyDiscover_PrintsHierarchySummary()
|
||||||
{
|
{
|
||||||
@@ -250,6 +259,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that galaxy-watch command prints text output for deploy events.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
|
public async Task RunAsync_GalaxyWatch_PrintsTextOutputForEvents()
|
||||||
{
|
{
|
||||||
@@ -303,6 +313,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Equal(string.Empty, error.ToString());
|
Assert.Equal(string.Empty, error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that galaxy-watch with --json emits one JSON object per event.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent()
|
public async Task RunAsync_GalaxyWatch_JsonEmitsOneObjectPerEvent()
|
||||||
{
|
{
|
||||||
@@ -337,23 +348,31 @@ public sealed class MxGatewayClientCliTests
|
|||||||
Assert.Contains("\"objectCount\": 99", text);
|
Assert.Contains("\"objectCount\": 99", text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Fake CLI client for testing.</summary>
|
||||||
private sealed class FakeCliClient : IMxGatewayCliClient
|
private sealed class FakeCliClient : IMxGatewayCliClient
|
||||||
{
|
{
|
||||||
|
/// <summary>Queue of invoke replies to return.</summary>
|
||||||
public Queue<MxCommandReply> InvokeReplies { get; } = new();
|
public Queue<MxCommandReply> InvokeReplies { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>List of received invoke requests.</summary>
|
||||||
public List<MxCommandRequest> InvokeRequests { get; } = [];
|
public List<MxCommandRequest> InvokeRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of received close session requests.</summary>
|
||||||
public List<CloseSessionRequest> CloseSessionRequests { get; } = [];
|
public List<CloseSessionRequest> CloseSessionRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of events to yield when streaming.</summary>
|
||||||
public List<MxEvent> Events { get; } = [];
|
public List<MxEvent> Events { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>Exception to throw on invoke, if any.</summary>
|
||||||
public Exception? InvokeFailure { get; init; }
|
public Exception? InvokeFailure { get; init; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<OpenSessionReply> OpenSessionAsync(
|
public Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -367,6 +386,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<CloseSessionReply> CloseSessionAsync(
|
public Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -380,6 +400,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -393,6 +414,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
return Task.FromResult(InvokeReplies.Dequeue());
|
return Task.FromResult(InvokeReplies.Dequeue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
@@ -405,18 +427,25 @@ public sealed class MxGatewayClientCliTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Galaxy test connection reply to return.</summary>
|
||||||
public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true };
|
public TestConnectionReply GalaxyTestConnectionReply { get; set; } = new() { Ok = true };
|
||||||
|
|
||||||
|
/// <summary>Galaxy get last deploy time reply to return.</summary>
|
||||||
public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false };
|
public GetLastDeployTimeReply GalaxyGetLastDeployTimeReply { get; set; } = new() { Present = false };
|
||||||
|
|
||||||
|
/// <summary>Galaxy discover hierarchy reply to return.</summary>
|
||||||
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
|
public DiscoverHierarchyReply GalaxyDiscoverHierarchyReply { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>List of received galaxy test connection requests.</summary>
|
||||||
public List<TestConnectionRequest> GalaxyTestConnectionRequests { get; } = [];
|
public List<TestConnectionRequest> GalaxyTestConnectionRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of received galaxy get last deploy time requests.</summary>
|
||||||
public List<GetLastDeployTimeRequest> GalaxyGetLastDeployTimeRequests { get; } = [];
|
public List<GetLastDeployTimeRequest> GalaxyGetLastDeployTimeRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of received galaxy discover hierarchy requests.</summary>
|
||||||
public List<DiscoverHierarchyRequest> GalaxyDiscoverHierarchyRequests { get; } = [];
|
public List<DiscoverHierarchyRequest> GalaxyDiscoverHierarchyRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
public Task<TestConnectionReply> GalaxyTestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -425,6 +454,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
return Task.FromResult(GalaxyTestConnectionReply);
|
return Task.FromResult(GalaxyTestConnectionReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
public Task<GetLastDeployTimeReply> GalaxyGetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -433,6 +463,7 @@ public sealed class MxGatewayClientCliTests
|
|||||||
return Task.FromResult(GalaxyGetLastDeployTimeReply);
|
return Task.FromResult(GalaxyGetLastDeployTimeReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
public Task<DiscoverHierarchyReply> GalaxyDiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
@@ -441,10 +472,13 @@ public sealed class MxGatewayClientCliTests
|
|||||||
return Task.FromResult(GalaxyDiscoverHierarchyReply);
|
return Task.FromResult(GalaxyDiscoverHierarchyReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>List of received galaxy watch deploy events requests.</summary>
|
||||||
public List<WatchDeployEventsRequest> GalaxyWatchDeployEventsRequests { get; } = [];
|
public List<WatchDeployEventsRequest> GalaxyWatchDeployEventsRequests { get; } = [];
|
||||||
|
|
||||||
|
/// <summary>List of deploy events to yield when watching.</summary>
|
||||||
public List<DeployEvent> GalaxyDeployEvents { get; } = [];
|
public List<DeployEvent> GalaxyDeployEvents { get; } = [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
public async IAsyncEnumerable<DeployEvent> GalaxyWatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxGatewayClientContractInfoTests
|
public sealed class MxGatewayClientContractInfoTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that the client's gateway protocol version matches the shared contract definition.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GatewayProtocolVersion_MatchesSharedContract()
|
public void GatewayProtocolVersion_MatchesSharedContract()
|
||||||
{
|
{
|
||||||
@@ -12,6 +13,7 @@ public sealed class MxGatewayClientContractInfoTests
|
|||||||
MxGatewayClientContractInfo.GatewayProtocolVersion);
|
MxGatewayClientContractInfo.GatewayProtocolVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that the client's worker protocol version matches the shared contract definition.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WorkerProtocolVersion_MatchesSharedContract()
|
public void WorkerProtocolVersion_MatchesSharedContract()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxGatewayClientOptionsTests
|
public sealed class MxGatewayClientOptionsTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that options with valid endpoint and API key pass validation.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_WithAbsoluteEndpointAndApiKey_Succeeds()
|
public void Validate_WithAbsoluteEndpointAndApiKey_Succeeds()
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,7 @@ public sealed class MxGatewayClientOptionsTests
|
|||||||
options.Validate();
|
options.Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that empty API key causes validation to fail.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_WithEmptyApiKey_Throws()
|
public void Validate_WithEmptyApiKey_Throws()
|
||||||
{
|
{
|
||||||
@@ -26,6 +28,7 @@ public sealed class MxGatewayClientOptionsTests
|
|||||||
Assert.Throws<ArgumentException>(options.Validate);
|
Assert.Throws<ArgumentException>(options.Validate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that invalid retry options cause validation to fail.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Validate_WithInvalidRetryOptions_Throws()
|
public void Validate_WithInvalidRetryOptions_Throws()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ using Grpc.Core;
|
|||||||
|
|
||||||
namespace MxGateway.Client.Tests;
|
namespace MxGateway.Client.Tests;
|
||||||
|
|
||||||
|
/// <summary>Tests for MxGatewaySession and client command behavior.</summary>
|
||||||
public sealed class MxGatewayClientSessionTests
|
public sealed class MxGatewayClientSessionTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that open session attaches API key metadata and cancellation token.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
|
public async Task OpenSessionRawAsync_AttachesApiKeyMetadataAndCancellation()
|
||||||
{
|
{
|
||||||
@@ -19,6 +21,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(cancellation.Token, call.CallOptions.CancellationToken);
|
Assert.Equal(cancellation.Token, call.CallOptions.CancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that open session returns a session with the raw open reply.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
|
public async Task OpenSessionAsync_ReturnsSessionWithRawOpenReply()
|
||||||
{
|
{
|
||||||
@@ -33,6 +36,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
|
Assert.Equal(1234, session.OpenSessionReply.WorkerProcessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that register builds a register command and returns server handle.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
|
public async Task RegisterAsync_BuildsRegisterCommandAndReturnsServerHandle()
|
||||||
{
|
{
|
||||||
@@ -57,6 +61,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal("fixture-client", call.Request.Command.Register.ClientName);
|
Assert.Equal("fixture-client", call.Request.Command.Register.ClientName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that add item 2 builds a command with the specified context.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
|
public async Task AddItem2Async_BuildsAddItem2CommandWithContext()
|
||||||
{
|
{
|
||||||
@@ -81,6 +86,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal("runtime", request.Command.AddItem2.ItemContext);
|
Assert.Equal("runtime", request.Command.AddItem2.ItemContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that write raw builds a write command with the raw value.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
|
public async Task WriteRawAsync_BuildsWriteCommandWithRawValue()
|
||||||
{
|
{
|
||||||
@@ -111,6 +117,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(56, request.Command.Write.UserId);
|
Assert.Equal(56, request.Command.Write.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that write 2 raw builds a write 2 command with value and timestamp.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
|
public async Task Write2RawAsync_BuildsWrite2CommandWithValueAndTimestamp()
|
||||||
{
|
{
|
||||||
@@ -138,6 +145,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(56, request.Command.Write2.UserId);
|
Assert.Equal(56, request.Command.Write2.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that subscribe bulk builds one command and returns per-item results.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
|
public async Task SubscribeBulkAsync_BuildsOneBulkCommandAndReturnsPerItemResults()
|
||||||
{
|
{
|
||||||
@@ -176,6 +184,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(["Area001.Pump001.Speed"], request.Command.SubscribeBulk.TagAddresses);
|
Assert.Equal(["Area001.Pump001.Speed"], request.Command.SubscribeBulk.TagAddresses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that stream events yields events in the order received from the gateway.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
|
public async Task StreamEventsAsync_YieldsEventsInGatewayOrder()
|
||||||
{
|
{
|
||||||
@@ -206,6 +215,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal("session-fixture", request.SessionId);
|
Assert.Equal("session-fixture", request.SessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that close is explicit and idempotent.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CloseAsync_IsExplicitAndIdempotent()
|
public async Task CloseAsync_IsExplicitAndIdempotent()
|
||||||
{
|
{
|
||||||
@@ -221,6 +231,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal("session-fixture", call.Request.SessionId);
|
Assert.Equal("session-fixture", call.Request.SessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that invoke retries safe diagnostic commands on transient RPC failure.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
|
public async Task InvokeAsync_RetriesSafeDiagnosticCommandOnTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -244,6 +255,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Equal(2, transport.InvokeCalls.Count);
|
Assert.Equal(2, transport.InvokeCalls.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that open session does not retry on transient RPC failure.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
|
public async Task OpenSessionAsync_DoesNotRetryTransientGrpcFailure()
|
||||||
{
|
{
|
||||||
@@ -256,6 +268,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Single(transport.OpenSessionCalls);
|
Assert.Single(transport.OpenSessionCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that invoke does not retry write commands on transient RPC failure.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeAsync_DoesNotRetryWriteCommand()
|
public async Task InvokeAsync_DoesNotRetryWriteCommand()
|
||||||
{
|
{
|
||||||
@@ -270,6 +283,7 @@ public sealed class MxGatewayClientSessionTests
|
|||||||
Assert.Single(transport.InvokeCalls);
|
Assert.Single(transport.InvokeCalls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that invoke helpers pass cancellation token to the transport.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task InvokeHelpers_PassCancellationTokenToTransport()
|
public async Task InvokeHelpers_PassCancellationTokenToTransport()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxGatewayGeneratedContractTests
|
public sealed class MxGatewayGeneratedContractTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that the generated gRPC client can be instantiated from the client factory.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
|
public async Task GeneratedGrpcClient_CanBeConstructedFromClientFactory()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxStatusProxyExtensionsTests
|
public sealed class MxStatusProxyExtensionsTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that fixture statuses correctly project success and preserve raw integer fields.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FixtureStatuses_ProjectSuccessAndPreserveRawFields()
|
public void FixtureStatuses_ProjectSuccessAndPreserveRawFields()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace MxGateway.Client.Tests;
|
|||||||
|
|
||||||
public sealed class MxValueExtensionsTests
|
public sealed class MxValueExtensionsTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that scalar values are converted to correctly-typed MxValue protobuf messages.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToMxValue_WithScalarValues_CreatesTypedProtobufValues()
|
public void ToMxValue_WithScalarValues_CreatesTypedProtobufValues()
|
||||||
{
|
{
|
||||||
@@ -18,6 +19,7 @@ public sealed class MxValueExtensionsTests
|
|||||||
Assert.Equal(MxValue.KindOneofCase.StringValue, "alpha".ToMxValue().KindCase);
|
Assert.Equal(MxValue.KindOneofCase.StringValue, "alpha".ToMxValue().KindCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that array values are converted to array-kind MxValue messages with correct element types and dimensions.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ToMxValue_WithArrays_CreatesTypedArrayProtobufValues()
|
public void ToMxValue_WithArrays_CreatesTypedArrayProtobufValues()
|
||||||
{
|
{
|
||||||
@@ -29,6 +31,7 @@ public sealed class MxValueExtensionsTests
|
|||||||
Assert.Equal([2U], value.ArrayValue.Dimensions);
|
Assert.Equal([2U], value.ArrayValue.Dimensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that fixture test cases project to expected MxValue kinds and preserve raw type metadata.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FixtureValues_ProjectExpectedKindsAndPreserveRawMetadata()
|
public void FixtureValues_ProjectExpectedKindsAndPreserveRawMetadata()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a Galaxy Repository client with custom transport and options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">Client options.</param>
|
||||||
|
/// <param name="transport">The underlying gRPC transport.</param>
|
||||||
internal GalaxyRepositoryClient(
|
internal GalaxyRepositoryClient(
|
||||||
MxGatewayClientOptions options,
|
MxGatewayClientOptions options,
|
||||||
IGalaxyRepositoryClientTransport transport)
|
IGalaxyRepositoryClientTransport transport)
|
||||||
@@ -50,12 +55,23 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
Options.LoggerFactory?.CreateLogger<GalaxyRepositoryClient>());
|
Options.LoggerFactory?.CreateLogger<GalaxyRepositoryClient>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Client options used to configure timeouts, authentication, and retry policy.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; }
|
public MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying generated gRPC client for advanced operations.
|
||||||
|
/// </summary>
|
||||||
public GalaxyRepository.GalaxyRepositoryClient RawClient =>
|
public GalaxyRepository.GalaxyRepositoryClient RawClient =>
|
||||||
_transport.RawClient
|
_transport.RawClient
|
||||||
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
|
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Galaxy Repository client with the given options, establishing a new gRPC channel.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">Client options.</param>
|
||||||
|
/// <returns>A new client instance.</returns>
|
||||||
public static GalaxyRepositoryClient Create(MxGatewayClientOptions options)
|
public static GalaxyRepositoryClient Create(MxGatewayClientOptions options)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
@@ -81,6 +97,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
/// Probes the Galaxy Repository database connection. Returns true when the
|
/// Probes the Galaxy Repository database connection. Returns true when the
|
||||||
/// gateway can reach the configured ZB SQL Server.
|
/// gateway can reach the configured ZB SQL Server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>True if connection is successful, false otherwise.</returns>
|
||||||
public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken = default)
|
public async Task<bool> TestConnectionAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
TestConnectionReply reply = await TestConnectionRawAsync(
|
TestConnectionReply reply = await TestConnectionRawAsync(
|
||||||
@@ -91,6 +109,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return reply.Ok;
|
return reply.Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Probes the Galaxy Repository database connection without result wrapping.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The test connection request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<TestConnectionReply> TestConnectionRawAsync(
|
public Task<TestConnectionReply> TestConnectionRawAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -107,6 +131,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
/// Returns the timestamp of the most recent Galaxy deployment, or
|
/// Returns the timestamp of the most recent Galaxy deployment, or
|
||||||
/// <see langword="null"/> when no deployment has been recorded.
|
/// <see langword="null"/> when no deployment has been recorded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The deployment timestamp, or null if not recorded.</returns>
|
||||||
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken cancellationToken = default)
|
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
GetLastDeployTimeReply reply = await GetLastDeployTimeRawAsync(
|
GetLastDeployTimeReply reply = await GetLastDeployTimeRawAsync(
|
||||||
@@ -122,6 +148,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return reply.TimeOfLastDeploy.ToDateTime();
|
return reply.TimeOfLastDeploy.ToDateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the most recent Galaxy deployment timestamp without result wrapping.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The last deploy-time request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<GetLastDeployTimeReply> GetLastDeployTimeRawAsync(
|
public Task<GetLastDeployTimeReply> GetLastDeployTimeRawAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -139,6 +171,8 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
/// includes its dynamic attributes so callers can determine which tag references
|
/// includes its dynamic attributes so callers can determine which tag references
|
||||||
/// they may subscribe to via the MxAccessGateway service.
|
/// they may subscribe to via the MxAccessGateway service.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The collection of Galaxy objects in the hierarchy.</returns>
|
||||||
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
|
public async Task<IReadOnlyList<GalaxyObject>> DiscoverHierarchyAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
|
DiscoverHierarchyReply reply = await DiscoverHierarchyRawAsync(
|
||||||
@@ -149,6 +183,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return reply.Objects;
|
return reply.Objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerates the Galaxy object hierarchy without result wrapping.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The discover-hierarchy request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<DiscoverHierarchyReply> DiscoverHierarchyRawAsync(
|
public Task<DiscoverHierarchyReply> DiscoverHierarchyRawAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -173,6 +213,9 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
/// at-least-once delivery beyond the per-subscriber buffer (gaps in
|
/// at-least-once delivery beyond the per-subscriber buffer (gaps in
|
||||||
/// <see cref="DeployEvent.Sequence"/> indicate dropped events).
|
/// <see cref="DeployEvent.Sequence"/> indicate dropped events).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
/// <param name="lastSeenDeployTime">Optional timestamp to suppress the bootstrap event.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>An async enumerable of deploy events.</returns>
|
||||||
public IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
public IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
DateTimeOffset? lastSeenDeployTime = null,
|
DateTimeOffset? lastSeenDeployTime = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -188,6 +231,12 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return WatchDeployEventsRawAsync(request, cancellationToken);
|
return WatchDeployEventsRawAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes to Galaxy deploy events without result wrapping.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The watch-deploy-events request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>An async enumerable of raw deploy events.</returns>
|
||||||
public IAsyncEnumerable<DeployEvent> WatchDeployEventsRawAsync(
|
public IAsyncEnumerable<DeployEvent> WatchDeployEventsRawAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -211,6 +260,9 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the gRPC channel and releases resources.
|
||||||
|
/// </summary>
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@@ -223,16 +275,32 @@ public sealed class GalaxyRepositoryClient : IAsyncDisposable
|
|||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates gRPC call options with the client's default timeout and API-key authorization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The call options.</returns>
|
||||||
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
|
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
|
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates gRPC call options for streaming RPCs with the stream timeout and API-key authorization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The stream call options.</returns>
|
||||||
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
|
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
|
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates gRPC call options with the specified timeout and API-key authorization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <param name="timeout">Optional timeout duration.</param>
|
||||||
|
/// <returns>The call options.</returns>
|
||||||
internal CallOptions CreateCallOptions(
|
internal CallOptions CreateCallOptions(
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
TimeSpan? timeout)
|
TimeSpan? timeout)
|
||||||
|
|||||||
@@ -3,16 +3,27 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// gRPC implementation of IGalaxyRepositoryClientTransport.
|
||||||
|
/// </summary>
|
||||||
internal sealed class GrpcGalaxyRepositoryClientTransport(
|
internal sealed class GrpcGalaxyRepositoryClientTransport(
|
||||||
MxGatewayClientOptions options,
|
MxGatewayClientOptions options,
|
||||||
GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport
|
GalaxyRepository.GalaxyRepositoryClient rawClient) : IGalaxyRepositoryClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway client options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the underlying gRPC client.
|
||||||
|
/// </summary>
|
||||||
public GalaxyRepository.GalaxyRepositoryClient RawClient { get; } = rawClient;
|
public GalaxyRepository.GalaxyRepositoryClient RawClient { get; } = rawClient;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
GalaxyRepository.GalaxyRepositoryClient? IGalaxyRepositoryClientTransport.RawClient => RawClient;
|
GalaxyRepository.GalaxyRepositoryClient? IGalaxyRepositoryClientTransport.RawClient => RawClient;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<TestConnectionReply> TestConnectionAsync(
|
public async Task<TestConnectionReply> TestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -29,6 +40,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
public async Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -45,6 +57,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
public async Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -61,6 +74,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
public async IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions,
|
CallOptions callOptions,
|
||||||
@@ -94,6 +108,7 @@ internal sealed class GrpcGalaxyRepositoryClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
IAsyncEnumerable<DeployEvent> IGalaxyRepositoryClientTransport.WatchDeployEventsAsync(
|
IAsyncEnumerable<DeployEvent> IGalaxyRepositoryClientTransport.WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
|
|||||||
@@ -3,16 +3,27 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// gRPC implementation of IMxGatewayClientTransport.
|
||||||
|
/// </summary>
|
||||||
internal sealed class GrpcMxGatewayClientTransport(
|
internal sealed class GrpcMxGatewayClientTransport(
|
||||||
MxGatewayClientOptions options,
|
MxGatewayClientOptions options,
|
||||||
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
|
MxAccessGateway.MxAccessGatewayClient rawClient) : IMxGatewayClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway client options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; } = options;
|
public MxGatewayClientOptions Options { get; } = options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the underlying gRPC client.
|
||||||
|
/// </summary>
|
||||||
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
|
public MxAccessGateway.MxAccessGatewayClient RawClient { get; } = rawClient;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
|
MxAccessGateway.MxAccessGatewayClient? IMxGatewayClientTransport.RawClient => RawClient;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<OpenSessionReply> OpenSessionAsync(
|
public async Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -29,6 +40,7 @@ internal sealed class GrpcMxGatewayClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<CloseSessionReply> CloseSessionAsync(
|
public async Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -45,6 +57,7 @@ internal sealed class GrpcMxGatewayClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<MxCommandReply> InvokeAsync(
|
public async Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
@@ -61,6 +74,7 @@ internal sealed class GrpcMxGatewayClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CallOptions callOptions,
|
CallOptions callOptions,
|
||||||
@@ -94,6 +108,7 @@ internal sealed class GrpcMxGatewayClientTransport(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
IAsyncEnumerable<MxEvent> IMxGatewayClientTransport.StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CallOptions callOptions)
|
CallOptions callOptions)
|
||||||
|
|||||||
@@ -3,24 +3,39 @@ using MxGateway.Contracts.Proto.Galaxy;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Transport layer for Galaxy Repository gRPC operations.</summary>
|
||||||
internal interface IGalaxyRepositoryClientTransport
|
internal interface IGalaxyRepositoryClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets the client options used to configure this transport.</summary>
|
||||||
MxGatewayClientOptions Options { get; }
|
MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>Gets the underlying gRPC client, or <c>null</c> if not yet initialized.</summary>
|
||||||
GalaxyRepository.GalaxyRepositoryClient? RawClient { get; }
|
GalaxyRepository.GalaxyRepositoryClient? RawClient { get; }
|
||||||
|
|
||||||
|
/// <summary>Tests the connection to the Galaxy Repository server.</summary>
|
||||||
|
/// <param name="request">The test connection request.</param>
|
||||||
|
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
||||||
Task<TestConnectionReply> TestConnectionAsync(
|
Task<TestConnectionReply> TestConnectionAsync(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
CallOptions callOptions);
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
/// <summary>Gets the last deploy time from the Galaxy Repository server.</summary>
|
||||||
|
/// <param name="request">The get last deploy time request.</param>
|
||||||
|
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
||||||
Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
Task<GetLastDeployTimeReply> GetLastDeployTimeAsync(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
CallOptions callOptions);
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
/// <summary>Discovers the object hierarchy in the Galaxy Repository.</summary>
|
||||||
|
/// <param name="request">The discover hierarchy request.</param>
|
||||||
|
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
||||||
Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
Task<DiscoverHierarchyReply> DiscoverHierarchyAsync(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
CallOptions callOptions);
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
/// <summary>Watches for deployment events from the Galaxy Repository server.</summary>
|
||||||
|
/// <param name="request">The watch deploy events request.</param>
|
||||||
|
/// <param name="callOptions">gRPC call options (timeout, cancellation, etc.).</param>
|
||||||
IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
IAsyncEnumerable<DeployEvent> WatchDeployEventsAsync(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
CallOptions callOptions);
|
CallOptions callOptions);
|
||||||
|
|||||||
@@ -5,22 +5,52 @@ namespace MxGateway.Client;
|
|||||||
|
|
||||||
internal interface IMxGatewayClientTransport
|
internal interface IMxGatewayClientTransport
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client configuration options.
|
||||||
|
/// </summary>
|
||||||
MxGatewayClientOptions Options { get; }
|
MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the underlying gRPC client, if available.
|
||||||
|
/// </summary>
|
||||||
MxAccessGateway.MxAccessGatewayClient? RawClient { get; }
|
MxAccessGateway.MxAccessGatewayClient? RawClient { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens a new gateway session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Session open request.</param>
|
||||||
|
/// <param name="callOptions">gRPC call options.</param>
|
||||||
|
/// <returns>The session open reply.</returns>
|
||||||
Task<OpenSessionReply> OpenSessionAsync(
|
Task<OpenSessionReply> OpenSessionAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CallOptions callOptions);
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes an open gateway session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Session close request.</param>
|
||||||
|
/// <param name="callOptions">gRPC call options.</param>
|
||||||
|
/// <returns>The session close reply.</returns>
|
||||||
Task<CloseSessionReply> CloseSessionAsync(
|
Task<CloseSessionReply> CloseSessionAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CallOptions callOptions);
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes an MXAccess command on the session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The command request.</param>
|
||||||
|
/// <param name="callOptions">gRPC call options.</param>
|
||||||
|
/// <returns>The command reply.</returns>
|
||||||
Task<MxCommandReply> InvokeAsync(
|
Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CallOptions callOptions);
|
CallOptions callOptions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams events from the session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The stream events request.</param>
|
||||||
|
/// <param name="callOptions">gRPC call options.</param>
|
||||||
|
/// <returns>An async enumerable of events.</returns>
|
||||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CallOptions callOptions);
|
CallOptions callOptions);
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when an MXAccess command fails with a non-zero HResult or failing status.</summary>
|
||||||
public sealed class MxAccessException : MxGatewayCommandException
|
public sealed class MxAccessException : MxGatewayCommandException
|
||||||
{
|
{
|
||||||
|
/// <summary>Initializes a new instance with the given message, reply, and optional inner exception.</summary>
|
||||||
|
/// <param name="message">The error message describing the MXAccess failure.</param>
|
||||||
|
/// <param name="reply">The MxCommandReply containing the failure details (statuses, HResult, etc.).</param>
|
||||||
|
/// <param name="innerException">The underlying exception, if any.</param>
|
||||||
public MxAccessException(
|
public MxAccessException(
|
||||||
string message,
|
string message,
|
||||||
MxCommandReply reply,
|
MxCommandReply reply,
|
||||||
@@ -20,5 +25,6 @@ public sealed class MxAccessException : MxGatewayCommandException
|
|||||||
Reply = reply;
|
Reply = reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the underlying MxCommandReply containing full failure details.</summary>
|
||||||
public MxCommandReply Reply { get; }
|
public MxCommandReply Reply { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Extension methods for checking MxCommandReply success conditions.</summary>
|
||||||
public static class MxCommandReplyExtensions
|
public static class MxCommandReplyExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>Validates that the reply has a successful protocol status (Ok or MxAccessFailure), throwing a gateway exception if not.</summary>
|
||||||
|
/// <param name="reply">The command reply to check.</param>
|
||||||
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
|
public static MxCommandReply EnsureProtocolSuccess(this MxCommandReply reply)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(reply);
|
ArgumentNullException.ThrowIfNull(reply);
|
||||||
@@ -19,6 +22,8 @@ public static class MxCommandReplyExtensions
|
|||||||
throw CreateProtocolException(reply, code);
|
throw CreateProtocolException(reply, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Validates that the reply indicates MXAccess success (no HResult or status failures), throwing MxAccessException if not.</summary>
|
||||||
|
/// <param name="reply">The command reply to check.</param>
|
||||||
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
|
public static MxCommandReply EnsureMxAccessSuccess(this MxCommandReply reply)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(reply);
|
ArgumentNullException.ThrowIfNull(reply);
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when an API key is invalid, expired, or malformed.</summary>
|
||||||
public sealed class MxGatewayAuthenticationException : MxGatewayException
|
public sealed class MxGatewayAuthenticationException : MxGatewayException
|
||||||
{
|
{
|
||||||
|
/// <summary>Initializes a new instance with the given details.</summary>
|
||||||
|
/// <param name="message">The error message describing the authentication failure.</param>
|
||||||
|
/// <param name="sessionId">The session ID, if available.</param>
|
||||||
|
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||||
|
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||||
|
/// <param name="hResult">The HResult code, if available.</param>
|
||||||
|
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||||
|
/// <param name="innerException">The underlying exception, if any.</param>
|
||||||
public MxGatewayAuthenticationException(
|
public MxGatewayAuthenticationException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when the API key lacks required scopes for an operation.</summary>
|
||||||
public sealed class MxGatewayAuthorizationException : MxGatewayException
|
public sealed class MxGatewayAuthorizationException : MxGatewayException
|
||||||
{
|
{
|
||||||
|
/// <summary>Initializes a new instance with the given details.</summary>
|
||||||
|
/// <param name="message">The error message describing the authorization failure.</param>
|
||||||
|
/// <param name="sessionId">The session ID, if available.</param>
|
||||||
|
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||||
|
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||||
|
/// <param name="hResult">The HResult code, if available.</param>
|
||||||
|
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||||
|
/// <param name="innerException">The underlying exception, if any.</param>
|
||||||
public MxGatewayAuthorizationException(
|
public MxGatewayAuthorizationException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
private readonly ResiliencePipeline _safeUnaryRetryPipeline;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="MxGatewayClient"/> with given options and transport.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">Client configuration options.</param>
|
||||||
|
/// <param name="transport">Transport implementation for gateway communication.</param>
|
||||||
internal MxGatewayClient(
|
internal MxGatewayClient(
|
||||||
MxGatewayClientOptions options,
|
MxGatewayClientOptions options,
|
||||||
IMxGatewayClientTransport transport)
|
IMxGatewayClientTransport transport)
|
||||||
@@ -46,12 +51,23 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
Options.LoggerFactory?.CreateLogger<MxGatewayClient>());
|
Options.LoggerFactory?.CreateLogger<MxGatewayClient>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the client configuration options.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientOptions Options { get; }
|
public MxGatewayClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the underlying generated gRPC client.
|
||||||
|
/// </summary>
|
||||||
public MxAccessGateway.MxAccessGatewayClient RawClient =>
|
public MxAccessGateway.MxAccessGatewayClient RawClient =>
|
||||||
_transport.RawClient
|
_transport.RawClient
|
||||||
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
|
?? throw new InvalidOperationException("The raw generated gRPC client is not available for this client instance.");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new gateway client with the given options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">Client configuration options.</param>
|
||||||
|
/// <returns>A new gateway client instance.</returns>
|
||||||
public static MxGatewayClient Create(MxGatewayClientOptions options)
|
public static MxGatewayClient Create(MxGatewayClientOptions options)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(options);
|
ArgumentNullException.ThrowIfNull(options);
|
||||||
@@ -73,6 +89,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
new MxAccessGateway.MxAccessGatewayClient(channel)));
|
new MxAccessGateway.MxAccessGatewayClient(channel)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens a new gateway session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Session open request; defaults to empty request if null.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>A wrapped gateway session.</returns>
|
||||||
public async Task<MxGatewaySession> OpenSessionAsync(
|
public async Task<MxGatewaySession> OpenSessionAsync(
|
||||||
OpenSessionRequest? request = null,
|
OpenSessionRequest? request = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -85,6 +107,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return new MxGatewaySession(this, reply);
|
return new MxGatewaySession(this, reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Opens a new gateway session and returns the raw protobuf reply.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Session open request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The raw gateway session open reply.</returns>
|
||||||
public Task<OpenSessionReply> OpenSessionRawAsync(
|
public Task<OpenSessionReply> OpenSessionRawAsync(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -95,6 +123,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return _transport.OpenSessionAsync(request, CreateCallOptions(cancellationToken));
|
return _transport.OpenSessionAsync(request, CreateCallOptions(cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes an open gateway session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Session close request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The session close reply.</returns>
|
||||||
public Task<CloseSessionReply> CloseSessionRawAsync(
|
public Task<CloseSessionReply> CloseSessionRawAsync(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -107,6 +141,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes an MXAccess command on the open session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The command request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The command reply.</returns>
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -124,6 +164,12 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return _transport.InvokeAsync(request, CreateCallOptions(cancellationToken));
|
return _transport.InvokeAsync(request, CreateCallOptions(cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams events from the gateway session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The stream events request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>An async enumerable of events.</returns>
|
||||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -134,6 +180,9 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return _transport.StreamEventsAsync(request, CreateStreamCallOptions(cancellationToken));
|
return _transport.StreamEventsAsync(request, CreateStreamCallOptions(cancellationToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes the client and releases all resources.
|
||||||
|
/// </summary>
|
||||||
public ValueTask DisposeAsync()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@@ -146,16 +195,32 @@ public sealed class MxGatewayClient : IAsyncDisposable
|
|||||||
return ValueTask.CompletedTask;
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates gRPC call options with default timeout and authorization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the call.</param>
|
||||||
|
/// <returns>Configured call options.</returns>
|
||||||
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
|
internal CallOptions CreateCallOptions(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
|
return CreateCallOptions(cancellationToken, Options.DefaultCallTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates gRPC call options for streaming with stream timeout and authorization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the call.</param>
|
||||||
|
/// <returns>Configured call options.</returns>
|
||||||
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
|
internal CallOptions CreateStreamCallOptions(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
|
return CreateCallOptions(cancellationToken, Options.StreamTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates gRPC call options with specified timeout and authorization.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the call.</param>
|
||||||
|
/// <param name="timeout">Optional timeout duration; null means no timeout.</param>
|
||||||
|
/// <returns>Configured call options.</returns>
|
||||||
internal CallOptions CreateCallOptions(
|
internal CallOptions CreateCallOptions(
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
TimeSpan? timeout)
|
TimeSpan? timeout)
|
||||||
|
|||||||
@@ -7,26 +7,62 @@ namespace MxGateway.Client;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MxGatewayClientOptions
|
public sealed class MxGatewayClientOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the gateway endpoint URI (required).
|
||||||
|
/// </summary>
|
||||||
public required Uri Endpoint { get; init; }
|
public required Uri Endpoint { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the API key for gateway authentication (required).
|
||||||
|
/// </summary>
|
||||||
public required string ApiKey { get; init; }
|
public required string ApiKey { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether to use TLS for the gateway connection.
|
||||||
|
/// </summary>
|
||||||
public bool UseTls { get; init; }
|
public bool UseTls { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the path to a CA certificate file for custom certificate validation.
|
||||||
|
/// </summary>
|
||||||
public string? CaCertificatePath { get; init; }
|
public string? CaCertificatePath { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the server name override for SNI during TLS handshake.
|
||||||
|
/// </summary>
|
||||||
public string? ServerNameOverride { get; init; }
|
public string? ServerNameOverride { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the timeout for establishing connection to the gateway.
|
||||||
|
/// </summary>
|
||||||
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
|
public TimeSpan ConnectTimeout { get; init; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default timeout for unary gRPC calls.
|
||||||
|
/// </summary>
|
||||||
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
|
public TimeSpan DefaultCallTimeout { get; init; } = TimeSpan.FromSeconds(30);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the optional timeout for streaming gRPC calls.
|
||||||
|
/// </summary>
|
||||||
public TimeSpan? StreamTimeout { get; init; }
|
public TimeSpan? StreamTimeout { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the retry configuration for safe unary calls.
|
||||||
|
/// </summary>
|
||||||
public MxGatewayClientRetryOptions Retry { get; init; } = new();
|
public MxGatewayClientRetryOptions Retry { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the logger factory for diagnostic logging.
|
||||||
|
/// </summary>
|
||||||
public ILoggerFactory? LoggerFactory { get; init; }
|
public ILoggerFactory? LoggerFactory { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the client options for consistency and correctness.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="ArgumentNullException">Endpoint is null.</exception>
|
||||||
|
/// <exception cref="ArgumentException">Options are invalid or inconsistent.</exception>
|
||||||
|
/// <exception cref="ArgumentOutOfRangeException">Timeout values are not greater than zero.</exception>
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(Endpoint);
|
ArgumentNullException.ThrowIfNull(Endpoint);
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Configuration for automatic retry behavior on transient gRPC call failures.</summary>
|
||||||
public sealed class MxGatewayClientRetryOptions
|
public sealed class MxGatewayClientRetryOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets the maximum number of attempts (initial + retries); default is 2.</summary>
|
||||||
public int MaxAttempts { get; init; } = 2;
|
public int MaxAttempts { get; init; } = 2;
|
||||||
|
|
||||||
|
/// <summary>Gets the initial delay between retry attempts; default is 200 milliseconds.</summary>
|
||||||
public TimeSpan Delay { get; init; } = TimeSpan.FromMilliseconds(200);
|
public TimeSpan Delay { get; init; } = TimeSpan.FromMilliseconds(200);
|
||||||
|
|
||||||
|
/// <summary>Gets the maximum delay between retry attempts; default is 2 seconds.</summary>
|
||||||
public TimeSpan MaxDelay { get; init; } = TimeSpan.FromSeconds(2);
|
public TimeSpan MaxDelay { get; init; } = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
|
/// <summary>Gets a value indicating whether to add randomness to retry delays; default is true.</summary>
|
||||||
public bool UseJitter { get; init; } = true;
|
public bool UseJitter { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>Validates the retry options and throws if any constraint is violated.</summary>
|
||||||
public void Validate()
|
public void Validate()
|
||||||
{
|
{
|
||||||
if (MaxAttempts <= 0)
|
if (MaxAttempts <= 0)
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ using Polly.Retry;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Factory and helpers for exponential-backoff retry policies on transient gRPC failures.</summary>
|
||||||
internal static class MxGatewayClientRetryPolicy
|
internal static class MxGatewayClientRetryPolicy
|
||||||
{
|
{
|
||||||
|
/// <summary>Creates a Polly ResiliencePipeline that retries transient gRPC failures with exponential backoff.</summary>
|
||||||
|
/// <param name="options">Retry configuration (max attempts, delay bounds, jitter).</param>
|
||||||
|
/// <param name="logger">Optional logger for retry diagnostics.</param>
|
||||||
public static ResiliencePipeline Create(
|
public static ResiliencePipeline Create(
|
||||||
MxGatewayClientRetryOptions options,
|
MxGatewayClientRetryOptions options,
|
||||||
ILogger? logger)
|
ILogger? logger)
|
||||||
@@ -36,6 +40,8 @@ internal static class MxGatewayClientRetryPolicy
|
|||||||
.Build();
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns whether a command kind is eligible for automatic retry on transient failures.</summary>
|
||||||
|
/// <param name="kind">The command kind to check.</param>
|
||||||
public static bool IsRetryableCommand(MxCommandKind kind)
|
public static bool IsRetryableCommand(MxCommandKind kind)
|
||||||
{
|
{
|
||||||
return kind is MxCommandKind.Ping
|
return kind is MxCommandKind.Ping
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when a gateway command fails due to an unclassified protocol error.</summary>
|
||||||
public class MxGatewayCommandException : MxGatewayException
|
public class MxGatewayCommandException : MxGatewayException
|
||||||
{
|
{
|
||||||
|
/// <summary>Initializes a new instance with the given details.</summary>
|
||||||
|
/// <param name="message">The error message describing the command failure.</param>
|
||||||
|
/// <param name="sessionId">The session ID, if available.</param>
|
||||||
|
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||||
|
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||||
|
/// <param name="hResult">The HResult code, if available.</param>
|
||||||
|
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||||
|
/// <param name="innerException">The underlying exception, if any.</param>
|
||||||
public MxGatewayCommandException(
|
public MxGatewayCommandException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -2,20 +2,42 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when a gateway RPC call fails or returns an error status.
|
||||||
|
/// </summary>
|
||||||
public class MxGatewayException : Exception
|
public class MxGatewayException : Exception
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the MxGatewayException class with the specified message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Diagnostic message describing the failure.</param>
|
||||||
public MxGatewayException(string message)
|
public MxGatewayException(string message)
|
||||||
: base(message)
|
: base(message)
|
||||||
{
|
{
|
||||||
Statuses = [];
|
Statuses = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the MxGatewayException class with the specified message and inner exception.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Diagnostic message describing the failure.</param>
|
||||||
|
/// <param name="innerException">Underlying exception that caused this failure.</param>
|
||||||
public MxGatewayException(string message, Exception? innerException)
|
public MxGatewayException(string message, Exception? innerException)
|
||||||
: base(message, innerException)
|
: base(message, innerException)
|
||||||
{
|
{
|
||||||
Statuses = [];
|
Statuses = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the MxGatewayException class with full diagnostic information.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Diagnostic message describing the failure.</param>
|
||||||
|
/// <param name="sessionId">Session ID associated with the exception, if available.</param>
|
||||||
|
/// <param name="correlationId">Correlation ID associated with the exception, if available.</param>
|
||||||
|
/// <param name="protocolStatus">Protocol-level status returned by the gateway, if available.</param>
|
||||||
|
/// <param name="hResult">HRESULT code returned by the worker or MXAccess, if available.</param>
|
||||||
|
/// <param name="statuses">List of MXAccess status codes returned by the operation.</param>
|
||||||
|
/// <param name="innerException">Underlying exception that caused this failure.</param>
|
||||||
public MxGatewayException(
|
public MxGatewayException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId,
|
string? sessionId,
|
||||||
@@ -33,13 +55,28 @@ public class MxGatewayException : Exception
|
|||||||
Statuses = statuses;
|
Statuses = statuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the session ID associated with the exception, if available.
|
||||||
|
/// </summary>
|
||||||
public string? SessionId { get; }
|
public string? SessionId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the correlation ID associated with the exception, if available.
|
||||||
|
/// </summary>
|
||||||
public string? CorrelationId { get; }
|
public string? CorrelationId { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the protocol-level status returned by the gateway, if available.
|
||||||
|
/// </summary>
|
||||||
public ProtocolStatus? ProtocolStatus { get; }
|
public ProtocolStatus? ProtocolStatus { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the HRESULT code returned by the worker or MXAccess, if available.
|
||||||
|
/// </summary>
|
||||||
public int? HResultCode { get; }
|
public int? HResultCode { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of MXAccess status codes returned by the operation.
|
||||||
|
/// </summary>
|
||||||
public IReadOnlyList<MxStatusProxy> Statuses { get; }
|
public IReadOnlyList<MxStatusProxy> Statuses { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
private readonly SemaphoreSlim _closeLock = new(1, 1);
|
||||||
private CloseSessionReply? _closeReply;
|
private CloseSessionReply? _closeReply;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new session backed by the given MXAccess gateway client.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">The gateway client used for commands and events.</param>
|
||||||
|
/// <param name="openSessionReply">The server's session creation response.</param>
|
||||||
internal MxGatewaySession(
|
internal MxGatewaySession(
|
||||||
MxGatewayClient client,
|
MxGatewayClient client,
|
||||||
OpenSessionReply openSessionReply)
|
OpenSessionReply openSessionReply)
|
||||||
@@ -19,10 +24,21 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
OpenSessionReply = openSessionReply ?? throw new ArgumentNullException(nameof(openSessionReply));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The session ID assigned by the gateway.
|
||||||
|
/// </summary>
|
||||||
public string SessionId => OpenSessionReply.SessionId;
|
public string SessionId => OpenSessionReply.SessionId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server's session creation response containing metadata.
|
||||||
|
/// </summary>
|
||||||
public OpenSessionReply OpenSessionReply { get; }
|
public OpenSessionReply OpenSessionReply { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the session on the gateway. Idempotent.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The server's close-session reply.</returns>
|
||||||
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
|
public async Task<CloseSessionReply> CloseAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (_closeReply is not null)
|
if (_closeReply is not null)
|
||||||
@@ -50,6 +66,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a client with the MXAccess session, returning a ServerHandle.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clientName">Name to register.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The server handle assigned to the registered client.</returns>
|
||||||
public async Task<int> RegisterAsync(
|
public async Task<int> RegisterAsync(
|
||||||
string clientName,
|
string clientName,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -60,6 +82,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
|
return reply.Register?.ServerHandle ?? reply.ReturnValue.Int32Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a client with the MXAccess session without error checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clientName">Name to register.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<MxCommandReply> RegisterRawAsync(
|
public Task<MxCommandReply> RegisterRawAsync(
|
||||||
string clientName,
|
string clientName,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -75,6 +103,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item to the MXAccess session, returning an ItemHandle.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemDefinition">The item tag address.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The item handle assigned to the new item.</returns>
|
||||||
public async Task<int> AddItemAsync(
|
public async Task<int> AddItemAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
string itemDefinition,
|
string itemDefinition,
|
||||||
@@ -89,6 +124,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
return reply.AddItem?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item to the MXAccess session without error checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemDefinition">The item tag address.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<MxCommandReply> AddItemRawAsync(
|
public Task<MxCommandReply> AddItemRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
string itemDefinition,
|
string itemDefinition,
|
||||||
@@ -109,6 +151,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item with context to the MXAccess session, returning an ItemHandle.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemDefinition">The item tag address.</param>
|
||||||
|
/// <param name="itemContext">Additional context for the item.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The item handle assigned to the new item.</returns>
|
||||||
public async Task<int> AddItem2Async(
|
public async Task<int> AddItem2Async(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
string itemDefinition,
|
string itemDefinition,
|
||||||
@@ -125,6 +175,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
return reply.AddItem2?.ItemHandle ?? reply.ReturnValue.Int32Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item with context to the MXAccess session without error checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemDefinition">The item tag address.</param>
|
||||||
|
/// <param name="itemContext">Additional context for the item.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<MxCommandReply> AddItem2RawAsync(
|
public Task<MxCommandReply> AddItem2RawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
string itemDefinition,
|
string itemDefinition,
|
||||||
@@ -147,6 +205,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes to events for an item (advises in MXAccess terminology).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
public async Task AdviseAsync(
|
public async Task AdviseAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -157,6 +221,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Subscribes to events for an item without error checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<MxCommandReply> AdviseRawAsync(
|
public Task<MxCommandReply> AdviseRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -175,6 +246,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribes from events for an item (unadvises in MXAccess terminology).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
public async Task UnAdviseAsync(
|
public async Task UnAdviseAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -185,6 +262,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribes from events for an item without error checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<MxCommandReply> UnAdviseRawAsync(
|
public Task<MxCommandReply> UnAdviseRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -203,6 +287,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an item from the MXAccess session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
public async Task RemoveItemAsync(
|
public async Task RemoveItemAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -213,6 +303,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an item from the MXAccess session without error checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<MxCommandReply> RemoveItemRawAsync(
|
public Task<MxCommandReply> RemoveItemRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -231,6 +328,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds multiple items to the MXAccess session in a single command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="tagAddresses">The item tag addresses to add.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Per-item subscription results.</returns>
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
|
public async Task<IReadOnlyList<SubscribeResult>> AddItemBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<string> tagAddresses,
|
IReadOnlyList<string> tagAddresses,
|
||||||
@@ -253,6 +357,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.AddItemBulk?.Results.ToArray() ?? [];
|
return reply.AddItemBulk?.Results.ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advises multiple items in a single command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandles">The ItemHandles to advise.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Per-item subscription results.</returns>
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
|
public async Task<IReadOnlyList<SubscribeResult>> AdviseItemBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<int> itemHandles,
|
IReadOnlyList<int> itemHandles,
|
||||||
@@ -275,6 +386,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
|
return reply.AdviseItemBulk?.Results.ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes multiple items in a single command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandles">The ItemHandles to remove.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Per-item subscription results.</returns>
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
|
public async Task<IReadOnlyList<SubscribeResult>> RemoveItemBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<int> itemHandles,
|
IReadOnlyList<int> itemHandles,
|
||||||
@@ -297,6 +415,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
|
return reply.RemoveItemBulk?.Results.ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unadvises multiple items in a single command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandles">The ItemHandles to unadvise.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Per-item subscription results.</returns>
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
|
public async Task<IReadOnlyList<SubscribeResult>> UnAdviseItemBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<int> itemHandles,
|
IReadOnlyList<int> itemHandles,
|
||||||
@@ -319,6 +444,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
|
return reply.UnAdviseItemBulk?.Results.ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds and advises multiple items in a single command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="tagAddresses">The item tag addresses to add and advise.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Per-item subscription results.</returns>
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
public async Task<IReadOnlyList<SubscribeResult>> SubscribeBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<string> tagAddresses,
|
IReadOnlyList<string> tagAddresses,
|
||||||
@@ -341,6 +473,13 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.SubscribeBulk?.Results.ToArray() ?? [];
|
return reply.SubscribeBulk?.Results.ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unadvises and removes multiple items in a single command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandles">The ItemHandles to unsubscribe.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Per-item subscription results.</returns>
|
||||||
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
|
public async Task<IReadOnlyList<SubscribeResult>> UnsubscribeBulkAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
IReadOnlyList<int> itemHandles,
|
IReadOnlyList<int> itemHandles,
|
||||||
@@ -363,6 +502,14 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
|
return reply.UnsubscribeBulk?.Results.ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a value to an item on the MXAccess server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="value">The value to write.</param>
|
||||||
|
/// <param name="userId">User ID context for the write.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
public async Task WriteAsync(
|
public async Task WriteAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -375,6 +522,15 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a value to an item on the MXAccess server without error checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="value">The value to write.</param>
|
||||||
|
/// <param name="userId">User ID context for the write.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<MxCommandReply> WriteRawAsync(
|
public Task<MxCommandReply> WriteRawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -399,6 +555,15 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a value and timestamp to an item on the MXAccess server.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="value">The value to write.</param>
|
||||||
|
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
||||||
|
/// <param name="userId">User ID context for the write.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
public async Task Write2Async(
|
public async Task Write2Async(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -418,6 +583,16 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
reply.EnsureProtocolSuccess().EnsureMxAccessSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes a value and timestamp to an item on the MXAccess server without error checking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serverHandle">The ServerHandle from register.</param>
|
||||||
|
/// <param name="itemHandle">The ItemHandle from add-item.</param>
|
||||||
|
/// <param name="value">The value to write.</param>
|
||||||
|
/// <param name="timestampValue">The timestamp to write with the value.</param>
|
||||||
|
/// <param name="userId">User ID context for the write.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<MxCommandReply> Write2RawAsync(
|
public Task<MxCommandReply> Write2RawAsync(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle,
|
int itemHandle,
|
||||||
@@ -445,6 +620,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invokes an MXAccess command on this session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The command request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The raw server reply.</returns>
|
||||||
public Task<MxCommandReply> InvokeAsync(
|
public Task<MxCommandReply> InvokeAsync(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -453,6 +634,12 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
return _client.InvokeAsync(request, cancellationToken);
|
return _client.InvokeAsync(request, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams events from the worker for this session, optionally starting after a given sequence number.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="afterWorkerSequence">The sequence number to stream from. Defaults to 0.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>An async enumerable of events.</returns>
|
||||||
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
ulong afterWorkerSequence = 0,
|
ulong afterWorkerSequence = 0,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -466,6 +653,9 @@ public sealed class MxGatewaySession : IAsyncDisposable
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Closes the session and releases resources.
|
||||||
|
/// </summary>
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await CloseAsync().ConfigureAwait(false);
|
await CloseAsync().ConfigureAwait(false);
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when a session is not found, not ready, or invalid.</summary>
|
||||||
public sealed class MxGatewaySessionException : MxGatewayException
|
public sealed class MxGatewaySessionException : MxGatewayException
|
||||||
{
|
{
|
||||||
|
/// <summary>Initializes a new instance with the given details.</summary>
|
||||||
|
/// <param name="message">The error message describing the session failure.</param>
|
||||||
|
/// <param name="sessionId">The session ID, if available.</param>
|
||||||
|
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||||
|
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||||
|
/// <param name="hResult">The HResult code, if available.</param>
|
||||||
|
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||||
|
/// <param name="innerException">The underlying exception, if any.</param>
|
||||||
public MxGatewaySessionException(
|
public MxGatewaySessionException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -2,8 +2,17 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Exception thrown when the worker process is unavailable or fails to process a command.</summary>
|
||||||
public sealed class MxGatewayWorkerException : MxGatewayException
|
public sealed class MxGatewayWorkerException : MxGatewayException
|
||||||
{
|
{
|
||||||
|
/// <summary>Initializes a new instance with the given details.</summary>
|
||||||
|
/// <param name="message">The error message describing the worker failure.</param>
|
||||||
|
/// <param name="sessionId">The session ID, if available.</param>
|
||||||
|
/// <param name="correlationId">The correlation ID for tracing, if available.</param>
|
||||||
|
/// <param name="protocolStatus">The protocol status details, if available.</param>
|
||||||
|
/// <param name="hResult">The HResult code, if available.</param>
|
||||||
|
/// <param name="statuses">The MXAccess statuses, if available.</param>
|
||||||
|
/// <param name="innerException">The underlying exception, if any.</param>
|
||||||
public MxGatewayWorkerException(
|
public MxGatewayWorkerException(
|
||||||
string message,
|
string message,
|
||||||
string? sessionId = null,
|
string? sessionId = null,
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Client;
|
namespace MxGateway.Client;
|
||||||
|
|
||||||
|
/// <summary>Extension methods for MxStatusProxy values.</summary>
|
||||||
public static class MxStatusProxyExtensions
|
public static class MxStatusProxyExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>Returns whether the status indicates success (success flag set and category is Ok).</summary>
|
||||||
|
/// <param name="status">The status to check.</param>
|
||||||
public static bool IsSuccess(this MxStatusProxy status)
|
public static bool IsSuccess(this MxStatusProxy status)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(status);
|
ArgumentNullException.ThrowIfNull(status);
|
||||||
@@ -12,6 +15,8 @@ public static class MxStatusProxyExtensions
|
|||||||
&& status.Category is MxStatusCategory.Ok;
|
&& status.Category is MxStatusCategory.Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns a formatted summary of the status for diagnostic output.</summary>
|
||||||
|
/// <param name="status">The status to summarize.</param>
|
||||||
public static string ToDiagnosticSummary(this MxStatusProxy status)
|
public static string ToDiagnosticSummary(this MxStatusProxy status)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(status);
|
ArgumentNullException.ThrowIfNull(status);
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ namespace MxGateway.Client;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class MxValueExtensions
|
public static class MxValueExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a boolean value to an MxValue with MxDataType.Boolean.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Scalar boolean value to wrap.</param>
|
||||||
public static MxValue ToMxValue(this bool value)
|
public static MxValue ToMxValue(this bool value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -20,6 +24,10 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a 32-bit integer value to an MxValue with MxDataType.Integer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">32-bit integer value to wrap.</param>
|
||||||
public static MxValue ToMxValue(this int value)
|
public static MxValue ToMxValue(this int value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -30,6 +38,10 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a 64-bit integer value to an MxValue with MxDataType.Integer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">64-bit integer value to wrap.</param>
|
||||||
public static MxValue ToMxValue(this long value)
|
public static MxValue ToMxValue(this long value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -40,6 +52,10 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a single-precision floating-point value to an MxValue with MxDataType.Float.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Single-precision floating-point value to wrap.</param>
|
||||||
public static MxValue ToMxValue(this float value)
|
public static MxValue ToMxValue(this float value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -50,6 +66,10 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a double-precision floating-point value to an MxValue with MxDataType.Double.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Double-precision floating-point value to wrap.</param>
|
||||||
public static MxValue ToMxValue(this double value)
|
public static MxValue ToMxValue(this double value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -60,6 +80,10 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a string value to an MxValue with MxDataType.String.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">String value to wrap.</param>
|
||||||
public static MxValue ToMxValue(this string value)
|
public static MxValue ToMxValue(this string value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
@@ -72,6 +96,10 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a DateTimeOffset value to an MxValue with MxDataType.Time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">DateTimeOffset value to wrap.</param>
|
||||||
public static MxValue ToMxValue(this DateTimeOffset value)
|
public static MxValue ToMxValue(this DateTimeOffset value)
|
||||||
{
|
{
|
||||||
return new MxValue
|
return new MxValue
|
||||||
@@ -82,6 +110,10 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a DateTime value to an MxValue with MxDataType.Time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">DateTime value to wrap.</param>
|
||||||
public static MxValue ToMxValue(this DateTime value)
|
public static MxValue ToMxValue(this DateTime value)
|
||||||
{
|
{
|
||||||
return new DateTimeOffset(
|
return new DateTimeOffset(
|
||||||
@@ -91,6 +123,10 @@ public static class MxValueExtensions
|
|||||||
.ToMxValue();
|
.ToMxValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a boolean array to an MxValue with MxDataType.Boolean.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">Array of boolean values to wrap.</param>
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<bool> values)
|
public static MxValue ToMxValue(this IReadOnlyList<bool> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -105,6 +141,10 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a 32-bit integer array to an MxValue with MxDataType.Integer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">Array of 32-bit integer values to wrap.</param>
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<int> values)
|
public static MxValue ToMxValue(this IReadOnlyList<int> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -119,6 +159,10 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a 64-bit integer array to an MxValue with MxDataType.Integer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">Array of 64-bit integer values to wrap.</param>
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<long> values)
|
public static MxValue ToMxValue(this IReadOnlyList<long> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -133,6 +177,10 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a single-precision floating-point array to an MxValue with MxDataType.Float.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">Array of single-precision floating-point values to wrap.</param>
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<float> values)
|
public static MxValue ToMxValue(this IReadOnlyList<float> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -147,6 +195,10 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a double-precision floating-point array to an MxValue with MxDataType.Double.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">Array of double-precision floating-point values to wrap.</param>
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<double> values)
|
public static MxValue ToMxValue(this IReadOnlyList<double> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -161,6 +213,10 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a string array to an MxValue with MxDataType.String.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">Array of string values to wrap.</param>
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<string> values)
|
public static MxValue ToMxValue(this IReadOnlyList<string> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -175,6 +231,10 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a DateTimeOffset array to an MxValue with MxDataType.Time.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="values">Array of DateTimeOffset values to wrap.</param>
|
||||||
public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
|
public static MxValue ToMxValue(this IReadOnlyList<DateTimeOffset> values)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(values);
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
@@ -189,6 +249,10 @@ public static class MxValueExtensions
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the projection kind (field name) of the given MxValue's current oneof value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The MxValue whose oneof projection kind is returned.</param>
|
||||||
public static string GetProjectionKind(this MxValue value)
|
public static string GetProjectionKind(this MxValue value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
@@ -208,6 +272,10 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an MxValue to a CLR object; returns the boxed value or null for null MxValues.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The MxValue to convert.</param>
|
||||||
public static object? ToClrValue(this MxValue value)
|
public static object? ToClrValue(this MxValue value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
@@ -227,6 +295,10 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an MxArray to a CLR array; returns null if the array does not have a known element type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="array">The MxArray to convert.</param>
|
||||||
public static object? ToClrArrayValue(this MxArray array)
|
public static object? ToClrArrayValue(this MxArray array)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(array);
|
ArgumentNullException.ThrowIfNull(array);
|
||||||
@@ -249,6 +321,13 @@ public static class MxValueExtensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an MxValue with MxDataType.Unknown from raw byte data, variant type, and diagnostic info.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Raw byte data representing the value.</param>
|
||||||
|
/// <param name="variantType">Variant type string (e.g., "VT_BSTR").</param>
|
||||||
|
/// <param name="rawDiagnostic">Diagnostic string describing the raw value.</param>
|
||||||
|
/// <param name="rawDataType">Optional MXAccess data type override.</param>
|
||||||
public static MxValue ToRawMxValue(
|
public static MxValue ToRawMxValue(
|
||||||
byte[] value,
|
byte[] value,
|
||||||
string variantType,
|
string variantType,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace MxGateway.IntegrationTests.Galaxy;
|
|||||||
|
|
||||||
public sealed class GalaxyRepositoryLiveTests
|
public sealed class GalaxyRepositoryLiveTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that the Galaxy Repository can establish a live connection to the ZB database.</summary>
|
||||||
[LiveGalaxyRepositoryFact]
|
[LiveGalaxyRepositoryFact]
|
||||||
[Trait("Category", "LiveGalaxy")]
|
[Trait("Category", "LiveGalaxy")]
|
||||||
public async Task TestConnection_AgainstZb_Succeeds()
|
public async Task TestConnection_AgainstZb_Succeeds()
|
||||||
@@ -15,6 +16,7 @@ public sealed class GalaxyRepositoryLiveTests
|
|||||||
Assert.True(ok, "TestConnectionAsync should return true against the ZB database.");
|
Assert.True(ok, "TestConnectionAsync should return true against the ZB database.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that the last deploy time can be retrieved from the ZB database.</summary>
|
||||||
[LiveGalaxyRepositoryFact]
|
[LiveGalaxyRepositoryFact]
|
||||||
[Trait("Category", "LiveGalaxy")]
|
[Trait("Category", "LiveGalaxy")]
|
||||||
public async Task GetLastDeployTime_AgainstZb_ReturnsTimestamp()
|
public async Task GetLastDeployTime_AgainstZb_ReturnsTimestamp()
|
||||||
@@ -26,6 +28,7 @@ public sealed class GalaxyRepositoryLiveTests
|
|||||||
Assert.NotNull(lastDeploy);
|
Assert.NotNull(lastDeploy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that the hierarchy can be retrieved from the ZB database.</summary>
|
||||||
[LiveGalaxyRepositoryFact]
|
[LiveGalaxyRepositoryFact]
|
||||||
[Trait("Category", "LiveGalaxy")]
|
[Trait("Category", "LiveGalaxy")]
|
||||||
public async Task GetHierarchy_AgainstZb_ReturnsObjects()
|
public async Task GetHierarchy_AgainstZb_ReturnsObjects()
|
||||||
@@ -43,6 +46,7 @@ public sealed class GalaxyRepositoryLiveTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that object attributes can be retrieved from the ZB database.</summary>
|
||||||
[LiveGalaxyRepositoryFact]
|
[LiveGalaxyRepositoryFact]
|
||||||
[Trait("Category", "LiveGalaxy")]
|
[Trait("Category", "LiveGalaxy")]
|
||||||
public async Task GetAttributes_AgainstZb_ReturnsAtLeastOneAttribute()
|
public async Task GetAttributes_AgainstZb_ReturnsAtLeastOneAttribute()
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
namespace MxGateway.IntegrationTests.Galaxy;
|
namespace MxGateway.IntegrationTests.Galaxy;
|
||||||
|
|
||||||
|
/// <summary>Fact attribute that skips tests unless live Galaxy Repository tests are explicitly enabled.</summary>
|
||||||
public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute
|
public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute
|
||||||
{
|
{
|
||||||
|
/// <summary>Environment variable name to enable live Galaxy Repository tests.</summary>
|
||||||
public const string EnableVariableName = "MXGATEWAY_RUN_LIVE_GALAXY_TESTS";
|
public const string EnableVariableName = "MXGATEWAY_RUN_LIVE_GALAXY_TESTS";
|
||||||
|
/// <summary>Environment variable name for the Galaxy Repository connection string.</summary>
|
||||||
public const string ConnectionStringVariableName = "MXGATEWAY_LIVE_GALAXY_CONN";
|
public const string ConnectionStringVariableName = "MXGATEWAY_LIVE_GALAXY_CONN";
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the LiveGalaxyRepositoryFactAttribute class.</summary>
|
||||||
public LiveGalaxyRepositoryFactAttribute()
|
public LiveGalaxyRepositoryFactAttribute()
|
||||||
{
|
{
|
||||||
if (!Enabled)
|
if (!Enabled)
|
||||||
@@ -13,12 +17,14 @@ public sealed class LiveGalaxyRepositoryFactAttribute : FactAttribute
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets a value indicating whether live Galaxy Repository tests are enabled.</summary>
|
||||||
public static bool Enabled =>
|
public static bool Enabled =>
|
||||||
string.Equals(
|
string.Equals(
|
||||||
Environment.GetEnvironmentVariable(EnableVariableName),
|
Environment.GetEnvironmentVariable(EnableVariableName),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Gets the Galaxy Repository connection string from environment or default.</summary>
|
||||||
public static string ConnectionString =>
|
public static string ConnectionString =>
|
||||||
Environment.GetEnvironmentVariable(ConnectionStringVariableName)
|
Environment.GetEnvironmentVariable(ConnectionStringVariableName)
|
||||||
?? "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;";
|
?? "Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;";
|
||||||
|
|||||||
@@ -8,27 +8,33 @@ public static class IntegrationTestEnvironment
|
|||||||
public const string LiveMxAccessClientNameVariableName = "MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME";
|
public const string LiveMxAccessClientNameVariableName = "MXGATEWAY_LIVE_MXACCESS_CLIENT_NAME";
|
||||||
public const string LiveMxAccessEventTimeoutSecondsVariableName = "MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS";
|
public const string LiveMxAccessEventTimeoutSecondsVariableName = "MXGATEWAY_LIVE_MXACCESS_EVENT_TIMEOUT_SECONDS";
|
||||||
|
|
||||||
|
/// <summary>Gets whether live MXAccess tests are enabled.</summary>
|
||||||
public static bool LiveMxAccessTestsEnabled =>
|
public static bool LiveMxAccessTestsEnabled =>
|
||||||
string.Equals(
|
string.Equals(
|
||||||
Environment.GetEnvironmentVariable(LiveMxAccessVariableName),
|
Environment.GetEnvironmentVariable(LiveMxAccessVariableName),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Gets the MXAccess item name for live tests.</summary>
|
||||||
public static string LiveMxAccessItem =>
|
public static string LiveMxAccessItem =>
|
||||||
GetOptionalEnvironmentVariable(
|
GetOptionalEnvironmentVariable(
|
||||||
LiveMxAccessItemVariableName,
|
LiveMxAccessItemVariableName,
|
||||||
"TestChildObject.TestInt");
|
"TestChildObject.TestInt");
|
||||||
|
|
||||||
|
/// <summary>Gets the client name for live tests.</summary>
|
||||||
public static string LiveMxAccessClientName =>
|
public static string LiveMxAccessClientName =>
|
||||||
GetOptionalEnvironmentVariable(
|
GetOptionalEnvironmentVariable(
|
||||||
LiveMxAccessClientNameVariableName,
|
LiveMxAccessClientNameVariableName,
|
||||||
"MxGateway.IntegrationTests");
|
"MxGateway.IntegrationTests");
|
||||||
|
|
||||||
|
/// <summary>Gets the timeout for waiting on events in live tests.</summary>
|
||||||
public static TimeSpan LiveMxAccessEventTimeout =>
|
public static TimeSpan LiveMxAccessEventTimeout =>
|
||||||
TimeSpan.FromSeconds(GetPositiveIntegerEnvironmentVariable(
|
TimeSpan.FromSeconds(GetPositiveIntegerEnvironmentVariable(
|
||||||
LiveMxAccessEventTimeoutSecondsVariableName,
|
LiveMxAccessEventTimeoutSecondsVariableName,
|
||||||
defaultValue: 15));
|
defaultValue: 15));
|
||||||
|
|
||||||
|
/// <summary>Resolves the path to the worker executable for live tests.</summary>
|
||||||
|
/// <returns>Path to MxGateway.Worker.exe.</returns>
|
||||||
public static string ResolveLiveMxAccessWorkerExecutablePath()
|
public static string ResolveLiveMxAccessWorkerExecutablePath()
|
||||||
{
|
{
|
||||||
string? configuredPath = Environment.GetEnvironmentVariable(LiveMxAccessWorkerExecutableVariableName);
|
string? configuredPath = Environment.GetEnvironmentVariable(LiveMxAccessWorkerExecutableVariableName);
|
||||||
@@ -74,6 +80,9 @@ public static class IntegrationTestEnvironment
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Resolves the root directory of the repository by searching for .git and src directories.</summary>
|
||||||
|
/// <param name="startDirectory">Starting directory to search from.</param>
|
||||||
|
/// <returns>The repository root path, or the start directory if not found.</returns>
|
||||||
internal static string ResolveRepositoryRoot(string startDirectory)
|
internal static string ResolveRepositoryRoot(string startDirectory)
|
||||||
{
|
{
|
||||||
DirectoryInfo? directory = new(startDirectory);
|
DirectoryInfo? directory = new(startDirectory);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ namespace MxGateway.IntegrationTests;
|
|||||||
|
|
||||||
public sealed class IntegrationTestEnvironmentTests
|
public sealed class IntegrationTestEnvironmentTests
|
||||||
{
|
{
|
||||||
|
/// <summary>Verifies that live MXAccess tests use correct environment variable name.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LiveMxAccessTests_AreOptInByEnvironmentVariable()
|
public void LiveMxAccessTests_AreOptInByEnvironmentVariable()
|
||||||
{
|
{
|
||||||
@@ -10,6 +11,7 @@ public sealed class IntegrationTestEnvironmentTests
|
|||||||
IntegrationTestEnvironment.LiveMxAccessVariableName);
|
IntegrationTestEnvironment.LiveMxAccessVariableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that worker executable uses correct environment variable name.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void LiveMxAccessWorkerExecutable_UsesDocumentedEnvironmentVariable()
|
public void LiveMxAccessWorkerExecutable_UsesDocumentedEnvironmentVariable()
|
||||||
{
|
{
|
||||||
@@ -18,6 +20,7 @@ public sealed class IntegrationTestEnvironmentTests
|
|||||||
IntegrationTestEnvironment.LiveMxAccessWorkerExecutableVariableName);
|
IntegrationTestEnvironment.LiveMxAccessWorkerExecutableVariableName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that repository root resolution accepts git worktree files.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResolveRepositoryRoot_AcceptsGitWorktreeFile()
|
public void ResolveRepositoryRoot_AcceptsGitWorktreeFile()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
namespace MxGateway.IntegrationTests;
|
namespace MxGateway.IntegrationTests;
|
||||||
|
|
||||||
|
/// <summary>Marks an xUnit test as requiring installed MXAccess COM and live provider state.</summary>
|
||||||
public sealed class LiveMxAccessFactAttribute : FactAttribute
|
public sealed class LiveMxAccessFactAttribute : FactAttribute
|
||||||
{
|
{
|
||||||
|
/// <summary>Initializes the attribute, skipping the test unless the integration test environment variable is set.</summary>
|
||||||
public LiveMxAccessFactAttribute()
|
public LiveMxAccessFactAttribute()
|
||||||
{
|
{
|
||||||
if (!IntegrationTestEnvironment.LiveMxAccessTestsEnabled)
|
if (!IntegrationTestEnvironment.LiveMxAccessTestsEnabled)
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(15);
|
private static readonly TimeSpan CommandTimeout = TimeSpan.FromSeconds(15);
|
||||||
private static readonly TimeSpan StreamShutdownTimeout = TimeSpan.FromSeconds(10);
|
private static readonly TimeSpan StreamShutdownTimeout = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies that a gateway session can register, add item, advise, and stream events from live MXAccess.
|
||||||
|
/// </summary>
|
||||||
[LiveMxAccessFact]
|
[LiveMxAccessFact]
|
||||||
[Trait("Category", "LiveMxAccess")]
|
[Trait("Category", "LiveMxAccess")]
|
||||||
public async Task GatewaySession_WithLiveWorker_RegistersAdvisesStreamsDataAndCloses()
|
public async Task GatewaySession_WithLiveWorker_RegistersAdvisesStreamsDataAndCloses()
|
||||||
@@ -208,12 +211,21 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
$"Event value_type={dataChange.Value?.DataType} raw_status={dataChange.RawStatus}");
|
$"Event value_type={dataChange.Value?.DataType} raw_status={dataChange.RawStatus}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test fixture that assembles the gateway service with a worker process factory for live MXAccess testing.
|
||||||
|
/// </summary>
|
||||||
private sealed class GatewayServiceFixture : IAsyncDisposable
|
private sealed class GatewayServiceFixture : IAsyncDisposable
|
||||||
{
|
{
|
||||||
private readonly GatewayMetrics _metrics = new();
|
private readonly GatewayMetrics _metrics = new();
|
||||||
private readonly SessionRegistry _registry = new();
|
private readonly SessionRegistry _registry = new();
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the fixture with worker executable path, factory, and test output helper.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="workerExecutablePath">Path to the worker process executable.</param>
|
||||||
|
/// <param name="processFactory">Factory for creating worker processes.</param>
|
||||||
|
/// <param name="output">Test output helper for logging.</param>
|
||||||
public GatewayServiceFixture(
|
public GatewayServiceFixture(
|
||||||
string workerExecutablePath,
|
string workerExecutablePath,
|
||||||
IWorkerProcessFactory processFactory,
|
IWorkerProcessFactory processFactory,
|
||||||
@@ -255,8 +267,14 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
_loggerFactory.CreateLogger<MxAccessGatewayService>());
|
_loggerFactory.CreateLogger<MxAccessGatewayService>());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The assembled gateway service instance.
|
||||||
|
/// </summary>
|
||||||
public MxAccessGatewayService Service { get; }
|
public MxAccessGatewayService Service { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes the fixture resources and closes all sessions.
|
||||||
|
/// </summary>
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
foreach (GatewaySession session in _registry.Snapshot())
|
foreach (GatewaySession session in _registry.Snapshot())
|
||||||
@@ -295,12 +313,18 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gathers messages written to a server stream for test inspection.
|
||||||
|
/// </summary>
|
||||||
private sealed class RecordingServerStreamWriter<T> : IServerStreamWriter<T>
|
private sealed class RecordingServerStreamWriter<T> : IServerStreamWriter<T>
|
||||||
{
|
{
|
||||||
private readonly object syncRoot = new();
|
private readonly object syncRoot = new();
|
||||||
private readonly TaskCompletionSource<T> firstMessage = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
private readonly TaskCompletionSource<T> firstMessage = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
private readonly List<T> messages = [];
|
private readonly List<T> messages = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// All messages that have been written to the stream.
|
||||||
|
/// </summary>
|
||||||
public IReadOnlyList<T> Messages
|
public IReadOnlyList<T> Messages
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -312,8 +336,15 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Inherited write options.
|
||||||
|
/// </summary>
|
||||||
public WriteOptions? WriteOptions { get; set; }
|
public WriteOptions? WriteOptions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records the message and completes the first-message task.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message to write.</param>
|
||||||
public Task WriteAsync(T message)
|
public Task WriteAsync(T message)
|
||||||
{
|
{
|
||||||
lock (syncRoot)
|
lock (syncRoot)
|
||||||
@@ -325,12 +356,20 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Waits for the first message up to the specified timeout.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeout">The maximum time to wait.</param>
|
||||||
|
/// <returns>The first message written to the stream.</returns>
|
||||||
public async Task<T> WaitForFirstMessageAsync(TimeSpan timeout)
|
public async Task<T> WaitForFirstMessageAsync(TimeSpan timeout)
|
||||||
{
|
{
|
||||||
return await firstMessage.Task.WaitAsync(timeout).ConfigureAwait(false);
|
return await firstMessage.Task.WaitAsync(timeout).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mock server call context for testing gRPC calls.
|
||||||
|
/// </summary>
|
||||||
private sealed class TestServerCallContext(CancellationToken cancellationToken = default) : ServerCallContext
|
private sealed class TestServerCallContext(CancellationToken cancellationToken = default) : ServerCallContext
|
||||||
{
|
{
|
||||||
private readonly Metadata requestHeaders = [];
|
private readonly Metadata requestHeaders = [];
|
||||||
@@ -339,43 +378,56 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
private Status status;
|
private Status status;
|
||||||
private WriteOptions? writeOptions;
|
private WriteOptions? writeOptions;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override string MethodCore => "/mxaccess_gateway.v1.MxAccessGateway/Test";
|
protected override string MethodCore => "/mxaccess_gateway.v1.MxAccessGateway/Test";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override string HostCore => "localhost";
|
protected override string HostCore => "localhost";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override string PeerCore => "ipv4:127.0.0.1:5000";
|
protected override string PeerCore => "ipv4:127.0.0.1:5000";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
|
protected override DateTime DeadlineCore => DateTime.UtcNow.AddMinutes(1);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Metadata RequestHeadersCore => requestHeaders;
|
protected override Metadata RequestHeadersCore => requestHeaders;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override CancellationToken CancellationTokenCore => cancellationToken;
|
protected override CancellationToken CancellationTokenCore => cancellationToken;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Metadata ResponseTrailersCore => responseTrailers;
|
protected override Metadata ResponseTrailersCore => responseTrailers;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Status StatusCore
|
protected override Status StatusCore
|
||||||
{
|
{
|
||||||
get => status;
|
get => status;
|
||||||
set => status = value;
|
set => status = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override WriteOptions? WriteOptionsCore
|
protected override WriteOptions? WriteOptionsCore
|
||||||
{
|
{
|
||||||
get => writeOptions;
|
get => writeOptions;
|
||||||
set => writeOptions = value;
|
set => writeOptions = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override AuthContext AuthContextCore { get; } = new(
|
protected override AuthContext AuthContextCore { get; } = new(
|
||||||
string.Empty,
|
string.Empty,
|
||||||
new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));
|
new Dictionary<string, List<AuthProperty>>(StringComparer.Ordinal));
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override IDictionary<object, object> UserStateCore => userState;
|
protected override IDictionary<object, object> UserStateCore => userState;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
|
protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders)
|
||||||
{
|
{
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override ContextPropagationToken CreatePropagationTokenCore(
|
protected override ContextPropagationToken CreatePropagationTokenCore(
|
||||||
ContextPropagationOptions? options)
|
ContextPropagationOptions? options)
|
||||||
{
|
{
|
||||||
@@ -383,10 +435,14 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory that launches worker processes and records their outputs for testing.
|
||||||
|
/// </summary>
|
||||||
private sealed class TestWorkerProcessFactory(ITestOutputHelper output) : IWorkerProcessFactory
|
private sealed class TestWorkerProcessFactory(ITestOutputHelper output) : IWorkerProcessFactory
|
||||||
{
|
{
|
||||||
private readonly ConcurrentBag<TestWorkerProcess> processes = [];
|
private readonly ConcurrentBag<TestWorkerProcess> processes = [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IWorkerProcess Start(ProcessStartInfo startInfo)
|
public IWorkerProcess Start(ProcessStartInfo startInfo)
|
||||||
{
|
{
|
||||||
startInfo.RedirectStandardError = true;
|
startInfo.RedirectStandardError = true;
|
||||||
@@ -418,6 +474,7 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
return workerProcess;
|
return workerProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task WaitForProcessesAsync(TimeSpan timeout)
|
public async Task WaitForProcessesAsync(TimeSpan timeout)
|
||||||
{
|
{
|
||||||
foreach (TestWorkerProcess process in processes)
|
foreach (TestWorkerProcess process in processes)
|
||||||
@@ -445,57 +502,77 @@ public sealed class WorkerLiveMxAccessSmokeTests(ITestOutputHelper output)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adapter wrapping a System.Diagnostics.Process as IWorkerProcess for testing.
|
||||||
|
/// </summary>
|
||||||
private sealed class TestWorkerProcess(Process process) : IWorkerProcess
|
private sealed class TestWorkerProcess(Process process) : IWorkerProcess
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public int Id => process.Id;
|
public int Id => process.Id;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public bool HasExited => process.HasExited;
|
public bool HasExited => process.HasExited;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public int? ExitCode => process.HasExited ? process.ExitCode : null;
|
public int? ExitCode => process.HasExited ? process.ExitCode : null;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async ValueTask WaitForExitAsync(CancellationToken cancellationToken)
|
public async ValueTask WaitForExitAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Kill(bool entireProcessTree)
|
public void Kill(bool entireProcessTree)
|
||||||
{
|
{
|
||||||
process.Kill(entireProcessTree);
|
process.Kill(entireProcessTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
process.Dispose();
|
process.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logger provider that writes all output to the test output helper.
|
||||||
|
/// </summary>
|
||||||
private sealed class TestOutputLoggerProvider(ITestOutputHelper output) : ILoggerProvider
|
private sealed class TestOutputLoggerProvider(ITestOutputHelper output) : ILoggerProvider
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public ILogger CreateLogger(string categoryName)
|
public ILogger CreateLogger(string categoryName)
|
||||||
{
|
{
|
||||||
return new TestOutputLogger(output, categoryName);
|
return new TestOutputLogger(output, categoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logger that writes messages to the test output helper.
|
||||||
|
/// </summary>
|
||||||
private sealed class TestOutputLogger(
|
private sealed class TestOutputLogger(
|
||||||
ITestOutputHelper output,
|
ITestOutputHelper output,
|
||||||
string categoryName) : ILogger
|
string categoryName) : ILogger
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public IDisposable? BeginScope<TState>(TState state)
|
public IDisposable? BeginScope<TState>(TState state)
|
||||||
where TState : notnull
|
where TState : notnull
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public bool IsEnabled(LogLevel logLevel)
|
public bool IsEnabled(LogLevel logLevel)
|
||||||
{
|
{
|
||||||
return logLevel >= LogLevel.Information;
|
return logLevel >= LogLevel.Information;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Log<TState>(
|
public void Log<TState>(
|
||||||
LogLevel logLevel,
|
LogLevel logLevel,
|
||||||
EventId eventId,
|
EventId eventId,
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ namespace MxGateway.Server.Configuration;
|
|||||||
|
|
||||||
public sealed class AuthenticationOptions
|
public sealed class AuthenticationOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets the authentication mode.</summary>
|
||||||
public AuthenticationMode Mode { get; init; } = AuthenticationMode.ApiKey;
|
public AuthenticationMode Mode { get; init; } = AuthenticationMode.ApiKey;
|
||||||
|
|
||||||
|
/// <summary>Gets the SQLite database path for authentication credentials.</summary>
|
||||||
public string SqlitePath { get; init; } = @"C:\ProgramData\MxGateway\gateway-auth.db";
|
public string SqlitePath { get; init; } = @"C:\ProgramData\MxGateway\gateway-auth.db";
|
||||||
|
|
||||||
|
/// <summary>Gets the secret manager name for API key pepper.</summary>
|
||||||
public string PepperSecretName { get; init; } = "MxGateway:ApiKeyPepper";
|
public string PepperSecretName { get; init; } = "MxGateway:ApiKeyPepper";
|
||||||
|
|
||||||
|
/// <summary>Gets whether database migrations should run on startup.</summary>
|
||||||
public bool RunMigrationsOnStartup { get; init; } = true;
|
public bool RunMigrationsOnStartup { get; init; } = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,27 @@ namespace MxGateway.Server.Configuration;
|
|||||||
|
|
||||||
public sealed class DashboardOptions
|
public sealed class DashboardOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets whether the dashboard is enabled.</summary>
|
||||||
public bool Enabled { get; init; } = true;
|
public bool Enabled { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>Gets the dashboard URL path base.</summary>
|
||||||
public string PathBase { get; init; } = "/dashboard";
|
public string PathBase { get; init; } = "/dashboard";
|
||||||
|
|
||||||
|
/// <summary>Gets whether dashboard access requires admin scope.</summary>
|
||||||
public bool RequireAdminScope { get; init; } = true;
|
public bool RequireAdminScope { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>Gets whether anonymous localhost access to dashboard is allowed.</summary>
|
||||||
public bool AllowAnonymousLocalhost { get; init; } = true;
|
public bool AllowAnonymousLocalhost { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>Gets the dashboard snapshot update interval in milliseconds.</summary>
|
||||||
public int SnapshotIntervalMilliseconds { get; init; } = 1_000;
|
public int SnapshotIntervalMilliseconds { get; init; } = 1_000;
|
||||||
|
|
||||||
|
/// <summary>Gets the maximum number of recent faults to display.</summary>
|
||||||
public int RecentFaultLimit { get; init; } = 100;
|
public int RecentFaultLimit { get; init; } = 100;
|
||||||
|
|
||||||
|
/// <summary>Gets the maximum number of recent sessions to display.</summary>
|
||||||
public int RecentSessionLimit { get; init; } = 200;
|
public int RecentSessionLimit { get; init; } = 200;
|
||||||
|
|
||||||
|
/// <summary>Gets whether to show full tag values in the dashboard.</summary>
|
||||||
public bool ShowTagValues { get; init; }
|
public bool ShowTagValues { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ namespace MxGateway.Server.Configuration;
|
|||||||
|
|
||||||
public sealed class EventOptions
|
public sealed class EventOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the event queue capacity.
|
||||||
|
/// </summary>
|
||||||
public int QueueCapacity { get; init; } = 10_000;
|
public int QueueCapacity { get; init; } = 10_000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the backpressure policy for event queue overflow.
|
||||||
|
/// </summary>
|
||||||
public EventBackpressurePolicy BackpressurePolicy { get; init; } = EventBackpressurePolicy.FailFast;
|
public EventBackpressurePolicy BackpressurePolicy { get; init; } = EventBackpressurePolicy.FailFast;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ using Microsoft.Extensions.Options;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Configuration;
|
namespace MxGateway.Server.Configuration;
|
||||||
|
|
||||||
|
/// <summary>Provides the effective gateway configuration with sensitive values redacted.</summary>
|
||||||
public sealed class GatewayConfigurationProvider(IOptions<GatewayOptions> options) : IGatewayConfigurationProvider
|
public sealed class GatewayConfigurationProvider(IOptions<GatewayOptions> options) : IGatewayConfigurationProvider
|
||||||
{
|
{
|
||||||
|
/// <summary>Marker string for redacted sensitive configuration values.</summary>
|
||||||
public const string RedactedValue = "[redacted]";
|
public const string RedactedValue = "[redacted]";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public EffectiveGatewayConfiguration GetEffectiveConfiguration()
|
public EffectiveGatewayConfiguration GetEffectiveConfiguration()
|
||||||
{
|
{
|
||||||
GatewayOptions value = options.Value;
|
GatewayOptions value = options.Value;
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ namespace MxGateway.Server.Configuration;
|
|||||||
|
|
||||||
public static class GatewayConfigurationServiceCollectionExtensions
|
public static class GatewayConfigurationServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>Registers gateway configuration services in the dependency injection container.</summary>
|
||||||
|
/// <param name="services">The service collection.</param>
|
||||||
|
/// <returns>The service collection for chaining.</returns>
|
||||||
public static IServiceCollection AddGatewayConfiguration(this IServiceCollection services)
|
public static IServiceCollection AddGatewayConfiguration(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services
|
services
|
||||||
|
|||||||
@@ -4,15 +4,33 @@ public sealed class GatewayOptions
|
|||||||
{
|
{
|
||||||
public const string SectionName = "MxGateway";
|
public const string SectionName = "MxGateway";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets authentication configuration options.
|
||||||
|
/// </summary>
|
||||||
public AuthenticationOptions Authentication { get; init; } = new();
|
public AuthenticationOptions Authentication { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets worker process configuration options.
|
||||||
|
/// </summary>
|
||||||
public WorkerOptions Worker { get; init; } = new();
|
public WorkerOptions Worker { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets session management configuration options.
|
||||||
|
/// </summary>
|
||||||
public SessionOptions Sessions { get; init; } = new();
|
public SessionOptions Sessions { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets event stream configuration options.
|
||||||
|
/// </summary>
|
||||||
public EventOptions Events { get; init; } = new();
|
public EventOptions Events { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets dashboard configuration options.
|
||||||
|
/// </summary>
|
||||||
public DashboardOptions Dashboard { get; init; } = new();
|
public DashboardOptions Dashboard { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets protocol configuration options.
|
||||||
|
/// </summary>
|
||||||
public ProtocolOptions Protocol { get; init; } = new();
|
public ProtocolOptions Protocol { get; init; } = new();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ public sealed class GatewayOptionsValidator : IValidateOptions<GatewayOptions>
|
|||||||
private const int MinimumMaxMessageBytes = 1024;
|
private const int MinimumMaxMessageBytes = 1024;
|
||||||
private const int MaximumMaxMessageBytes = 256 * 1024 * 1024;
|
private const int MaximumMaxMessageBytes = 256 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates gateway configuration options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">Options name.</param>
|
||||||
|
/// <param name="options">Gateway options to validate.</param>
|
||||||
|
/// <returns>Validation result.</returns>
|
||||||
public ValidateOptionsResult Validate(string? name, GatewayOptions options)
|
public ValidateOptionsResult Validate(string? name, GatewayOptions options)
|
||||||
{
|
{
|
||||||
List<string> failures = [];
|
List<string> failures = [];
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
namespace MxGateway.Server.Configuration;
|
namespace MxGateway.Server.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides the effective gateway configuration, applying defaults and validations.
|
||||||
|
/// </summary>
|
||||||
public interface IGatewayConfigurationProvider
|
public interface IGatewayConfigurationProvider
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the validated and effective gateway configuration.
|
||||||
|
/// </summary>
|
||||||
EffectiveGatewayConfiguration GetEffectiveConfiguration();
|
EffectiveGatewayConfiguration GetEffectiveConfiguration();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ using MxGateway.Contracts;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Configuration;
|
namespace MxGateway.Server.Configuration;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for the worker protocol version.
|
||||||
|
/// </summary>
|
||||||
public sealed class ProtocolOptions
|
public sealed class ProtocolOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the worker protocol version.
|
||||||
|
/// </summary>
|
||||||
public uint WorkerProtocolVersion { get; init; } = GatewayContractInfo.WorkerProtocolVersion;
|
public uint WorkerProtocolVersion { get; init; } = GatewayContractInfo.WorkerProtocolVersion;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,23 @@ namespace MxGateway.Server.Configuration;
|
|||||||
|
|
||||||
public sealed class SessionOptions
|
public sealed class SessionOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default command timeout in seconds.
|
||||||
|
/// </summary>
|
||||||
public int DefaultCommandTimeoutSeconds { get; init; } = 30;
|
public int DefaultCommandTimeoutSeconds { get; init; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maximum number of concurrent sessions.
|
||||||
|
/// </summary>
|
||||||
public int MaxSessions { get; init; } = 64;
|
public int MaxSessions { get; init; } = 64;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the maximum number of pending commands per session.
|
||||||
|
/// </summary>
|
||||||
public int MaxPendingCommandsPerSession { get; init; } = 128;
|
public int MaxPendingCommandsPerSession { get; init; } = 128;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a value indicating whether multiple event subscribers are allowed per session.
|
||||||
|
/// </summary>
|
||||||
public bool AllowMultipleEventSubscribers { get; init; }
|
public bool AllowMultipleEventSubscribers { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,37 @@ namespace MxGateway.Server.Configuration;
|
|||||||
|
|
||||||
public sealed class WorkerOptions
|
public sealed class WorkerOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>The path to the worker executable.</summary>
|
||||||
public string ExecutablePath { get; init; } =
|
public string ExecutablePath { get; init; } =
|
||||||
@"src\MxGateway.Worker\bin\x86\Release\MxGateway.Worker.exe";
|
@"src\MxGateway.Worker\bin\x86\Release\MxGateway.Worker.exe";
|
||||||
|
|
||||||
|
/// <summary>The working directory for the worker process, or null to inherit.</summary>
|
||||||
public string? WorkingDirectory { get; init; }
|
public string? WorkingDirectory { get; init; }
|
||||||
|
|
||||||
|
/// <summary>The required processor architecture for the worker.</summary>
|
||||||
public WorkerArchitecture RequiredArchitecture { get; init; } = WorkerArchitecture.X86;
|
public WorkerArchitecture RequiredArchitecture { get; init; } = WorkerArchitecture.X86;
|
||||||
|
|
||||||
|
/// <summary>The maximum time in seconds for the worker to start.</summary>
|
||||||
public int StartupTimeoutSeconds { get; init; } = 30;
|
public int StartupTimeoutSeconds { get; init; } = 30;
|
||||||
|
|
||||||
|
/// <summary>The number of retry attempts for the startup probe.</summary>
|
||||||
public int StartupProbeRetryAttempts { get; init; } = 3;
|
public int StartupProbeRetryAttempts { get; init; } = 3;
|
||||||
|
|
||||||
|
/// <summary>The delay in milliseconds between startup probe retries.</summary>
|
||||||
public int StartupProbeRetryDelayMilliseconds { get; init; } = 250;
|
public int StartupProbeRetryDelayMilliseconds { get; init; } = 250;
|
||||||
|
|
||||||
|
/// <summary>The timeout in milliseconds for connecting to the worker pipe.</summary>
|
||||||
public int PipeConnectAttemptTimeoutMilliseconds { get; init; } = 2000;
|
public int PipeConnectAttemptTimeoutMilliseconds { get; init; } = 2000;
|
||||||
|
|
||||||
|
/// <summary>The maximum time in seconds for graceful shutdown.</summary>
|
||||||
public int ShutdownTimeoutSeconds { get; init; } = 10;
|
public int ShutdownTimeoutSeconds { get; init; } = 10;
|
||||||
|
|
||||||
|
/// <summary>The interval in seconds for worker heartbeats.</summary>
|
||||||
public int HeartbeatIntervalSeconds { get; init; } = 5;
|
public int HeartbeatIntervalSeconds { get; init; } = 5;
|
||||||
|
|
||||||
|
/// <summary>The grace period in seconds after a heartbeat before considering the worker unresponsive.</summary>
|
||||||
public int HeartbeatGraceSeconds { get; init; } = 15;
|
public int HeartbeatGraceSeconds { get; init; } = 15;
|
||||||
|
|
||||||
|
/// <summary>The maximum message size in bytes for IPC communication.</summary>
|
||||||
public int MaxMessageBytes { get; init; } = 16 * 1024 * 1024;
|
public int MaxMessageBytes { get; init; } = 16 * 1024 * 1024;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ namespace MxGateway.Server.Dashboard.Components;
|
|||||||
|
|
||||||
public static class DashboardDisplay
|
public static class DashboardDisplay
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Formats a nullable date and time value for display.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The date and time to format.</param>
|
||||||
|
/// <returns>Formatted date and time string or "-" if null.</returns>
|
||||||
public static string DateTime(DateTimeOffset? value)
|
public static string DateTime(DateTimeOffset? value)
|
||||||
{
|
{
|
||||||
return value.HasValue
|
return value.HasValue
|
||||||
@@ -9,6 +14,11 @@ public static class DashboardDisplay
|
|||||||
: "-";
|
: "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats a time span duration for display.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The duration to format.</param>
|
||||||
|
/// <returns>Formatted duration string.</returns>
|
||||||
public static string Duration(TimeSpan value)
|
public static string Duration(TimeSpan value)
|
||||||
{
|
{
|
||||||
return value.TotalDays >= 1
|
return value.TotalDays >= 1
|
||||||
@@ -16,16 +26,33 @@ public static class DashboardDisplay
|
|||||||
: value.ToString(@"hh\:mm\:ss", System.Globalization.CultureInfo.InvariantCulture);
|
: value.ToString(@"hh\:mm\:ss", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats a nullable text value for display.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The text to format.</param>
|
||||||
|
/// <returns>Formatted text or "-" if null or empty.</returns>
|
||||||
public static string Text(string? value)
|
public static string Text(string? value)
|
||||||
{
|
{
|
||||||
return string.IsNullOrWhiteSpace(value) ? "-" : value;
|
return string.IsNullOrWhiteSpace(value) ? "-" : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats a long count value for display with thousands separator.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The count to format.</param>
|
||||||
|
/// <returns>Formatted count string.</returns>
|
||||||
public static string Count(long value)
|
public static string Count(long value)
|
||||||
{
|
{
|
||||||
return value.ToString("N0", System.Globalization.CultureInfo.InvariantCulture);
|
return value.ToString("N0", System.Globalization.CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves a metric value from a snapshot by name and optional dimension.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="snapshot">Dashboard snapshot.</param>
|
||||||
|
/// <param name="name">Metric name.</param>
|
||||||
|
/// <param name="dimension">Optional metric dimension.</param>
|
||||||
|
/// <returns>Metric value or zero if not found.</returns>
|
||||||
public static long MetricValue(DashboardSnapshot snapshot, string name, string? dimension = null)
|
public static long MetricValue(DashboardSnapshot snapshot, string name, string? dimension = null)
|
||||||
{
|
{
|
||||||
return snapshot.Metrics.FirstOrDefault(metric =>
|
return snapshot.Metrics.FirstOrDefault(metric =>
|
||||||
|
|||||||
@@ -2,21 +2,32 @@ using Microsoft.AspNetCore.Components;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Dashboard.Components;
|
namespace MxGateway.Server.Dashboard.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for Blazor dashboard pages that watch gateway metrics snapshots.
|
||||||
|
/// </summary>
|
||||||
public abstract class DashboardPageBase : ComponentBase, IAsyncDisposable
|
public abstract class DashboardPageBase : ComponentBase, IAsyncDisposable
|
||||||
{
|
{
|
||||||
private readonly CancellationTokenSource _disposeCancellation = new();
|
private readonly CancellationTokenSource _disposeCancellation = new();
|
||||||
private Task? _watchTask;
|
private Task? _watchTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service that provides gateway metric snapshots.
|
||||||
|
/// </summary>
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDashboardSnapshotService SnapshotService { get; set; } = null!;
|
protected IDashboardSnapshotService SnapshotService { get; set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The most recent gateway metric snapshot, updated as it changes.
|
||||||
|
/// </summary>
|
||||||
protected DashboardSnapshot? Snapshot { get; private set; }
|
protected DashboardSnapshot? Snapshot { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
_watchTask = WatchSnapshotsAsync();
|
_watchTask = WatchSnapshotsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await _disposeCancellation.CancelAsync().ConfigureAwait(false);
|
await _disposeCancellation.CancelAsync().ConfigureAwait(false);
|
||||||
@@ -29,6 +40,9 @@ public abstract class DashboardPageBase : ComponentBase, IAsyncDisposable
|
|||||||
GC.SuppressFinalize(this);
|
GC.SuppressFinalize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Watches snapshot changes and triggers component refresh.
|
||||||
|
/// </summary>
|
||||||
private async Task WatchSnapshotsAsync()
|
private async Task WatchSnapshotsAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -2,16 +2,36 @@ using System.Security.Claims;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Dashboard;
|
namespace MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of a dashboard authentication attempt.
|
||||||
|
/// </summary>
|
||||||
public sealed record DashboardAuthenticationResult(
|
public sealed record DashboardAuthenticationResult(
|
||||||
|
/// <summary>
|
||||||
|
/// Whether authentication succeeded.
|
||||||
|
/// </summary>
|
||||||
bool Succeeded,
|
bool Succeeded,
|
||||||
|
/// <summary>
|
||||||
|
/// The authenticated principal if successful; otherwise null.
|
||||||
|
/// </summary>
|
||||||
ClaimsPrincipal? Principal,
|
ClaimsPrincipal? Principal,
|
||||||
|
/// <summary>
|
||||||
|
/// The failure message if authentication failed; otherwise null.
|
||||||
|
/// </summary>
|
||||||
string? FailureMessage)
|
string? FailureMessage)
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a successful authentication result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="principal">Authenticated principal.</param>
|
||||||
public static DashboardAuthenticationResult Success(ClaimsPrincipal principal)
|
public static DashboardAuthenticationResult Success(ClaimsPrincipal principal)
|
||||||
{
|
{
|
||||||
return new DashboardAuthenticationResult(true, principal, null);
|
return new DashboardAuthenticationResult(true, principal, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a failed authentication result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="failureMessage">Diagnostic message describing the failure.</param>
|
||||||
public static DashboardAuthenticationResult Fail(string failureMessage)
|
public static DashboardAuthenticationResult Fail(string failureMessage)
|
||||||
{
|
{
|
||||||
return new DashboardAuthenticationResult(false, null, failureMessage);
|
return new DashboardAuthenticationResult(false, null, failureMessage);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ public sealed class DashboardAuthenticator(
|
|||||||
{
|
{
|
||||||
private const string GenericFailureMessage = "The API key is invalid or is not authorized for dashboard access.";
|
private const string GenericFailureMessage = "The API key is invalid or is not authorized for dashboard access.";
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<DashboardAuthenticationResult> AuthenticateAsync(
|
public async Task<DashboardAuthenticationResult> AuthenticateAsync(
|
||||||
string? apiKey,
|
string? apiKey,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ public sealed class DashboardAuthorizationHandler(
|
|||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
IOptions<GatewayOptions> options) : AuthorizationHandler<DashboardAuthorizationRequirement>
|
IOptions<GatewayOptions> options) : AuthorizationHandler<DashboardAuthorizationRequirement>
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override Task HandleRequirementAsync(
|
protected override Task HandleRequirementAsync(
|
||||||
AuthorizationHandlerContext context,
|
AuthorizationHandlerContext context,
|
||||||
DashboardAuthorizationRequirement requirement)
|
DashboardAuthorizationRequirement requirement)
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ using MxGateway.Server.Dashboard.Components;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Dashboard;
|
namespace MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
|
/// <summary>Endpoint extensions for registering the gateway dashboard routes.</summary>
|
||||||
public static class DashboardEndpointRouteBuilderExtensions
|
public static class DashboardEndpointRouteBuilderExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>Maps all gateway dashboard routes including login, logout, and Razor components.</summary>
|
||||||
|
/// <param name="endpoints">The endpoint route builder.</param>
|
||||||
|
/// <returns>The route builder for chaining.</returns>
|
||||||
public static IEndpointRouteBuilder MapGatewayDashboard(this IEndpointRouteBuilder endpoints)
|
public static IEndpointRouteBuilder MapGatewayDashboard(this IEndpointRouteBuilder endpoints)
|
||||||
{
|
{
|
||||||
IConfiguration configuration = endpoints.ServiceProvider.GetRequiredService<IConfiguration>();
|
IConfiguration configuration = endpoints.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace MxGateway.Server.Dashboard;
|
|||||||
/// per-category breakdowns are computed here rather than stored on the cache so the
|
/// per-category breakdowns are computed here rather than stored on the cache so the
|
||||||
/// Galaxy namespace stays free of dashboard-presentation concepts.
|
/// Galaxy namespace stays free of dashboard-presentation concepts.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>Projects Galaxy Repository cache entries to dashboard presentation format.</summary>
|
||||||
internal static class DashboardGalaxyProjector
|
internal static class DashboardGalaxyProjector
|
||||||
{
|
{
|
||||||
private const int TopTemplatesLimit = 10;
|
private const int TopTemplatesLimit = 10;
|
||||||
@@ -25,6 +26,9 @@ internal static class DashboardGalaxyProjector
|
|||||||
[26] = "OPCClient",
|
[26] = "OPCClient",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>Projects a Galaxy Repository cache entry to a dashboard summary.</summary>
|
||||||
|
/// <param name="entry">Galaxy cache entry to project.</param>
|
||||||
|
/// <returns>Dashboard-formatted Galaxy summary.</returns>
|
||||||
public static DashboardGalaxySummary Project(GalaxyHierarchyCacheEntry entry)
|
public static DashboardGalaxySummary Project(GalaxyHierarchyCacheEntry entry)
|
||||||
{
|
{
|
||||||
DashboardGalaxyStatus status = entry.Status switch
|
DashboardGalaxyStatus status = entry.Status switch
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public sealed record DashboardGalaxySummary(
|
|||||||
IReadOnlyList<DashboardGalaxyTemplateUsage> TopTemplates,
|
IReadOnlyList<DashboardGalaxyTemplateUsage> TopTemplates,
|
||||||
IReadOnlyList<DashboardGalaxyCategoryCount> ObjectCategories)
|
IReadOnlyList<DashboardGalaxyCategoryCount> ObjectCategories)
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets the unknown Galaxy status placeholder.</summary>
|
||||||
public static DashboardGalaxySummary Unknown { get; } = new(
|
public static DashboardGalaxySummary Unknown { get; } = new(
|
||||||
DashboardGalaxyStatus.Unknown,
|
DashboardGalaxyStatus.Unknown,
|
||||||
LastQueriedAt: null,
|
LastQueriedAt: null,
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ internal static class DashboardRedactor
|
|||||||
"token",
|
"token",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Redacts sensitive content from a value for dashboard display.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Value to redact.</param>
|
||||||
|
/// <returns>Redacted value or original value if not sensitive.</returns>
|
||||||
public static string? Redact(string? value)
|
public static string? Redact(string? value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ using MxGateway.Server.Configuration;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Dashboard;
|
namespace MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for configuring the gateway dashboard services.
|
||||||
|
/// </summary>
|
||||||
public static class DashboardServiceCollectionExtensions
|
public static class DashboardServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers all dashboard services, authentication, and Razor components.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">Service collection to register services.</param>
|
||||||
public static IServiceCollection AddGatewayDashboard(this IServiceCollection services)
|
public static IServiceCollection AddGatewayDashboard(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddSingleton<IDashboardSnapshotService, DashboardSnapshotService>();
|
services.AddSingleton<IDashboardSnapshotService, DashboardSnapshotService>();
|
||||||
|
|||||||
@@ -22,6 +22,13 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
|||||||
private readonly int _recentFaultLimit;
|
private readonly int _recentFaultLimit;
|
||||||
private readonly int _recentSessionLimit;
|
private readonly int _recentSessionLimit;
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the DashboardSnapshotService class.</summary>
|
||||||
|
/// <param name="sessionRegistry">Registry of active gateway sessions.</param>
|
||||||
|
/// <param name="metrics">Gateway metrics collector.</param>
|
||||||
|
/// <param name="configurationProvider">Gateway configuration provider.</param>
|
||||||
|
/// <param name="galaxyHierarchyCache">Galaxy hierarchy cache.</param>
|
||||||
|
/// <param name="options">Gateway configuration options.</param>
|
||||||
|
/// <param name="timeProvider">Provider for current time; defaults to system time.</param>
|
||||||
public DashboardSnapshotService(
|
public DashboardSnapshotService(
|
||||||
ISessionRegistry sessionRegistry,
|
ISessionRegistry sessionRegistry,
|
||||||
GatewayMetrics metrics,
|
GatewayMetrics metrics,
|
||||||
@@ -43,6 +50,10 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
|||||||
_recentSessionLimit = options.Value.Dashboard.RecentSessionLimit;
|
_recentSessionLimit = options.Value.Dashboard.RecentSessionLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a current dashboard snapshot of gateway state.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Dashboard snapshot.</returns>
|
||||||
public DashboardSnapshot GetSnapshot()
|
public DashboardSnapshot GetSnapshot()
|
||||||
{
|
{
|
||||||
DateTimeOffset generatedAt = _timeProvider.GetUtcNow();
|
DateTimeOffset generatedAt = _timeProvider.GetUtcNow();
|
||||||
@@ -73,6 +84,11 @@ public sealed class DashboardSnapshotService : IDashboardSnapshotService
|
|||||||
Galaxy: DashboardGalaxyProjector.Project(_galaxyHierarchyCache.Current));
|
Galaxy: DashboardGalaxyProjector.Project(_galaxyHierarchyCache.Current));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Watches dashboard snapshots at regular intervals asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Async enumerable of dashboard snapshots.</returns>
|
||||||
public async IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(
|
public async IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
namespace MxGateway.Server.Dashboard;
|
namespace MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticates dashboard access with API keys.
|
||||||
|
/// </summary>
|
||||||
public interface IDashboardAuthenticator
|
public interface IDashboardAuthenticator
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Authenticates the dashboard session with an API key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiKey">The API key to authenticate.</param>
|
||||||
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||||
Task<DashboardAuthenticationResult> AuthenticateAsync(
|
Task<DashboardAuthenticationResult> AuthenticateAsync(
|
||||||
string? apiKey,
|
string? apiKey,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
namespace MxGateway.Server.Dashboard;
|
namespace MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides snapshots of the dashboard state for UI updates.
|
||||||
|
/// </summary>
|
||||||
public interface IDashboardSnapshotService
|
public interface IDashboardSnapshotService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current dashboard snapshot.
|
||||||
|
/// </summary>
|
||||||
DashboardSnapshot GetSnapshot();
|
DashboardSnapshot GetSnapshot();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Watches for changes to the dashboard state as an async enumerable.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||||
IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(CancellationToken cancellationToken);
|
IAsyncEnumerable<DashboardSnapshot> WatchSnapshotsAsync(CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
namespace MxGateway.Server.Diagnostics;
|
namespace MxGateway.Server.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Redacts sensitive information from log entries.
|
||||||
|
/// </summary>
|
||||||
public static class GatewayLogRedactor
|
public static class GatewayLogRedactor
|
||||||
{
|
{
|
||||||
|
/// <summary>Placeholder for redacted values.</summary>
|
||||||
public const string RedactedValue = "[redacted]";
|
public const string RedactedValue = "[redacted]";
|
||||||
|
|
||||||
private static readonly HashSet<string> SensitiveCommandMethods = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> SensitiveCommandMethods = new(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -11,12 +15,20 @@ public static class GatewayLogRedactor
|
|||||||
"WriteSecured2"
|
"WriteSecured2"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether a command method bears credentials.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandMethod">The command method name to check.</param>
|
||||||
public static bool IsCredentialBearingCommand(string? commandMethod)
|
public static bool IsCredentialBearingCommand(string? commandMethod)
|
||||||
{
|
{
|
||||||
return commandMethod is not null
|
return commandMethod is not null
|
||||||
&& SensitiveCommandMethods.Contains(commandMethod);
|
&& SensitiveCommandMethods.Contains(commandMethod);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Redacts the API key secret portion of a Bearer authorization header.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorizationHeader">The authorization header value to redact.</param>
|
||||||
public static string? RedactApiKey(string? authorizationHeader)
|
public static string? RedactApiKey(string? authorizationHeader)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(authorizationHeader))
|
if (string.IsNullOrWhiteSpace(authorizationHeader))
|
||||||
@@ -46,6 +58,10 @@ public static class GatewayLogRedactor
|
|||||||
return $"{bearerPrefix}mxgw_{tokenParts[1]}_{RedactedValue}";
|
return $"{bearerPrefix}mxgw_{tokenParts[1]}_{RedactedValue}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Redacts the client identity if it contains an API key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="clientIdentity">The client identity string to redact.</param>
|
||||||
public static string? RedactClientIdentity(string? clientIdentity)
|
public static string? RedactClientIdentity(string? clientIdentity)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(clientIdentity))
|
if (string.IsNullOrWhiteSpace(clientIdentity))
|
||||||
@@ -58,6 +74,12 @@ public static class GatewayLogRedactor
|
|||||||
: clientIdentity;
|
: clientIdentity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Redacts a command value if it contains credentials or value logging is disabled.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="commandMethod">The command method name to check for credentials.</param>
|
||||||
|
/// <param name="value">The command value to redact.</param>
|
||||||
|
/// <param name="valueLoggingEnabled">Whether value logging is enabled.</param>
|
||||||
public static object? RedactCommandValue(
|
public static object? RedactCommandValue(
|
||||||
string? commandMethod,
|
string? commandMethod,
|
||||||
object? value,
|
object? value,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public sealed record GatewayLogScope(
|
|||||||
string? CommandMethod = null,
|
string? CommandMethod = null,
|
||||||
string? ClientIdentity = null)
|
string? ClientIdentity = null)
|
||||||
{
|
{
|
||||||
|
/// <summary>Converts the log scope to a dictionary with redacted sensitive fields.</summary>
|
||||||
public IReadOnlyDictionary<string, object?> ToDictionary()
|
public IReadOnlyDictionary<string, object?> ToDictionary()
|
||||||
{
|
{
|
||||||
Dictionary<string, object?> values = [];
|
Dictionary<string, object?> values = [];
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ namespace MxGateway.Server.Diagnostics;
|
|||||||
|
|
||||||
public static class GatewayLoggerExtensions
|
public static class GatewayLoggerExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>Begins a gateway log scope with the specified scope properties.</summary>
|
||||||
|
/// <param name="logger">Logger used for diagnostic output.</param>
|
||||||
|
/// <param name="scope">Scope properties to apply.</param>
|
||||||
|
/// <returns>A disposable that ends the scope when disposed.</returns>
|
||||||
public static IDisposable? BeginGatewayScope(
|
public static IDisposable? BeginGatewayScope(
|
||||||
this ILogger logger,
|
this ILogger logger,
|
||||||
GatewayLogScope scope)
|
GatewayLogScope scope)
|
||||||
|
|||||||
@@ -2,13 +2,23 @@ using Microsoft.Extensions.Primitives;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Diagnostics;
|
namespace MxGateway.Server.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>Middleware extensions for structured gateway request logging with correlation context.</summary>
|
||||||
public static class GatewayRequestLoggingMiddlewareExtensions
|
public static class GatewayRequestLoggingMiddlewareExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>Header name for the session ID.</summary>
|
||||||
public const string SessionIdHeaderName = "x-session-id";
|
public const string SessionIdHeaderName = "x-session-id";
|
||||||
|
|
||||||
|
/// <summary>Header name for the worker process ID.</summary>
|
||||||
public const string WorkerProcessIdHeaderName = "x-worker-process-id";
|
public const string WorkerProcessIdHeaderName = "x-worker-process-id";
|
||||||
|
|
||||||
|
/// <summary>Header name for the correlation ID.</summary>
|
||||||
public const string CorrelationIdHeaderName = "x-correlation-id";
|
public const string CorrelationIdHeaderName = "x-correlation-id";
|
||||||
|
|
||||||
|
/// <summary>Header name for the command method name.</summary>
|
||||||
public const string CommandMethodHeaderName = "x-command-method";
|
public const string CommandMethodHeaderName = "x-command-method";
|
||||||
|
|
||||||
|
/// <summary>Adds gateway request logging scope middleware that reads correlation headers and redacts sensitive data.</summary>
|
||||||
|
/// <param name="app">Application builder.</param>
|
||||||
public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicationBuilder app)
|
public static IApplicationBuilder UseGatewayRequestLoggingScope(this IApplicationBuilder app)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(app);
|
ArgumentNullException.ThrowIfNull(app);
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ namespace MxGateway.Server.Galaxy;
|
|||||||
/// other subscribers or the publisher. When a subscriber's channel is full the oldest
|
/// other subscribers or the publisher. When a subscriber's channel is full the oldest
|
||||||
/// event is dropped — clients use the sequence field to detect gaps.
|
/// event is dropped — clients use the sequence field to detect gaps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <summary>
|
||||||
|
/// Publishes Galaxy deploy events to streaming gRPC subscribers via private bounded channels.
|
||||||
|
/// </summary>
|
||||||
public sealed class GalaxyDeployNotifier : IGalaxyDeployNotifier
|
public sealed class GalaxyDeployNotifier : IGalaxyDeployNotifier
|
||||||
{
|
{
|
||||||
private const int SubscriberQueueCapacity = 16;
|
private const int SubscriberQueueCapacity = 16;
|
||||||
@@ -17,8 +20,12 @@ public sealed class GalaxyDeployNotifier : IGalaxyDeployNotifier
|
|||||||
private readonly ConcurrentDictionary<Guid, Channel<GalaxyDeployEventInfo>> _subscribers = new();
|
private readonly ConcurrentDictionary<Guid, Channel<GalaxyDeployEventInfo>> _subscribers = new();
|
||||||
private GalaxyDeployEventInfo? _latest;
|
private GalaxyDeployEventInfo? _latest;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The most recent deploy event, or null if none has been published.
|
||||||
|
/// </summary>
|
||||||
public GalaxyDeployEventInfo? Latest => Volatile.Read(ref _latest);
|
public GalaxyDeployEventInfo? Latest => Volatile.Read(ref _latest);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public void Publish(GalaxyDeployEventInfo info)
|
public void Publish(GalaxyDeployEventInfo info)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(info);
|
ArgumentNullException.ThrowIfNull(info);
|
||||||
@@ -33,6 +40,7 @@ public sealed class GalaxyDeployNotifier : IGalaxyDeployNotifier
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async IAsyncEnumerable<GalaxyDeployEventInfo> SubscribeAsync(
|
public async IAsyncEnumerable<GalaxyDeployEventInfo> SubscribeAsync(
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
private readonly SemaphoreSlim _refreshGate = new(1, 1);
|
private readonly SemaphoreSlim _refreshGate = new(1, 1);
|
||||||
private GalaxyHierarchyCacheEntry _current = GalaxyHierarchyCacheEntry.Empty;
|
private GalaxyHierarchyCacheEntry _current = GalaxyHierarchyCacheEntry.Empty;
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the <see cref="GalaxyHierarchyCache"/> class.</summary>
|
||||||
|
/// <param name="repository">Galaxy Repository client for SQL queries.</param>
|
||||||
|
/// <param name="notifier">Galaxy deploy event notifier.</param>
|
||||||
|
/// <param name="timeProvider">Provider for current time; defaults to system time.</param>
|
||||||
|
/// <param name="logger">Optional logger for diagnostic output.</param>
|
||||||
public GalaxyHierarchyCache(
|
public GalaxyHierarchyCache(
|
||||||
GalaxyRepository repository,
|
GalaxyRepository repository,
|
||||||
IGalaxyDeployNotifier notifier,
|
IGalaxyDeployNotifier notifier,
|
||||||
@@ -37,6 +42,7 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the current Galaxy hierarchy cache entry with projected status.</summary>
|
||||||
public GalaxyHierarchyCacheEntry Current
|
public GalaxyHierarchyCacheEntry Current
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -47,6 +53,9 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Refreshes the Galaxy hierarchy cache if the deploy time has advanced.</summary>
|
||||||
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||||
|
/// <returns>Asynchronous task representing the refresh operation.</returns>
|
||||||
public async Task RefreshAsync(CancellationToken cancellationToken)
|
public async Task RefreshAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _refreshGate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
await _refreshGate.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
@@ -60,6 +69,9 @@ public sealed class GalaxyHierarchyCache : IGalaxyHierarchyCache
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Waits for the Galaxy hierarchy cache to complete its first load.</summary>
|
||||||
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||||
|
/// <returns>Asynchronous task representing the wait operation.</returns>
|
||||||
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken)
|
public Task WaitForFirstLoadAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return _firstLoad.Task.WaitAsync(cancellationToken);
|
return _firstLoad.Task.WaitAsync(cancellationToken);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public sealed record GalaxyHierarchyCacheEntry(
|
|||||||
int HistorizedAttributeCount,
|
int HistorizedAttributeCount,
|
||||||
int AlarmAttributeCount)
|
int AlarmAttributeCount)
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets an empty Galaxy hierarchy cache entry.</summary>
|
||||||
public static GalaxyHierarchyCacheEntry Empty { get; } = new(
|
public static GalaxyHierarchyCacheEntry Empty { get; } = new(
|
||||||
Status: GalaxyCacheStatus.Unknown,
|
Status: GalaxyCacheStatus.Unknown,
|
||||||
Sequence: 0,
|
Sequence: 0,
|
||||||
@@ -39,5 +40,6 @@ public sealed record GalaxyHierarchyCacheEntry(
|
|||||||
HistorizedAttributeCount: 0,
|
HistorizedAttributeCount: 0,
|
||||||
AlarmAttributeCount: 0);
|
AlarmAttributeCount: 0);
|
||||||
|
|
||||||
|
/// <summary>Gets a value indicating whether the cache entry contains usable data.</summary>
|
||||||
public bool HasData => Status is GalaxyCacheStatus.Healthy or GalaxyCacheStatus.Stale;
|
public bool HasData => Status is GalaxyCacheStatus.Healthy or GalaxyCacheStatus.Stale;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,7 @@ using Microsoft.Extensions.Options;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Galaxy;
|
namespace MxGateway.Server.Galaxy;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Background service that periodically refreshes the Galaxy Repository hierarchy cache off the request path.</summary>
|
||||||
/// Periodically refreshes <see cref="IGalaxyHierarchyCache"/> off the request path. The
|
|
||||||
/// interval comes from <see cref="GalaxyRepositoryOptions.DashboardRefreshIntervalSeconds"/>;
|
|
||||||
/// each tick is cheap when the deploy timestamp is unchanged.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class GalaxyHierarchyRefreshService(
|
public sealed class GalaxyHierarchyRefreshService(
|
||||||
IGalaxyHierarchyCache cache,
|
IGalaxyHierarchyCache cache,
|
||||||
IOptions<GalaxyRepositoryOptions> options,
|
IOptions<GalaxyRepositoryOptions> options,
|
||||||
@@ -17,6 +13,7 @@ public sealed class GalaxyHierarchyRefreshService(
|
|||||||
{
|
{
|
||||||
private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System;
|
private readonly TimeProvider _timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
TimeSpan interval = TimeSpan.FromSeconds(Math.Max(1, options.Value.DashboardRefreshIntervalSeconds));
|
TimeSpan interval = TimeSpan.FromSeconds(Math.Max(1, options.Value.DashboardRefreshIntervalSeconds));
|
||||||
|
|||||||
@@ -6,30 +6,70 @@ namespace MxGateway.Server.Galaxy;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GalaxyHierarchyRow
|
public sealed class GalaxyHierarchyRow
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets the Galaxy object identifier.</summary>
|
||||||
public int GobjectId { get; init; }
|
public int GobjectId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets the tag name.</summary>
|
||||||
public string TagName { get; init; } = string.Empty;
|
public string TagName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets the contained name.</summary>
|
||||||
public string ContainedName { get; init; } = string.Empty;
|
public string ContainedName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets the browse name.</summary>
|
||||||
public string BrowseName { get; init; } = string.Empty;
|
public string BrowseName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets the parent Galaxy object identifier.</summary>
|
||||||
public int ParentGobjectId { get; init; }
|
public int ParentGobjectId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets a value indicating whether this is an area.</summary>
|
||||||
public bool IsArea { get; init; }
|
public bool IsArea { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets the category identifier.</summary>
|
||||||
public int CategoryId { get; init; }
|
public int CategoryId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets the Galaxy object identifier of the host.</summary>
|
||||||
public int HostedByGobjectId { get; init; }
|
public int HostedByGobjectId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets the template derivation chain.</summary>
|
||||||
public IReadOnlyList<string> TemplateChain { get; init; } = Array.Empty<string>();
|
public IReadOnlyList<string> TemplateChain { get; init; } = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>One row from <see cref="GalaxyRepository.GetAttributesAsync"/>.</summary>
|
/// <summary>One row from <see cref="GalaxyRepository.GetAttributesAsync"/>.</summary>
|
||||||
public sealed class GalaxyAttributeRow
|
public sealed class GalaxyAttributeRow
|
||||||
{
|
{
|
||||||
|
/// <summary>Gets the Galaxy object identifier.</summary>
|
||||||
public int GobjectId { get; init; }
|
public int GobjectId { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets the tag name.</summary>
|
||||||
public string TagName { get; init; } = string.Empty;
|
public string TagName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets the attribute name.</summary>
|
||||||
public string AttributeName { get; init; } = string.Empty;
|
public string AttributeName { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets the full tag reference.</summary>
|
||||||
public string FullTagReference { get; init; } = string.Empty;
|
public string FullTagReference { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>Gets the MXAccess data type code.</summary>
|
||||||
public int MxDataType { get; init; }
|
public int MxDataType { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets the data type name.</summary>
|
||||||
public string? DataTypeName { get; init; }
|
public string? DataTypeName { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets a value indicating whether this is an array.</summary>
|
||||||
public bool IsArray { get; init; }
|
public bool IsArray { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets the array dimension, if applicable.</summary>
|
||||||
public int? ArrayDimension { get; init; }
|
public int? ArrayDimension { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets the MXAccess attribute category code.</summary>
|
||||||
public int MxAttributeCategory { get; init; }
|
public int MxAttributeCategory { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets the security classification code.</summary>
|
||||||
public int SecurityClassification { get; init; }
|
public int SecurityClassification { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets a value indicating whether this is historized.</summary>
|
||||||
public bool IsHistorized { get; init; }
|
public bool IsHistorized { get; init; }
|
||||||
|
|
||||||
|
/// <summary>Gets a value indicating whether this is an alarm.</summary>
|
||||||
public bool IsAlarm { get; init; }
|
public bool IsAlarm { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ namespace MxGateway.Server.Galaxy;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GalaxyRepository(GalaxyRepositoryOptions options)
|
public sealed class GalaxyRepository(GalaxyRepositoryOptions options)
|
||||||
{
|
{
|
||||||
|
/// <summary>Tests the connection to the Galaxy Repository database.</summary>
|
||||||
|
/// <param name="ct">Token to cancel the asynchronous operation.</param>
|
||||||
public async Task<bool> TestConnectionAsync(CancellationToken ct = default)
|
public async Task<bool> TestConnectionAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -24,6 +26,8 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options)
|
|||||||
catch (InvalidOperationException) { return false; }
|
catch (InvalidOperationException) { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Retrieves the last deployment time from the Galaxy Repository.</summary>
|
||||||
|
/// <param name="ct">Token to cancel the asynchronous operation.</param>
|
||||||
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
|
public async Task<DateTime?> GetLastDeployTimeAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
using SqlConnection conn = new(options.ConnectionString);
|
using SqlConnection conn = new(options.ConnectionString);
|
||||||
@@ -34,6 +38,8 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options)
|
|||||||
return result is DateTime dt ? dt : null;
|
return result is DateTime dt ? dt : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Retrieves the complete hierarchy of Galaxy objects from the repository.</summary>
|
||||||
|
/// <param name="ct">Token to cancel the asynchronous operation.</param>
|
||||||
public async Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default)
|
public async Task<List<GalaxyHierarchyRow>> GetHierarchyAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
List<GalaxyHierarchyRow> rows = new();
|
List<GalaxyHierarchyRow> rows = new();
|
||||||
@@ -70,6 +76,8 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options)
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Retrieves all attributes for Galaxy objects from the repository.</summary>
|
||||||
|
/// <param name="ct">Token to cancel the asynchronous operation.</param>
|
||||||
public async Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default)
|
public async Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
List<GalaxyAttributeRow> rows = new();
|
List<GalaxyAttributeRow> rows = new();
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ public sealed class GalaxyRepositoryOptions
|
|||||||
{
|
{
|
||||||
public const string SectionName = "MxGateway:Galaxy";
|
public const string SectionName = "MxGateway:Galaxy";
|
||||||
|
|
||||||
|
/// <summary>The SQL Server connection string for the Galaxy Repository database.</summary>
|
||||||
public string ConnectionString { get; init; } =
|
public string ConnectionString { get; init; } =
|
||||||
"Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;";
|
"Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;";
|
||||||
|
|
||||||
|
/// <summary>The timeout in seconds for SQL commands executed against the Galaxy Repository.</summary>
|
||||||
public int CommandTimeoutSeconds { get; init; } = 60;
|
public int CommandTimeoutSeconds { get; init; } = 60;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ namespace MxGateway.Server.Galaxy;
|
|||||||
|
|
||||||
public static class GalaxyRepositoryServiceCollectionExtensions
|
public static class GalaxyRepositoryServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
|
/// <summary>Registers Galaxy Repository services in the dependency injection container.</summary>
|
||||||
|
/// <param name="services">The service collection.</param>
|
||||||
|
/// <returns>The service collection for chaining.</returns>
|
||||||
public static IServiceCollection AddGalaxyRepository(this IServiceCollection services)
|
public static IServiceCollection AddGalaxyRepository(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services
|
services
|
||||||
|
|||||||
@@ -1,18 +1,17 @@
|
|||||||
namespace MxGateway.Server.Galaxy;
|
namespace MxGateway.Server.Galaxy;
|
||||||
|
|
||||||
|
/// <summary>Publishes Galaxy repository deploy events to subscribers.</summary>
|
||||||
public interface IGalaxyDeployNotifier
|
public interface IGalaxyDeployNotifier
|
||||||
{
|
{
|
||||||
/// <summary>The most recently published event, or <c>null</c> if no event has fired yet.</summary>
|
/// <summary>The most recently published event, or null if no event has fired yet.</summary>
|
||||||
GalaxyDeployEventInfo? Latest { get; }
|
GalaxyDeployEventInfo? Latest { get; }
|
||||||
|
|
||||||
/// <summary>Publishes a deploy event to all current subscribers and stores it as <see cref="Latest"/>.</summary>
|
/// <summary>Publishes a deploy event to all current subscribers and stores it as Latest.</summary>
|
||||||
|
/// <param name="info">The deploy event to publish.</param>
|
||||||
void Publish(GalaxyDeployEventInfo info);
|
void Publish(GalaxyDeployEventInfo info);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>Subscribes to deploy events. The sequence yields the latest event first (if available) then streams new events as they fire.</summary>
|
||||||
/// Subscribe to deploy events. The async sequence yields events as they fire. If
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||||
/// <see cref="Latest"/> is set, it is yielded first so subscribers can bootstrap their
|
/// <returns>Async enumerable of deploy events.</returns>
|
||||||
/// local cache without waiting for the next deploy. Pass a cancellation token to
|
|
||||||
/// unsubscribe.
|
|
||||||
/// </summary>
|
|
||||||
IAsyncEnumerable<GalaxyDeployEventInfo> SubscribeAsync(CancellationToken cancellationToken);
|
IAsyncEnumerable<GalaxyDeployEventInfo> SubscribeAsync(CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
namespace MxGateway.Server.Galaxy;
|
namespace MxGateway.Server.Galaxy;
|
||||||
|
|
||||||
|
/// <summary>Cache for Galaxy Repository hierarchy data.</summary>
|
||||||
public interface IGalaxyHierarchyCache
|
public interface IGalaxyHierarchyCache
|
||||||
{
|
{
|
||||||
/// <summary>The latest cache entry. Status freshness is recomputed against the clock.</summary>
|
/// <summary>The latest cache entry. Status freshness is recomputed against the clock.</summary>
|
||||||
@@ -11,6 +12,7 @@ public interface IGalaxyHierarchyCache
|
|||||||
/// attributes rowsets when the deploy time has changed since the last successful
|
/// attributes rowsets when the deploy time has changed since the last successful
|
||||||
/// refresh.
|
/// refresh.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||||
Task RefreshAsync(CancellationToken cancellationToken);
|
Task RefreshAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -18,5 +20,6 @@ public interface IGalaxyHierarchyCache
|
|||||||
/// gRPC handlers that want to serve from cache without returning Unavailable on the
|
/// gRPC handlers that want to serve from cache without returning Unavailable on the
|
||||||
/// very first request after gateway start.
|
/// very first request after gateway start.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||||
Task WaitForFirstLoadAsync(CancellationToken cancellationToken);
|
Task WaitForFirstLoadAsync(CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,18 @@ using MxGateway.Server.Workers;
|
|||||||
|
|
||||||
namespace MxGateway.Server;
|
namespace MxGateway.Server;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures and builds the gateway web application.
|
||||||
|
/// </summary>
|
||||||
public static class GatewayApplication
|
public static class GatewayApplication
|
||||||
{
|
{
|
||||||
private const string StaticAssetsManifestFileName = "MxGateway.Server.staticwebassets.endpoints.json";
|
private const string StaticAssetsManifestFileName = "MxGateway.Server.staticwebassets.endpoints.json";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a configured web application with all gateway services and middleware.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command-line arguments passed to the application.</param>
|
||||||
|
/// <returns>A configured web application ready to run.</returns>
|
||||||
public static WebApplication Build(string[] args)
|
public static WebApplication Build(string[] args)
|
||||||
{
|
{
|
||||||
WebApplicationBuilder builder = CreateBuilder(args);
|
WebApplicationBuilder builder = CreateBuilder(args);
|
||||||
@@ -32,6 +40,11 @@ public static class GatewayApplication
|
|||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a web application builder configured with gateway services.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command-line arguments passed to the application.</param>
|
||||||
|
/// <returns>A configured web application builder.</returns>
|
||||||
public static WebApplicationBuilder CreateBuilder(string[] args)
|
public static WebApplicationBuilder CreateBuilder(string[] args)
|
||||||
{
|
{
|
||||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions
|
WebApplicationBuilder builder = WebApplication.CreateBuilder(new WebApplicationOptions
|
||||||
@@ -112,6 +125,11 @@ public static class GatewayApplication
|
|||||||
&& Directory.Exists(Path.Combine(path, "wwwroot"));
|
&& Directory.Exists(Path.Combine(path, "wwwroot"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps gateway endpoints including gRPC services, health checks, and the dashboard.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="endpoints">Endpoint route builder to map endpoints to.</param>
|
||||||
|
/// <returns>The same endpoint route builder for chaining.</returns>
|
||||||
public static IEndpointRouteBuilder MapGatewayEndpoints(this IEndpointRouteBuilder endpoints)
|
public static IEndpointRouteBuilder MapGatewayEndpoints(this IEndpointRouteBuilder endpoints)
|
||||||
{
|
{
|
||||||
endpoints.MapStaticAssets(ResolveStaticAssetsManifestPath());
|
endpoints.MapStaticAssets(ResolveStaticAssetsManifestPath());
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ public sealed class EventStreamService(
|
|||||||
GatewayMetrics metrics,
|
GatewayMetrics metrics,
|
||||||
ILogger<EventStreamService> logger) : IEventStreamService
|
ILogger<EventStreamService> logger) : IEventStreamService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Streams events from a session to the client asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Stream events request.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Async enumerable of MX events.</returns>
|
||||||
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
public async IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
[EnumeratorCancellation] CancellationToken cancellationToken)
|
[EnumeratorCancellation] CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ namespace MxGateway.Server.Grpc;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class GalaxyProtoMapper
|
public static class GalaxyProtoMapper
|
||||||
{
|
{
|
||||||
|
/// <summary>Maps Galaxy hierarchy and attribute rows to Galaxy object protos.</summary>
|
||||||
|
/// <param name="hierarchy">Hierarchy rows from Galaxy Repository.</param>
|
||||||
|
/// <param name="attributes">Attribute rows from Galaxy Repository.</param>
|
||||||
public static IEnumerable<GalaxyObject> MapHierarchy(
|
public static IEnumerable<GalaxyObject> MapHierarchy(
|
||||||
IReadOnlyList<GalaxyHierarchyRow> hierarchy,
|
IReadOnlyList<GalaxyHierarchyRow> hierarchy,
|
||||||
IReadOnlyList<GalaxyAttributeRow> attributes)
|
IReadOnlyList<GalaxyAttributeRow> attributes)
|
||||||
@@ -24,6 +27,9 @@ public static class GalaxyProtoMapper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Maps a Galaxy hierarchy row to a Galaxy object proto.</summary>
|
||||||
|
/// <param name="row">Hierarchy row from Galaxy Repository.</param>
|
||||||
|
/// <param name="attributesByGobjectId">Attributes indexed by gobject ID.</param>
|
||||||
public static GalaxyObject MapObject(
|
public static GalaxyObject MapObject(
|
||||||
GalaxyHierarchyRow row,
|
GalaxyHierarchyRow row,
|
||||||
IReadOnlyDictionary<int, List<GalaxyAttributeRow>> attributesByGobjectId)
|
IReadOnlyDictionary<int, List<GalaxyAttributeRow>> attributesByGobjectId)
|
||||||
@@ -52,6 +58,8 @@ public static class GalaxyProtoMapper
|
|||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Maps a Galaxy attribute row to a Galaxy attribute proto.</summary>
|
||||||
|
/// <param name="row">Attribute row from Galaxy Repository.</param>
|
||||||
public static GalaxyAttribute MapAttribute(GalaxyAttributeRow row) => new()
|
public static GalaxyAttribute MapAttribute(GalaxyAttributeRow row) => new()
|
||||||
{
|
{
|
||||||
AttributeName = row.AttributeName,
|
AttributeName = row.AttributeName,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
{
|
{
|
||||||
private static readonly TimeSpan FirstLoadWaitBudget = TimeSpan.FromSeconds(5);
|
private static readonly TimeSpan FirstLoadWaitBudget = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<TestConnectionReply> TestConnection(
|
public override async Task<TestConnectionReply> TestConnection(
|
||||||
TestConnectionRequest request,
|
TestConnectionRequest request,
|
||||||
ServerCallContext context)
|
ServerCallContext context)
|
||||||
@@ -30,6 +31,7 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
return new TestConnectionReply { Ok = ok };
|
return new TestConnectionReply { Ok = ok };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<GetLastDeployTimeReply> GetLastDeployTime(
|
public override async Task<GetLastDeployTimeReply> GetLastDeployTime(
|
||||||
GetLastDeployTimeRequest request,
|
GetLastDeployTimeRequest request,
|
||||||
ServerCallContext context)
|
ServerCallContext context)
|
||||||
@@ -52,6 +54,7 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
return reply;
|
return reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<DiscoverHierarchyReply> DiscoverHierarchy(
|
public override async Task<DiscoverHierarchyReply> DiscoverHierarchy(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
ServerCallContext context)
|
ServerCallContext context)
|
||||||
@@ -71,6 +74,7 @@ public sealed class GalaxyRepositoryGrpcService(
|
|||||||
return entry.Reply;
|
return entry.Reply;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task WatchDeployEvents(
|
public override async Task WatchDeployEvents(
|
||||||
WatchDeployEventsRequest request,
|
WatchDeployEventsRequest request,
|
||||||
IServerStreamWriter<DeployEvent> responseStream,
|
IServerStreamWriter<DeployEvent> responseStream,
|
||||||
|
|||||||
@@ -2,8 +2,16 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Grpc;
|
namespace MxGateway.Server.Grpc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Streams MXAccess events to gRPC clients.
|
||||||
|
/// </summary>
|
||||||
public interface IEventStreamService
|
public interface IEventStreamService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Streams events for the specified session to the caller.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request payload.</param>
|
||||||
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||||
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
IAsyncEnumerable<MxEvent> StreamEventsAsync(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
CancellationToken cancellationToken);
|
CancellationToken cancellationToken);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using MxGateway.Server.Workers;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Grpc;
|
namespace MxGateway.Server.Grpc;
|
||||||
|
|
||||||
|
/// <summary>gRPC service implementation for MXAccess Gateway operations.</summary>
|
||||||
public sealed class MxAccessGatewayService(
|
public sealed class MxAccessGatewayService(
|
||||||
ISessionManager sessionManager,
|
ISessionManager sessionManager,
|
||||||
IGatewayRequestIdentityAccessor identityAccessor,
|
IGatewayRequestIdentityAccessor identityAccessor,
|
||||||
@@ -18,6 +19,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
GatewayMetrics metrics,
|
GatewayMetrics metrics,
|
||||||
ILogger<MxAccessGatewayService> logger) : MxAccessGateway.MxAccessGatewayBase
|
ILogger<MxAccessGatewayService> logger) : MxAccessGateway.MxAccessGatewayBase
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<OpenSessionReply> OpenSession(
|
public override async Task<OpenSessionReply> OpenSession(
|
||||||
OpenSessionRequest request,
|
OpenSessionRequest request,
|
||||||
ServerCallContext context)
|
ServerCallContext context)
|
||||||
@@ -56,6 +58,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<CloseSessionReply> CloseSession(
|
public override async Task<CloseSessionReply> CloseSession(
|
||||||
CloseSessionRequest request,
|
CloseSessionRequest request,
|
||||||
ServerCallContext context)
|
ServerCallContext context)
|
||||||
@@ -80,6 +83,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task<MxCommandReply> Invoke(
|
public override async Task<MxCommandReply> Invoke(
|
||||||
MxCommandRequest request,
|
MxCommandRequest request,
|
||||||
ServerCallContext context)
|
ServerCallContext context)
|
||||||
@@ -100,6 +104,7 @@ public sealed class MxAccessGatewayService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override async Task StreamEvents(
|
public override async Task StreamEvents(
|
||||||
StreamEventsRequest request,
|
StreamEventsRequest request,
|
||||||
IServerStreamWriter<MxEvent> responseStream,
|
IServerStreamWriter<MxEvent> responseStream,
|
||||||
|
|||||||
@@ -3,15 +3,26 @@ using MxGateway.Contracts.Proto;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Grpc;
|
namespace MxGateway.Server.Grpc;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps between worker IPC types and gRPC contract types.
|
||||||
|
/// </summary>
|
||||||
public sealed class MxAccessGrpcMapper
|
public sealed class MxAccessGrpcMapper
|
||||||
{
|
{
|
||||||
private readonly TimeProvider _timeProvider;
|
private readonly TimeProvider _timeProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the mapper with an optional time provider.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="timeProvider">Time provider for timestamps; defaults to system time if null.</param>
|
||||||
public MxAccessGrpcMapper(TimeProvider? timeProvider = null)
|
public MxAccessGrpcMapper(TimeProvider? timeProvider = null)
|
||||||
{
|
{
|
||||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a gRPC MX command request to a worker command.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request payload.</param>
|
||||||
public WorkerCommand MapCommand(MxCommandRequest request)
|
public WorkerCommand MapCommand(MxCommandRequest request)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
@@ -24,6 +35,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a worker command reply to a gRPC MX command reply.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reply">Worker command reply.</param>
|
||||||
public MxCommandReply MapCommandReply(WorkerCommandReply reply)
|
public MxCommandReply MapCommandReply(WorkerCommandReply reply)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(reply);
|
ArgumentNullException.ThrowIfNull(reply);
|
||||||
@@ -39,6 +54,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
return reply.Reply.Clone();
|
return reply.Reply.Clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a worker event to a gRPC MX event.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="workerEvent">Worker event to map.</param>
|
||||||
public MxEvent MapEvent(WorkerEvent workerEvent)
|
public MxEvent MapEvent(WorkerEvent workerEvent)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(workerEvent);
|
ArgumentNullException.ThrowIfNull(workerEvent);
|
||||||
@@ -50,6 +69,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an OK protocol status.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Status message.</param>
|
||||||
public static ProtocolStatus Ok(string message = "OK")
|
public static ProtocolStatus Ok(string message = "OK")
|
||||||
{
|
{
|
||||||
return new ProtocolStatus
|
return new ProtocolStatus
|
||||||
@@ -59,6 +82,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an InvalidRequest protocol status.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Status message.</param>
|
||||||
public static ProtocolStatus InvalidRequest(string message)
|
public static ProtocolStatus InvalidRequest(string message)
|
||||||
{
|
{
|
||||||
return new ProtocolStatus
|
return new ProtocolStatus
|
||||||
@@ -68,6 +95,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a SessionNotFound protocol status.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Status message.</param>
|
||||||
public static ProtocolStatus SessionNotFound(string message)
|
public static ProtocolStatus SessionNotFound(string message)
|
||||||
{
|
{
|
||||||
return new ProtocolStatus
|
return new ProtocolStatus
|
||||||
@@ -77,6 +108,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a SessionNotReady protocol status.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Status message.</param>
|
||||||
public static ProtocolStatus SessionNotReady(string message)
|
public static ProtocolStatus SessionNotReady(string message)
|
||||||
{
|
{
|
||||||
return new ProtocolStatus
|
return new ProtocolStatus
|
||||||
@@ -86,6 +121,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a WorkerUnavailable protocol status.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Status message.</param>
|
||||||
public static ProtocolStatus WorkerUnavailable(string message)
|
public static ProtocolStatus WorkerUnavailable(string message)
|
||||||
{
|
{
|
||||||
return new ProtocolStatus
|
return new ProtocolStatus
|
||||||
@@ -95,6 +134,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Timeout protocol status.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Status message.</param>
|
||||||
public static ProtocolStatus Timeout(string message)
|
public static ProtocolStatus Timeout(string message)
|
||||||
{
|
{
|
||||||
return new ProtocolStatus
|
return new ProtocolStatus
|
||||||
@@ -104,6 +147,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a Canceled protocol status.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Status message.</param>
|
||||||
public static ProtocolStatus Canceled(string message)
|
public static ProtocolStatus Canceled(string message)
|
||||||
{
|
{
|
||||||
return new ProtocolStatus
|
return new ProtocolStatus
|
||||||
@@ -113,6 +160,10 @@ public sealed class MxAccessGrpcMapper
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a ProtocolViolation protocol status.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">Status message.</param>
|
||||||
public static ProtocolStatus ProtocolViolation(string message)
|
public static ProtocolStatus ProtocolViolation(string message)
|
||||||
{
|
{
|
||||||
return new ProtocolStatus
|
return new ProtocolStatus
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ namespace MxGateway.Server.Grpc;
|
|||||||
|
|
||||||
public sealed class MxAccessGrpcRequestValidator
|
public sealed class MxAccessGrpcRequestValidator
|
||||||
{
|
{
|
||||||
|
/// <summary>Validates an open session request.</summary>
|
||||||
|
/// <param name="request">The request to validate.</param>
|
||||||
public void ValidateOpenSession(OpenSessionRequest request)
|
public void ValidateOpenSession(OpenSessionRequest request)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
@@ -15,18 +17,24 @@ public sealed class MxAccessGrpcRequestValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Validates a close session request.</summary>
|
||||||
|
/// <param name="request">The request to validate.</param>
|
||||||
public void ValidateCloseSession(CloseSessionRequest request)
|
public void ValidateCloseSession(CloseSessionRequest request)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
RequireSessionId(request.SessionId);
|
RequireSessionId(request.SessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Validates a stream events request.</summary>
|
||||||
|
/// <param name="request">The request to validate.</param>
|
||||||
public void ValidateStreamEvents(StreamEventsRequest request)
|
public void ValidateStreamEvents(StreamEventsRequest request)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
RequireSessionId(request.SessionId);
|
RequireSessionId(request.SessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Validates an invoke request with command payload.</summary>
|
||||||
|
/// <param name="request">The request to validate.</param>
|
||||||
public void ValidateInvoke(MxCommandRequest request)
|
public void ValidateInvoke(MxCommandRequest request)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
private long _retryAttempts;
|
private long _retryAttempts;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes the gateway metrics with OpenTelemetry counters and histograms.
|
||||||
|
/// </summary>
|
||||||
public GatewayMetrics()
|
public GatewayMetrics()
|
||||||
{
|
{
|
||||||
_meter = new Meter(MeterName, typeof(GatewayMetrics).Assembly.GetName().Version?.ToString());
|
_meter = new Meter(MeterName, typeof(GatewayMetrics).Assembly.GetName().Version?.ToString());
|
||||||
@@ -75,6 +78,9 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_meter.CreateObservableGauge("mxgateway.events.grpc_stream_queue.depth", GetGrpcEventStreamQueueDepth);
|
_meter.CreateObservableGauge("mxgateway.events.grpc_stream_queue.depth", GetGrpcEventStreamQueueDepth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a session has been opened.
|
||||||
|
/// </summary>
|
||||||
public void SessionOpened()
|
public void SessionOpened()
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -86,6 +92,9 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_sessionsOpenedCounter.Add(1);
|
_sessionsOpenedCounter.Add(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a session has been closed.
|
||||||
|
/// </summary>
|
||||||
public void SessionClosed()
|
public void SessionClosed()
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -101,6 +110,9 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_sessionsClosedCounter.Add(1);
|
_sessionsClosedCounter.Add(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a session has been removed from registry.
|
||||||
|
/// </summary>
|
||||||
public void SessionRemoved()
|
public void SessionRemoved()
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -112,6 +124,10 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a worker process has started and its startup latency.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="startupDuration">Duration elapsed while starting the worker.</param>
|
||||||
public void WorkerStarted(TimeSpan startupDuration)
|
public void WorkerStarted(TimeSpan startupDuration)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -122,6 +138,10 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_workerStartupLatencyHistogram.Record(startupDuration.TotalMilliseconds);
|
_workerStartupLatencyHistogram.Record(startupDuration.TotalMilliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a worker process has stopped with the given reason.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reason">Cause of the worker stopping.</param>
|
||||||
public void WorkerStopped(string reason)
|
public void WorkerStopped(string reason)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -137,6 +157,10 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_workerExitsCounter.Add(1, new KeyValuePair<string, object?>("reason", reason));
|
_workerExitsCounter.Add(1, new KeyValuePair<string, object?>("reason", reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a worker process was killed with the given reason.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reason">Cause of the worker termination.</param>
|
||||||
public void WorkerKilled(string reason)
|
public void WorkerKilled(string reason)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -147,6 +171,10 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_workerKillsCounter.Add(1, new KeyValuePair<string, object?>("reason", reason));
|
_workerKillsCounter.Add(1, new KeyValuePair<string, object?>("reason", reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a command has started for the given method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">Name of the command method.</param>
|
||||||
public void CommandStarted(string method)
|
public void CommandStarted(string method)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -157,6 +185,11 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_commandsStartedCounter.Add(1, new KeyValuePair<string, object?>("method", method));
|
_commandsStartedCounter.Add(1, new KeyValuePair<string, object?>("method", method));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a command succeeded for the given method and duration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">Name of the command method.</param>
|
||||||
|
/// <param name="duration">Elapsed time to complete the command.</param>
|
||||||
public void CommandSucceeded(string method, TimeSpan duration)
|
public void CommandSucceeded(string method, TimeSpan duration)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -169,6 +202,12 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_commandLatencyHistogram.Record(duration.TotalMilliseconds, methodTag);
|
_commandLatencyHistogram.Record(duration.TotalMilliseconds, methodTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a command failed for the given method, category, and duration.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="method">Name of the command method.</param>
|
||||||
|
/// <param name="category">Classification of the failure.</param>
|
||||||
|
/// <param name="duration">Elapsed time before command failed.</param>
|
||||||
public void CommandFailed(string method, string category, TimeSpan duration)
|
public void CommandFailed(string method, string category, TimeSpan duration)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -183,6 +222,11 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_commandLatencyHistogram.Record(duration.TotalMilliseconds, methodTag, categoryTag);
|
_commandLatencyHistogram.Record(duration.TotalMilliseconds, methodTag, categoryTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that an event was received for the given session and family.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionId">Identifier of the session receiving the event.</param>
|
||||||
|
/// <param name="family">Event family classification.</param>
|
||||||
public void EventReceived(string sessionId, string family)
|
public void EventReceived(string sessionId, string family)
|
||||||
{
|
{
|
||||||
Interlocked.Increment(ref _eventsReceived);
|
Interlocked.Increment(ref _eventsReceived);
|
||||||
@@ -194,6 +238,11 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
new KeyValuePair<string, object?>("family", family));
|
new KeyValuePair<string, object?>("family", family));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records the latency of sending an event to a client stream.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="family">Event family name.</param>
|
||||||
|
/// <param name="duration">Time taken to send the event.</param>
|
||||||
public void RecordEventStreamSend(string family, TimeSpan duration)
|
public void RecordEventStreamSend(string family, TimeSpan duration)
|
||||||
{
|
{
|
||||||
_eventStreamSendLatencyHistogram.Record(
|
_eventStreamSendLatencyHistogram.Record(
|
||||||
@@ -201,11 +250,19 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
new KeyValuePair<string, object?>("family", family));
|
new KeyValuePair<string, object?>("family", family));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the worker event queue depth; delegates to SetWorkerEventQueueDepth.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="depth">Queue depth value.</param>
|
||||||
public void SetEventQueueDepth(int depth)
|
public void SetEventQueueDepth(int depth)
|
||||||
{
|
{
|
||||||
SetWorkerEventQueueDepth(depth);
|
SetWorkerEventQueueDepth(depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the worker event queue depth to the given value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="depth">Queue depth value.</param>
|
||||||
public void SetWorkerEventQueueDepth(int depth)
|
public void SetWorkerEventQueueDepth(int depth)
|
||||||
{
|
{
|
||||||
if (depth < 0)
|
if (depth < 0)
|
||||||
@@ -219,6 +276,10 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adjusts the gRPC event stream queue depth by the given delta.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="delta">Amount to adjust the queue depth by.</param>
|
||||||
public void AdjustGrpcEventStreamQueueDepth(int delta)
|
public void AdjustGrpcEventStreamQueueDepth(int delta)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -227,11 +288,19 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes event counters for the given session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionId">Identifier of the session.</param>
|
||||||
public void RemoveSessionEvents(string sessionId)
|
public void RemoveSessionEvents(string sessionId)
|
||||||
{
|
{
|
||||||
_eventsBySession.TryRemove(sessionId, out _);
|
_eventsBySession.TryRemove(sessionId, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a queue overflow occurred for the given queue name.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queueName">Name of the queue that overflowed.</param>
|
||||||
public void QueueOverflow(string queueName)
|
public void QueueOverflow(string queueName)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -242,6 +311,10 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_queueOverflowsCounter.Add(1, new KeyValuePair<string, object?>("queue", queueName));
|
_queueOverflowsCounter.Add(1, new KeyValuePair<string, object?>("queue", queueName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a fault occurred in the given category.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="category">Category of the fault.</param>
|
||||||
public void Fault(string category)
|
public void Fault(string category)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -252,6 +325,10 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_faultsCounter.Add(1, new KeyValuePair<string, object?>("category", category));
|
_faultsCounter.Add(1, new KeyValuePair<string, object?>("category", category));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a heartbeat failed for the given session.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sessionId">Identifier of the session.</param>
|
||||||
public void HeartbeatFailed(string sessionId)
|
public void HeartbeatFailed(string sessionId)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -262,6 +339,10 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_heartbeatFailuresCounter.Add(1, new KeyValuePair<string, object?>("session_id", sessionId));
|
_heartbeatFailuresCounter.Add(1, new KeyValuePair<string, object?>("session_id", sessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that an event stream was disconnected with the given reason.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="reason">Reason for the disconnection.</param>
|
||||||
public void StreamDisconnected(string reason)
|
public void StreamDisconnected(string reason)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -272,6 +353,10 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_streamDisconnectsCounter.Add(1, new KeyValuePair<string, object?>("reason", reason));
|
_streamDisconnectsCounter.Add(1, new KeyValuePair<string, object?>("reason", reason));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records that a retry was attempted in the given area.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="area">Area in which the retry was attempted.</param>
|
||||||
public void RetryAttempted(string area)
|
public void RetryAttempted(string area)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -283,6 +368,9 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
_retryAttemptsCounter.Add(1, new KeyValuePair<string, object?>("area", area));
|
_retryAttemptsCounter.Add(1, new KeyValuePair<string, object?>("area", area));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a snapshot of all current metric values.
|
||||||
|
/// </summary>
|
||||||
public GatewayMetricsSnapshot GetSnapshot()
|
public GatewayMetricsSnapshot GetSnapshot()
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -312,6 +400,9 @@ public sealed class GatewayMetrics : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes the underlying OpenTelemetry meter.
|
||||||
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ using System.Text.Json;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Security.Authentication;
|
namespace MxGateway.Server.Security.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes API key administration commands from the CLI.
|
||||||
|
/// </summary>
|
||||||
public sealed class ApiKeyAdminCliRunner(
|
public sealed class ApiKeyAdminCliRunner(
|
||||||
IAuthStoreMigrator migrator,
|
IAuthStoreMigrator migrator,
|
||||||
IApiKeyAdminStore adminStore,
|
IApiKeyAdminStore adminStore,
|
||||||
@@ -13,6 +16,12 @@ public sealed class ApiKeyAdminCliRunner(
|
|||||||
WriteIndented = true
|
WriteIndented = true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs an API key administration command and writes the output.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="command">API key administration command to execute.</param>
|
||||||
|
/// <param name="output">Text writer for command output.</param>
|
||||||
|
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
|
||||||
public async Task<int> RunAsync(
|
public async Task<int> RunAsync(
|
||||||
ApiKeyAdminCommand command,
|
ApiKeyAdminCommand command,
|
||||||
TextWriter output,
|
TextWriter output,
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ namespace MxGateway.Server.Security.Authentication;
|
|||||||
|
|
||||||
public static class ApiKeyAdminCommandLineParser
|
public static class ApiKeyAdminCommandLineParser
|
||||||
{
|
{
|
||||||
|
/// <summary>Parses command-line arguments for the API key admin subcommand.</summary>
|
||||||
|
/// <param name="args">Command-line arguments to parse.</param>
|
||||||
|
/// <returns>Parse result containing the command kind and options, or a failure message.</returns>
|
||||||
public static ApiKeyAdminParseResult Parse(IReadOnlyList<string> args)
|
public static ApiKeyAdminParseResult Parse(IReadOnlyList<string> args)
|
||||||
{
|
{
|
||||||
if (args.Count == 0 || !string.Equals(args[0], "apikey", StringComparison.OrdinalIgnoreCase))
|
if (args.Count == 0 || !string.Equals(args[0], "apikey", StringComparison.OrdinalIgnoreCase))
|
||||||
|
|||||||
@@ -5,16 +5,21 @@ public sealed record ApiKeyAdminParseResult(
|
|||||||
ApiKeyAdminCommand? Command,
|
ApiKeyAdminCommand? Command,
|
||||||
string? Error)
|
string? Error)
|
||||||
{
|
{
|
||||||
|
/// <summary>Returns a result indicating the input was not an API key command.</summary>
|
||||||
public static ApiKeyAdminParseResult NotApiKeyCommand()
|
public static ApiKeyAdminParseResult NotApiKeyCommand()
|
||||||
{
|
{
|
||||||
return new ApiKeyAdminParseResult(false, null, null);
|
return new ApiKeyAdminParseResult(false, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns a successful parse result with the parsed API key command.</summary>
|
||||||
|
/// <param name="command">Parsed API key administration command.</param>
|
||||||
public static ApiKeyAdminParseResult Success(ApiKeyAdminCommand command)
|
public static ApiKeyAdminParseResult Success(ApiKeyAdminCommand command)
|
||||||
{
|
{
|
||||||
return new ApiKeyAdminParseResult(true, command, null);
|
return new ApiKeyAdminParseResult(true, command, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns a parse result with the specified error message.</summary>
|
||||||
|
/// <param name="error">Error message describing the parse failure.</param>
|
||||||
public static ApiKeyAdminParseResult Fail(string error)
|
public static ApiKeyAdminParseResult Fail(string error)
|
||||||
{
|
{
|
||||||
return new ApiKeyAdminParseResult(true, null, error);
|
return new ApiKeyAdminParseResult(true, null, error);
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ public sealed class ApiKeyParser : IApiKeyParser
|
|||||||
private const string BearerPrefix = "Bearer ";
|
private const string BearerPrefix = "Bearer ";
|
||||||
private const string TokenPrefix = "mxgw_";
|
private const string TokenPrefix = "mxgw_";
|
||||||
|
|
||||||
|
/// <summary>Attempts to parse a Bearer token from an Authorization header and extract the API key ID and secret.</summary>
|
||||||
|
/// <param name="authorizationHeader">Authorization header value to parse.</param>
|
||||||
|
/// <param name="apiKey">Parsed API key with ID and secret, or null if parsing failed.</param>
|
||||||
|
/// <returns>True if the header was successfully parsed; otherwise, false.</returns>
|
||||||
public bool TryParseAuthorizationHeader(string? authorizationHeader, out ParsedApiKey? apiKey)
|
public bool TryParseAuthorizationHeader(string? authorizationHeader, out ParsedApiKey? apiKey)
|
||||||
{
|
{
|
||||||
apiKey = null;
|
apiKey = null;
|
||||||
|
|||||||
@@ -2,8 +2,12 @@ using Microsoft.Data.Sqlite;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Security.Authentication;
|
namespace MxGateway.Server.Security.Authentication;
|
||||||
|
|
||||||
|
/// <summary>Reads API key records from SQLite query results.</summary>
|
||||||
public static class ApiKeyRecordReader
|
public static class ApiKeyRecordReader
|
||||||
{
|
{
|
||||||
|
/// <summary>Deserializes a row from the API key table into an ApiKeyRecord.</summary>
|
||||||
|
/// <param name="reader">The data reader positioned at the API key row.</param>
|
||||||
|
/// <returns>The deserialized API key record.</returns>
|
||||||
public static ApiKeyRecord Read(SqliteDataReader reader)
|
public static ApiKeyRecord Read(SqliteDataReader reader)
|
||||||
{
|
{
|
||||||
return new ApiKeyRecord(
|
return new ApiKeyRecord(
|
||||||
|
|||||||
@@ -4,11 +4,17 @@ namespace MxGateway.Server.Security.Authentication;
|
|||||||
|
|
||||||
public static class ApiKeyScopeSerializer
|
public static class ApiKeyScopeSerializer
|
||||||
{
|
{
|
||||||
|
/// <summary>Serializes scopes to JSON string.</summary>
|
||||||
|
/// <param name="scopes">The scopes to serialize.</param>
|
||||||
|
/// <returns>JSON string representation.</returns>
|
||||||
public static string Serialize(IReadOnlySet<string> scopes)
|
public static string Serialize(IReadOnlySet<string> scopes)
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(scopes.Order(StringComparer.Ordinal));
|
return JsonSerializer.Serialize(scopes.Order(StringComparer.Ordinal));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Deserializes scopes from JSON string.</summary>
|
||||||
|
/// <param name="value">The JSON string to deserialize.</param>
|
||||||
|
/// <returns>Deserialized scopes set.</returns>
|
||||||
public static IReadOnlySet<string> Deserialize(string value)
|
public static IReadOnlySet<string> Deserialize(string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ using System.Security.Cryptography;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Security.Authentication;
|
namespace MxGateway.Server.Security.Authentication;
|
||||||
|
|
||||||
|
/// <summary>Generates cryptographically secure API key secrets.</summary>
|
||||||
public static class ApiKeySecretGenerator
|
public static class ApiKeySecretGenerator
|
||||||
{
|
{
|
||||||
|
/// <summary>Generates a new random API key secret string.</summary>
|
||||||
public static string Generate()
|
public static string Generate()
|
||||||
{
|
{
|
||||||
Span<byte> bytes = stackalloc byte[32];
|
Span<byte> bytes = stackalloc byte[32];
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ public sealed class ApiKeySecretHasher(
|
|||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
IOptions<GatewayOptions> options) : IApiKeySecretHasher
|
IOptions<GatewayOptions> options) : IApiKeySecretHasher
|
||||||
{
|
{
|
||||||
|
/// <summary>Hashes an API key secret with pepper using HMAC-SHA256.</summary>
|
||||||
|
/// <param name="secret">The secret to hash.</param>
|
||||||
|
/// <returns>The hashed secret.</returns>
|
||||||
public byte[] HashSecret(string secret)
|
public byte[] HashSecret(string secret)
|
||||||
{
|
{
|
||||||
string pepper = GetPepper();
|
string pepper = GetPepper();
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ public sealed record ApiKeyVerificationResult(
|
|||||||
ApiKeyIdentity? Identity,
|
ApiKeyIdentity? Identity,
|
||||||
ApiKeyVerificationFailure Failure)
|
ApiKeyVerificationFailure Failure)
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a successful verification result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="identity">API key identity.</param>
|
||||||
|
/// <returns>Success result.</returns>
|
||||||
public static ApiKeyVerificationResult Success(ApiKeyIdentity identity)
|
public static ApiKeyVerificationResult Success(ApiKeyIdentity identity)
|
||||||
{
|
{
|
||||||
return new ApiKeyVerificationResult(
|
return new ApiKeyVerificationResult(
|
||||||
@@ -13,6 +18,11 @@ public sealed record ApiKeyVerificationResult(
|
|||||||
Failure: ApiKeyVerificationFailure.None);
|
Failure: ApiKeyVerificationFailure.None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a failed verification result.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="failure">Verification failure reason.</param>
|
||||||
|
/// <returns>Failure result.</returns>
|
||||||
public static ApiKeyVerificationResult Fail(ApiKeyVerificationFailure failure)
|
public static ApiKeyVerificationResult Fail(ApiKeyVerificationFailure failure)
|
||||||
{
|
{
|
||||||
return new ApiKeyVerificationResult(
|
return new ApiKeyVerificationResult(
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ public sealed class ApiKeyVerifier(
|
|||||||
IApiKeySecretHasher hasher,
|
IApiKeySecretHasher hasher,
|
||||||
IApiKeyStore keyStore) : IApiKeyVerifier
|
IApiKeyStore keyStore) : IApiKeyVerifier
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies an API key from an authorization header asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorizationHeader">Authorization header value.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Verification result.</returns>
|
||||||
public async Task<ApiKeyVerificationResult> VerifyAsync(
|
public async Task<ApiKeyVerificationResult> VerifyAsync(
|
||||||
string? authorizationHeader,
|
string? authorizationHeader,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -4,8 +4,14 @@ using MxGateway.Server.Configuration;
|
|||||||
|
|
||||||
namespace MxGateway.Server.Security.Authentication;
|
namespace MxGateway.Server.Security.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for creating SQLite connections to the authentication store.
|
||||||
|
/// </summary>
|
||||||
public sealed class AuthSqliteConnectionFactory(IOptions<GatewayOptions> options)
|
public sealed class AuthSqliteConnectionFactory(IOptions<GatewayOptions> options)
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Creates and configures a SQLite connection to the auth database.
|
||||||
|
/// </summary>
|
||||||
public SqliteConnection CreateConnection()
|
public SqliteConnection CreateConnection()
|
||||||
{
|
{
|
||||||
string sqlitePath = options.Value.Authentication.SqlitePath;
|
string sqlitePath = options.Value.Authentication.SqlitePath;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user