Files
lmxopcua/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverProbe.cs
T
Joseph Doherty c19d124e89 feat(drivers): TCP-connect IDriverProbe for all 9 driver types
Cheap-and-fast probe: open TCP socket to the configured endpoint,
close immediately. Surfaces SocketError on failure, latency on
success, "timed out" on caller cancel. Sufficient for the AdminUI
Test Connect "can we reach the host?" question. Richer protocol-
level probes (OPC UA session open, FOCAS handshake, gRPC ping)
are a documented follow-up. Each probe registered as
AddSingleton<IDriverProbe, X> in DriverFactoryBootstrap so they
flow through DI into AdminOperationsActor.

Historian.Wonderware returns a clean "TCP probe not applicable"
result because it communicates over a Windows named pipe, not TCP.
Also adds OpcUaClient + Historian.Wonderware.Client project
references to Host.csproj (both were missing from the driver
ItemGroup).
2026-05-28 10:53:42 -04:00

86 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 />
public string DriverType => GalaxyDriverFactoryExtensions.DriverTypeName; // "GalaxyMxGateway"
/// <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);
}
}