fix(driver-s7): resolve High code-review findings (Driver.S7-001, -006, -007, -011)
Driver.S7-001: Timer (T{n}) / Counter (C{n}) addresses parsed cleanly but
the read path had no S7DataType or decode case for them, so a Timer/Counter
tag passed fail-fast init and then threw a misleading type-mismatch on every
read. InitializeAsync now runs RejectUnsupportedTagAddresses, throwing a clear
NotSupportedException ("not yet supported", echoing tag name + address) so the
config error fails fast at init.
Driver.S7-006: ShutdownAsync cancelled the probe/poll CTSs but did not await
the fire-and-forget loop tasks before DisposeAsync disposed _gate, letting a
loop iteration mid-semaphore race a disposed object. The probe task is now
tracked in _probeTask and each poll task in SubscriptionState.PollTask;
ShutdownAsync cancels every CTS, awaits Task.WhenAll of those handles with a
bounded 5 s DrainTimeout, then disposes the CTSs and gate. Task.Run is passed
CancellationToken.None so the handle is always awaitable.
Driver.S7-007: a PUT/GET-disabled fault (permanent misconfiguration) was
mapped identically to a transient PlcException — both BadDeviceFailure +
Degraded. ReadAsync/WriteAsync now split the catch via an IsAccessDenied
filter (S7.Net exposes no typed code for AccessingObjectNotAllowed, so the
inner-exception chain is inspected for the "not allowed" marker). Access-denied
now maps to BadNotSupported and Faulted with a config-alert message pointing
at the TIA Portal PUT/GET toggle; genuine device faults stay BadDeviceFailure.
Driver.S7-011: S7Driver ignored driverConfigJson on Initialize/Reinitialize,
so a config change delivered through ReinitializeAsync (the only Core-initiated
in-process recovery path) was silently discarded. Config parsing was factored
into S7DriverFactoryExtensions.ParseOptions; InitializeAsync now re-parses
driverConfigJson and rebuilds _options whenever the document has a real body.
An empty / placeholder document keeps the constructor options.
Adds S7DriverCodeReviewFixTests covering Timer/Counter rejection, config-json
application on Initialize/Reinitialize, and shutdown-drain with active
subscriptions. All 68 S7 driver tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
| Review date | 2026-05-22 |
|
||||
| Commit reviewed | `76d35d1` |
|
||||
| Status | Reviewed |
|
||||
| Open findings | 14 |
|
||||
| Open findings | 10 |
|
||||
|
||||
## Checklist coverage
|
||||
|
||||
@@ -36,7 +36,7 @@ a category produced nothing rather than leaving it blank.
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Location | `S7AddressParser.cs:93`, `S7Driver.cs:231` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** S7AddressParser.Parse accepts Timer (T0) and Counter (C0)
|
||||
addresses and the test suite asserts they parse successfully, but the read path
|
||||
@@ -55,7 +55,11 @@ until they are wired through to S7.Net, or implement the Timer/Counter read path
|
||||
If kept, reject Timer/Counter tags at InitializeAsync with a clear "not yet
|
||||
supported" error rather than letting them parse clean.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — `InitializeAsync` now runs
|
||||
`RejectUnsupportedTagAddresses`, which throws `NotSupportedException` with a
|
||||
clear "not yet supported" message (echoing the tag name + address) for any tag
|
||||
whose address parses as a Timer or Counter, so the bad config fails fast at init
|
||||
rather than throwing a misleading type-mismatch on every read.
|
||||
|
||||
### Driver.S7-002
|
||||
|
||||
@@ -150,7 +154,7 @@ redundant global::S7.Net. qualifiers where using S7.Net already covers them.
|
||||
| Severity | High |
|
||||
| Category | Concurrency & thread safety |
|
||||
| Location | `S7Driver.cs:140`, `S7Driver.cs:457`, `S7Driver.cs:506` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** Disposal races with the in-flight probe / poll tasks.
|
||||
ShutdownAsync calls _probeCts.Cancel() and cancels each subscription CTS, but it
|
||||
@@ -168,7 +172,13 @@ running while ProbeLoopAsync may still touch the linked token.
|
||||
(or DisposeAsync) await Task.WhenAll(...) with a bounded timeout after cancelling,
|
||||
before disposing _gate and the CTS objects.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — the probe loop now stores its Task in
|
||||
`_probeTask` and each subscription records its poll Task in `SubscriptionState.PollTask`.
|
||||
`ShutdownAsync` cancels every CTS, awaits `Task.WhenAll` of those handles with a
|
||||
bounded 5 s `DrainTimeout`, and only then disposes `_probeCts`, the subscription
|
||||
CTSs, and (via `DisposeAsync`) `_gate` — so no loop can touch a disposed
|
||||
semaphore. `Task.Run` is now passed `CancellationToken.None` so the handle is
|
||||
always awaitable even if the token is already cancelled.
|
||||
|
||||
### Driver.S7-007
|
||||
|
||||
@@ -177,7 +187,7 @@ before disposing _gate and the CTS objects.
|
||||
| Severity | High |
|
||||
| Category | Error handling & resilience |
|
||||
| Location | `S7Driver.cs:200`, `S7DriverOptions.cs:13`, `docs/v2/driver-specs.md:434` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** PUT/GET-disabled handling contradicts the design and the
|
||||
module own docstring. driver-specs.md section 5 (line 434) and the
|
||||
@@ -197,7 +207,15 @@ PUT/GET-disabled / access-denied code to BadNotSupported with a distinct
|
||||
config-alert health state; keep BadDeviceFailure/Degraded only for genuine
|
||||
device-fault error codes.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — `ReadAsync` / `WriteAsync` now split the
|
||||
`PlcException` catch via an `IsAccessDenied` filter. S7.Net exposes no typed
|
||||
error code for the S7 `AccessingObjectNotAllowed` status (its
|
||||
`ValidateResponseCode` throws a plain `Exception` wrapped as the inner exception
|
||||
of a `PlcException`), so `IsAccessDenied` walks the inner-exception chain for the
|
||||
"not allowed" marker. A PUT/GET-disabled fault now maps to `BadNotSupported` and
|
||||
sets health to `Faulted` with a config-alert message pointing operators at the
|
||||
TIA Portal PUT/GET toggle; a genuine device fault still maps to
|
||||
`BadDeviceFailure`/`Degraded`.
|
||||
|
||||
### Driver.S7-008
|
||||
|
||||
@@ -279,7 +297,7 @@ round-tripping through the async path.
|
||||
| Severity | High |
|
||||
| Category | Design-document adherence |
|
||||
| Location | `S7Driver.cs:82`, `S7Driver.cs:134`, `IDriver.cs:24` |
|
||||
| Status | Open |
|
||||
| Status | Resolved |
|
||||
|
||||
**Description:** S7Driver ignores the driverConfigJson parameter on both
|
||||
InitializeAsync and ReinitializeAsync. The IDriver contract states InitializeAsync
|
||||
@@ -298,7 +316,14 @@ explicitly that S7 reconfiguration requires instance recreation and have
|
||||
ReinitializeAsync signal that the passed JSON is unused so the contract mismatch
|
||||
is visible.
|
||||
|
||||
**Resolution:** _(open)_
|
||||
**Resolution:** Resolved 2026-05-22 — config parsing was factored out of the
|
||||
factory into `S7DriverFactoryExtensions.ParseOptions`. `InitializeAsync` (and
|
||||
therefore `ReinitializeAsync`, which delegates to it) now re-parses
|
||||
`driverConfigJson` and rebuilds `_options` from it whenever the document carries
|
||||
a real body, so a config change delivered through `ReinitializeAsync` — the only
|
||||
Core-initiated in-process recovery path — is honoured. An empty / placeholder
|
||||
document (`""`, `{}`, `[]`) keeps the constructor-supplied options so existing
|
||||
lifecycle unit tests that pass `"{}"` are unaffected.
|
||||
|
||||
### Driver.S7-012
|
||||
|
||||
|
||||
Reference in New Issue
Block a user