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; /// /// Cheap TCP-connect probe for the -shaped driver config. /// Parses the Gateway.Endpoint gRPC endpoint (e.g. http://host:5001 or /// host:5001), 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. /// public sealed class GalaxyDriverProbe : IDriverProbe { private static readonly JsonSerializerOptions _opts = new() { PropertyNameCaseInsensitive = true, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, }; /// public string DriverType => GalaxyDriverFactoryExtensions.DriverTypeName; // "GalaxyMxGateway" /// public async Task ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct) { GalaxyDriverOptions? opts; try { opts = JsonSerializer.Deserialize(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); } }