diff --git a/archreview/plans/R2-01-s7-fault-hardening-plan.md.tasks.json b/archreview/plans/R2-01-s7-fault-hardening-plan.md.tasks.json index 066c703e..8136a897 100644 --- a/archreview/plans/R2-01-s7-fault-hardening-plan.md.tasks.json +++ b/archreview/plans/R2-01-s7-fault-hardening-plan.md.tasks.json @@ -22,7 +22,7 @@ { "id": 3, "subject": "GREEN: EnsureConnectedAsync + InitializeAsync connect-timeout OCE -> TimeoutException conversion (STAB-14 root fix)", - "status": "pending", + "status": "completed", "blockedBy": [1, 2] }, { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs index 282fecd1..31afceed 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs @@ -187,6 +187,16 @@ public sealed class S7Driver cts.CancelAfter(_options.Timeout); await plc.OpenAsync(cts.Token).ConfigureAwait(false); } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + // The linked CancelAfter fired — a connect TIMEOUT, not a caller cancellation. + // Surface it as a TimeoutException so init-time and reconnect-time timeouts report + // the same exception type (STAB-14 consistency); the outer catch still faults health. + try { plc.Dispose(); } catch { } + throw new TimeoutException( + $"S7 connect to {_options.Host}:{_options.Port} timed out after " + + $"{(int)_options.Timeout.TotalMilliseconds} ms."); + } catch { // Dispose the partially-constructed connection so a failed init doesn't leak it @@ -1205,6 +1215,17 @@ public sealed class S7Driver cts.CancelAfter(_options.Timeout); await plc.OpenAsync(cts.Token).ConfigureAwait(false); } + catch (OperationCanceledException) when (!ct.IsCancellationRequested) + { + // The linked CancelAfter fired — a connect TIMEOUT, not a caller cancellation. + // Surface it as a connection FAILURE (TimeoutException) so callers route it through + // their degrade paths: an escaping OCE reads as teardown and permanently killed the + // subscription poll loops (STAB-14, the unreachable-host regression in the Crit-3 fix). + try { plc.Dispose(); } catch { /* best-effort */ } + throw new TimeoutException( + $"S7 connect to {_options.Host}:{_options.Port} timed out after " + + $"{(int)_options.Timeout.TotalMilliseconds} ms."); + } catch { try { plc.Dispose(); } catch { /* best-effort */ }