fix(probe): bound OpcUaClient/AbCip handshakes by timeout CTS; IPv4 preflight; stop sw (code-review)

This commit is contained in:
Joseph Doherty
2026-06-16 06:56:16 -04:00
parent 2d688c2a6d
commit 9bfbbb0fd8
2 changed files with 20 additions and 9 deletions
@@ -59,12 +59,17 @@ public sealed class AbCipDriverProbe : IDriverProbe
var host = parsed.Gateway; var host = parsed.Gateway;
var port = parsed.Port; var port = parsed.Port;
// Bound both phases by the caller timeout, independent of `ct`, so a device that accepts
// TCP but stalls the CIP Forward Open cannot hang the probe.
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(timeout);
// Phase 1: bare TCP preflight — fast rejection for unreachable hosts. // Phase 1: bare TCP preflight — fast rejection for unreachable hosts.
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
try try
{ {
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(host, port, ct); await socket.ConnectAsync(host, port, cts.Token);
} }
catch (SocketException ex) catch (SocketException ex)
{ {
@@ -106,7 +111,7 @@ public sealed class AbCipDriverProbe : IDriverProbe
var rt = new LibplctagTagRuntime(p); var rt = new LibplctagTagRuntime(p);
try try
{ {
await rt.InitializeAsync(ct); await rt.InitializeAsync(cts.Token);
sw.Stop(); sw.Stop();
// InitializeAsync completed without throwing — either the tag was found (Ok) or // InitializeAsync completed without throwing — either the tag was found (Ok) or
@@ -131,7 +136,10 @@ public sealed class AbCipDriverProbe : IDriverProbe
{ {
// LibPlcTagException carries the Status; classify as reachable vs transport failure. // LibPlcTagException carries the Status; classify as reachable vs transport failure.
if (IsReachableException(ex)) if (IsReachableException(ex))
{
sw.Stop();
return new(true, "CIP session OK (controller reachable; probe tag not found)", sw.Elapsed); return new(true, "CIP session OK (controller reachable; probe tag not found)", sw.Elapsed);
}
return new(false, $"Reachable at {host}:{port} but CIP handshake failed: {ex.Message}", null); return new(false, $"Reachable at {host}:{port} but CIP handshake failed: {ex.Message}", null);
} }
@@ -43,15 +43,18 @@ public sealed class OpcUaClientDriverProbe : IDriverProbe
if (string.IsNullOrWhiteSpace(host) || port <= 0) if (string.IsNullOrWhiteSpace(host) || port <= 0)
return new(false, "Config has no host/port to probe.", null); return new(false, "Config has no host/port to probe.", null);
// Bound the whole probe (both phases) by the caller timeout, independent of `ct` — a
// stalled OPC UA endpoint that accepts TCP but never answers GetEndpoints must not block
// the probe when the caller passes CancellationToken.None.
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(timeout);
// --- TCP preflight: fast-fail for closed ports / unreachable hosts --- // --- TCP preflight: fast-fail for closed ports / unreachable hosts ---
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
try try
{ {
using var socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp) using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
{ await socket.ConnectAsync(host, port, cts.Token);
DualMode = true,
};
await socket.ConnectAsync(host, port, ct);
} }
catch (SocketException ex) catch (SocketException ex)
{ {
@@ -74,8 +77,8 @@ public sealed class OpcUaClientDriverProbe : IDriverProbe
{ {
var appConfig = BuildMinimalAppConfig(); var appConfig = BuildMinimalAppConfig();
using var client = await DiscoveryClient.CreateAsync( using var client = await DiscoveryClient.CreateAsync(
appConfig, new Uri(endpointUrl), DiagnosticsMasks.None, ct).ConfigureAwait(false); appConfig, new Uri(endpointUrl), DiagnosticsMasks.None, cts.Token).ConfigureAwait(false);
var endpoints = await client.GetEndpointsAsync(null, ct).ConfigureAwait(false); var endpoints = await client.GetEndpointsAsync(null, cts.Token).ConfigureAwait(false);
sw.Stop(); sw.Stop();
if (endpoints is { Count: > 0 }) if (endpoints is { Count: > 0 })