using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser;
///
/// Opens transient gateway connections for the AdminUI address picker. Mirrors the
/// runtime GalaxyDriver.BuildClientOptions pattern so the gateway sees the
/// same option shape, but tweaks
/// resolution + the gateway-side client identity so browse sessions are
/// distinguishable from the runtime driver's live MX session.
///
public sealed class GalaxyDriverBrowser : IDriverBrowser
{
///
/// Identifier used in gateway-side logs / metrics for AdminUI browse sessions.
/// Distinct from any runtime driver's MxAccess.ClientName so an operator
/// can tell the two apart when triaging.
///
internal const string BrowseClientIdentity = "OtOpcUa-AdminUI-Browse";
/// Hard cap on the time we'll wait for the initial gateway handshake.
private static readonly TimeSpan ConnectBudget = TimeSpan.FromSeconds(30);
private static readonly JsonSerializerOptions JsonOpts = new()
{
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
PropertyNameCaseInsensitive = true,
};
private readonly ILogger _logger;
/// Creates a new browser. Logger defaults to .
/// Optional logger; null is allowed for unit-test construction.
public GalaxyDriverBrowser(ILogger? logger = null)
{
_logger = logger ?? NullLogger.Instance;
}
///
// Hardcoded literal: this project references Driver.Galaxy.Contracts, not Driver.Galaxy,
// so GalaxyDriverFactoryExtensions.DriverTypeName isn't available here.
public string DriverType => "GalaxyMxGateway";
// Deserializes a GalaxyDriverOptions blob, opens a transient GalaxyRepositoryClient
// against the configured gateway endpoint, and returns a browse session over it. The
// session owns the client and disposes it on IBrowseSession.DisposeAsync. Throws
// InvalidOperationException when the JSON deserialises to null, when Gateway.Endpoint
// is empty, or when MxAccess.ClientName is empty.
///
public async Task OpenAsync(string configJson, CancellationToken cancellationToken)
{
var opts = JsonSerializer.Deserialize(configJson, JsonOpts)
?? throw new InvalidOperationException("Galaxy options deserialized to null.");
if (string.IsNullOrWhiteSpace(opts.Gateway.Endpoint))
throw new InvalidOperationException("Galaxy browser requires Gateway.Endpoint.");
// The form persists MXAccess identity as ClientName (there is no separate
// "galaxy name" knob on the driver — the gateway picks the galaxy via its
// own GalaxyRepository config). Refuse a blank ClientName so the gateway side
// doesn't see anonymous browse sessions during triage.
if (string.IsNullOrWhiteSpace(opts.MxAccess.ClientName))
throw new InvalidOperationException("Galaxy browser requires MxAccess.ClientName.");
var clientOpts = BuildClientOptions(opts.Gateway);
// 30s wall-clock budget for the connect phase, linked to the caller's token so
// an AdminUI cancel still wins early.
using var connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
connectCts.CancelAfter(ConnectBudget);
GalaxyRepositoryClient? client = null;
try
{
client = GalaxyRepositoryClient.Create(clientOpts);
// TestConnectionAsync gives the gateway a chance to surface auth / TLS / DNS
// failures synchronously inside the connect budget rather than waiting for
// the first DiscoverHierarchyAsync call to fail. The client's own
// ConnectTimeout already bounds the underlying gRPC handshake; the linked
// CTS layered on top guarantees the AdminUI never blocks past 30s.
await client.TestConnectionAsync(connectCts.Token).ConfigureAwait(false);
_logger.LogInformation(
"AdminUI Galaxy browse session opened against {Endpoint} (admin-client={Identity}, runtime-client={RuntimeClient})",
opts.Gateway.Endpoint, BrowseClientIdentity, opts.MxAccess.ClientName);
var session = new GalaxyBrowseSession(client);
client = null; // Ownership transferred — keep finally from disposing.
return session;
}
catch
{
if (client is not null)
{
try
{
await client.DisposeAsync().ConfigureAwait(false);
}
catch
{
// Best-effort cleanup; the original exception is more useful.
}
}
throw;
}
}
///
/// Build the gateway client options from the form's Gateway section. Mirrors the
/// runtime driver's GalaxyDriver.BuildClientOptions field-for-field so the
/// gateway sees an identical option shape. The API-key reference is resolved via
/// the shared in Driver.Galaxy.Contracts
/// (the same resolver the runtime driver uses), so browse and runtime stay in lock-step.
///
private MxGatewayClientOptions BuildClientOptions(GalaxyGatewayOptions gw) => new()
{
Endpoint = new Uri(gw.Endpoint, UriKind.Absolute),
ApiKey = GalaxySecretRef.ResolveApiKey(gw.ApiKeySecretRef, _logger),
UseTls = gw.UseTls,
CaCertificatePath = gw.CaCertificatePath,
ConnectTimeout = TimeSpan.FromSeconds(gw.ConnectTimeoutSeconds),
DefaultCallTimeout = TimeSpan.FromSeconds(gw.DefaultCallTimeoutSeconds),
StreamTimeout = gw.StreamTimeoutSeconds > 0
? TimeSpan.FromSeconds(gw.StreamTimeoutSeconds)
: null,
};
}