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.Historian.Wonderware.Client.Internal; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client.Ipc; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Wonderware.Client; /// /// TCP-connect probe for the -shaped driver /// config. Opens a socket to the configured Host:Port (optionally performing the TLS /// client handshake when UseTls is set, reusing the same pinned-thumbprint / CA-chain /// validation as ), then sends a /// with the configured shared secret and confirms the sidecar's /// is accepted — a true end-to-end reachability + auth check. /// Surfaces a green tick + latency on success; a clear red message on timeout / connection /// refused / TLS failure / rejected Hello. /// public sealed class WonderwareHistorianDriverProbe : IDriverProbe { private static readonly JsonSerializerOptions _opts = new() { PropertyNameCaseInsensitive = true, UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip, }; /// public string DriverType => "Historian.Wonderware"; /// public async Task ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct) { WonderwareHistorianClientOptions? 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); if (string.IsNullOrWhiteSpace(opts.Host) || opts.Port <= 0) return new(false, "Config has no host/port to probe.", null); var sw = Stopwatch.StartNew(); Stream? stream = null; try { // Reuse the runtime connect factory so the probe exercises the exact TCP + TLS // (pinned-thumbprint or CA-chain) path the client uses in production. stream = await FrameChannel.DefaultTcpConnectFactory(opts, ct).ConfigureAwait(false); using var reader = new FrameReader(stream, leaveOpen: true); using var writer = new FrameWriter(stream, leaveOpen: true); var hello = new Hello { ProtocolMajor = Hello.CurrentMajor, ProtocolMinor = Hello.CurrentMinor, PeerName = opts.PeerName, SharedSecret = opts.SharedSecret, }; await writer.WriteAsync(MessageKind.Hello, hello, ct).ConfigureAwait(false); var ackFrame = await reader.ReadFrameAsync(ct).ConfigureAwait(false) ?? throw new EndOfStreamException("Sidecar closed connection before HelloAck."); if (ackFrame.Kind != MessageKind.HelloAck) return new(false, $"Sidecar replied to Hello with kind {ackFrame.Kind}; expected HelloAck.", null); var ack = FrameReader.Deserialize(ackFrame.Body); if (!ack.Accepted) return new(false, $"Sidecar rejected Hello: {ack.RejectReason ?? ""}.", null); sw.Stop(); return new(true, $"Connected to {opts.Host}:{opts.Port} (tls={opts.UseTls})", 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); } finally { if (stream is not null) await stream.DisposeAsync().ConfigureAwait(false); } } }