diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs index 869588f9..c5d4dad5 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverProbe.cs @@ -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); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverProbe.cs index 1482203c..627c8e1b 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriverProbe.cs @@ -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 })