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 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.
var sw = Stopwatch.StartNew();
try
{
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)
{
@@ -106,7 +111,7 @@ public sealed class AbCipDriverProbe : IDriverProbe
var rt = new LibplctagTagRuntime(p);
try
{
await rt.InitializeAsync(ct);
await rt.InitializeAsync(cts.Token);
sw.Stop();
// 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.
if (IsReachableException(ex))
{
sw.Stop();
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);
}
@@ -43,15 +43,18 @@ public sealed class OpcUaClientDriverProbe : IDriverProbe
if (string.IsNullOrWhiteSpace(host) || port <= 0)
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 ---
var sw = Stopwatch.StartNew();
try
{
using var socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp)
{
DualMode = true,
};
await socket.ConnectAsync(host, port, ct);
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(host, port, cts.Token);
}
catch (SocketException ex)
{
@@ -74,8 +77,8 @@ public sealed class OpcUaClientDriverProbe : IDriverProbe
{
var appConfig = BuildMinimalAppConfig();
using var client = await DiscoveryClient.CreateAsync(
appConfig, new Uri(endpointUrl), DiagnosticsMasks.None, ct).ConfigureAwait(false);
var endpoints = await client.GetEndpointsAsync(null, ct).ConfigureAwait(false);
appConfig, new Uri(endpointUrl), DiagnosticsMasks.None, cts.Token).ConfigureAwait(false);
var endpoints = await client.GetEndpointsAsync(null, cts.Token).ConfigureAwait(false);
sw.Stop();
if (endpoints is { Count: > 0 })