From 5df3c73204be7a1c79725156cf8377091985b14f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 16 Jun 2026 07:03:55 -0400 Subject: [PATCH] fix(probe): bound AbLegacy handshake by timeout CTS + stop sw; correct FOCAS timeout comment (code-review) --- .../AbLegacyDriverProbe.cs | 12 ++++++++++-- .../FocasDriverProbe.cs | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs index f2f0715e..ffe9dff6 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverProbe.cs @@ -59,12 +59,17 @@ public sealed class AbLegacyDriverProbe : 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 PCCC session 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) { @@ -104,7 +109,7 @@ public sealed class AbLegacyDriverProbe : IDriverProbe var rt = new LibplctagLegacyTagRuntime(p); try { - await rt.InitializeAsync(ct); + await rt.InitializeAsync(cts.Token); sw.Stop(); // InitializeAsync completed without throwing — either the tag was found (Ok) or @@ -129,7 +134,10 @@ public sealed class AbLegacyDriverProbe : IDriverProbe { // LibPlcTagException carries the Status; classify as reachable vs transport failure. if (IsReachableException(ex)) + { + sw.Stop(); return new(true, "PCCC session OK (controller reachable; probe tag not found)", sw.Elapsed); + } return new(false, $"Reachable at {host}:{port} but PCCC handshake failed: {ex.Message}", null); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriverProbe.cs index 865b6ab5..807046d0 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriverProbe.cs @@ -114,7 +114,9 @@ public sealed class FocasDriverProbe : IDriverProbe } catch (OperationCanceledException) { - // ct cancelled or the native handshake exceeded the timeout budget. + // The caller cancelled, or the Task.Run was cancelled before the native call started. + // (A native cnc_allclibhndl3 that is already running is bounded by the timeoutSeconds + // argument passed into it, not by handshakeCts — see TryAllocateAndFreeHandle.) return new(false, $"Probe timed out after {timeout.TotalSeconds:F0}s.", null); } }