32d7fd7cc9
v2-ci / build (push) Failing after 48s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
The driver/factory/seed use 'GalaxyMxGateway' (legacy 'Galaxy' was retired),
but the AdminUI editor router, GalaxyDriverPage, address picker, identity
dropdown, the Galaxy browser/probe, and DraftValidator still keyed on 'Galaxy'.
Result: the seeded GalaxyMxGateway driver couldn't be edited ('no editor
registered'), UI-created Galaxy drivers wrote a type with no factory, and a
SystemPlatform-bound GalaxyMxGateway driver failed publish validation.
Align all stragglers to GalaxyMxGateway (+ failing-test-first DraftValidator
coverage). ShouldStub's 'Galaxy' legacy safety-net left intact.
87 lines
3.4 KiB
C#
87 lines
3.4 KiB
C#
using System.Diagnostics;
|
|
using System.Net.Sockets;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
|
|
|
|
/// <summary>
|
|
/// Cheap TCP-connect probe for the <see cref="GalaxyDriverOptions"/>-shaped driver config.
|
|
/// Parses the <c>Gateway.Endpoint</c> gRPC endpoint (e.g. <c>http://host:5001</c> or
|
|
/// <c>host:5001</c>), opens a socket and closes immediately. Surfaces a green tick +
|
|
/// latency on success; red chip + SocketError on failure; "timed out" on the caller's
|
|
/// cancellation. Does NOT exchange any gRPC frames — a richer gRPC ping probe is a
|
|
/// documented follow-up.
|
|
/// </summary>
|
|
public sealed class GalaxyDriverProbe : IDriverProbe
|
|
{
|
|
private static readonly JsonSerializerOptions _opts = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true,
|
|
UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
|
|
};
|
|
|
|
/// <inheritdoc />
|
|
// Matches DriverInstance.DriverType strings set by the AdminUI's GalaxyDriverPage.
|
|
public string DriverType => GalaxyDriverFactoryExtensions.DriverTypeName;
|
|
|
|
/// <inheritdoc />
|
|
public async Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct)
|
|
{
|
|
GalaxyDriverOptions? opts;
|
|
try { opts = JsonSerializer.Deserialize<GalaxyDriverOptions>(configJson, _opts); }
|
|
catch (Exception ex) { return new(false, $"Config JSON is invalid: {ex.Message}", null); }
|
|
if (opts is null) return new(false, "Config JSON deserialized to null.", null);
|
|
|
|
var (host, port) = ExtractTarget(opts);
|
|
if (string.IsNullOrWhiteSpace(host) || port <= 0)
|
|
return new(false, "Config has no host/port to probe.", null);
|
|
|
|
var sw = Stopwatch.StartNew();
|
|
try
|
|
{
|
|
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
|
await socket.ConnectAsync(host, port, ct);
|
|
sw.Stop();
|
|
return new(true, null, sw.Elapsed);
|
|
}
|
|
catch (SocketException ex)
|
|
{
|
|
return new(false, $"Connect failed: {ex.SocketErrorCode}", null);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new(false, ex.Message, null);
|
|
}
|
|
}
|
|
|
|
private static (string host, int port) ExtractTarget(GalaxyDriverOptions opts)
|
|
{
|
|
var endpoint = opts.Gateway.Endpoint;
|
|
if (string.IsNullOrWhiteSpace(endpoint)) return (string.Empty, 0);
|
|
|
|
// Try absolute URI first (e.g. "http://hostname:5001" or "https://hostname:5001").
|
|
if (Uri.TryCreate(endpoint, UriKind.Absolute, out var uri))
|
|
{
|
|
var host = uri.Host;
|
|
// Uri.Port is -1 when not specified; default mxaccessgw port is 5001.
|
|
var port = uri.Port > 0 ? uri.Port : 5001;
|
|
return (host, port);
|
|
}
|
|
|
|
// Fallback: treat as "host:port" (no scheme).
|
|
var colonIdx = endpoint.LastIndexOf(':');
|
|
if (colonIdx > 0 && int.TryParse(endpoint[(colonIdx + 1)..], out var rawPort) && rawPort > 0)
|
|
return (endpoint[..colonIdx], rawPort);
|
|
|
|
// No port found — return the whole string as host with default port.
|
|
return (endpoint, 5001);
|
|
}
|
|
}
|