fix(driver-twincat): resolve High code-review findings (Driver.TwinCAT-001, -002, -007, -008, -013)

Driver.TwinCAT-001 — InitializeAsync/ReinitializeAsync ignored driverConfigJson.
Extracted the DTO-to-options parse into a shared TwinCATDriverFactoryExtensions.ParseOptions;
InitializeAsync now re-parses driverConfigJson into a mutable _options field, so a config
generation pushed via ReinitializeAsync (added/removed devices, tags, probe settings) is
actually applied at runtime.

Driver.TwinCAT-002 — LInt/ULInt narrowed to Int32. ToDriverDataType now maps LInt to Int64,
ULInt to UInt64, UDInt to UInt32, UInt/USInt to UInt16, Int/SInt to Int16, and the IEC
TIME/DATE/DT/TOD types to UInt32 (their raw UDINT counter). Removed the stale "Int64 gap"
comment — no truncation or sign flips at the OPC UA encode layer.

Driver.TwinCAT-007 — EnsureConnectedAsync was not thread-safe. Connect/reconnect is now
serialized per device by a SemaphoreSlim (DeviceState.ConnectGate) with a double-checked
connect, mirroring the S7 driver. Concurrent read/write/probe callers can no longer leak a
client or race a create-vs-dispose.

Driver.TwinCAT-008 — native ADS notification callbacks ran driver logic on the AMS router
thread. AdsTwinCATClient now enqueues AdsNotificationEx callbacks onto a bounded Channel
drained by a dedicated managed task; the router-thread callback only does a non-blocking
TryWrite, so a slow consumer cannot stall ADS notification delivery process-wide.

Driver.TwinCAT-013 — TwinCATDriver did not implement IRediscoverable. The driver now
implements IRediscoverable; AdsTwinCATClient detects ADS 0x0702 (symbol-version-changed) on
read/write paths and raises OnSymbolVersionChanged, which the driver forwards as
OnRediscoveryNeeded so Core rebuilds the address space after a PLC program re-download.

Adds TwinCATHighFindingsRegressionTests covering all five fixes; updates the data-type
mapping assertion in TwinCATDriverTests. TwinCAT driver builds clean; 119 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 06:37:05 -04:00
parent 66e8bfbab3
commit 5197b6c237
10 changed files with 400 additions and 37 deletions

View File

@@ -7,7 +7,7 @@
| Review date | 2026-05-22 |
| Commit reviewed | `76d35d1` |
| Status | Reviewed |
| Open findings | 16 |
| Open findings | 11 |
## Checklist coverage
@@ -36,7 +36,7 @@ a category produced nothing rather than leaving it blank.
| Severity | High |
| Category | Correctness & logic bugs |
| Location | `TwinCATDriver.cs:41-78` |
| Status | Open |
| Status | Resolved |
**Description:** `InitializeAsync` and `ReinitializeAsync` both ignore their `driverConfigJson`
parameter entirely. `InitializeAsync` builds device/tag state exclusively from `_options`,
@@ -55,7 +55,7 @@ parser) and assign the resulting options to a mutable field, rather than relying
constructor-captured `_options`. Alternatively, document explicitly that the constructor is
the sole config source and have the Core recreate the driver instance on config change.
**Resolution:** _(open)_
**Resolution:** Resolved 2026-05-22 — extracted the DTO→options parse into a shared TwinCATDriverFactoryExtensions.ParseOptions; InitializeAsync re-parses driverConfigJson into a now-mutable _options field, so ReinitializeAsync applies a changed config generation.
### Driver.TwinCAT-002
@@ -64,7 +64,7 @@ the sole config source and have the Core recreate the driver instance on config
| Severity | High |
| Category | Correctness & logic bugs |
| Location | `TwinCATDataType.cs:34-48`, `AdsTwinCATClient.cs:264-281` |
| Status | Open |
| Status | Resolved |
**Description:** `TwinCATDataTypeExtensions.ToDriverDataType` maps `LInt` and `ULInt` (signed/
unsigned 64-bit) to `DriverDataType.Int32` (comment: "matches Int64 gap"). The address-space
@@ -79,7 +79,7 @@ the range 0x80000000 to 0xFFFFFFFF surface as negative.
**Recommendation:** Map `LInt` to `Int64`, `ULInt` to `UInt64`, `UDInt` to `UInt32`, `UInt`
to `UInt16`, and `USInt`/`SInt` to their natural widths. Remove the stale "Int64 gap" comment.
**Resolution:** _(open)_
**Resolution:** Resolved 2026-05-22 — ToDriverDataType now maps LInt→Int64, ULInt→UInt64, UDInt→UInt32, UInt/USInt→UInt16, Int/SInt→Int16, and the IEC time types→UInt32; removed the stale Int64-gap comment.
### Driver.TwinCAT-003
@@ -178,7 +178,7 @@ fallback. Prefer the first device HostAddress only when one exists (already done
| Severity | High |
| Category | Concurrency & thread safety |
| Location | `TwinCATDriver.cs:413-429` |
| Status | Open |
| Status | Resolved |
**Description:** `EnsureConnectedAsync` is not thread-safe. `ReadAsync`, `WriteAsync`,
`SubscribeAsync`, and the per-device `ProbeLoopAsync` background task can all call it
@@ -196,7 +196,7 @@ continuously, so this race is not hypothetical under any concurrent read/write l
serialize device access with a `SemaphoreSlim` — follow that pattern. Note this also
serializes the wire, which `docs/v2/driver-specs.md` recommends for single-connection-per-PLC.
**Resolution:** _(open)_
**Resolution:** Resolved 2026-05-22 — EnsureConnectedAsync is now serialized per device by a SemaphoreSlim (DeviceState.ConnectGate) with a double-checked connect, mirroring the S7 driver; no client is leaked and no disposal race remains.
### Driver.TwinCAT-008
@@ -205,7 +205,7 @@ serializes the wire, which `docs/v2/driver-specs.md` recommends for single-conne
| Severity | High |
| Category | Concurrency & thread safety |
| Location | `AdsTwinCATClient.cs:162-169`, `TwinCATDriver.cs:319-324` |
| Status | Open |
| Status | Resolved |
**Description:** Native ADS notification callbacks (`OnAdsNotificationEx`) run on the
`AdsClient` AMS router thread. `docs/v2/driver-specs.md` section 6 explicitly calls this out
@@ -221,7 +221,7 @@ device in the process.
a dedicated managed task before invoking `OnChange`/`OnDataChange`, exactly as the Galaxy
`EventPump` does. Keep the router-thread callback to a non-blocking enqueue only.
**Resolution:** _(open)_
**Resolution:** Resolved 2026-05-22 — AdsTwinCATClient now enqueues native AdsNotificationEx callbacks onto a bounded Channel drained by a dedicated managed task; the AMS router thread only does a non-blocking TryWrite, so a slow consumer cannot stall ADS delivery.
### Driver.TwinCAT-009
@@ -334,7 +334,7 @@ stream-and-discard design is intentional, report the real footprint of `_nativeS
| Severity | High |
| Category | Design-document adherence |
| Location | `TwinCATDriver.cs:11-12` (capability list), whole file |
| Status | Open |
| Status | Resolved |
**Description:** `TwinCATDriver` does not implement `IRediscoverable`. Both
`docs/v2/driver-specs.md` section 6 and `docs/v2/driver-stability.md` section "TwinCAT — Deep
@@ -352,7 +352,7 @@ on read/write/notification paths, raise `OnRediscoveryNeeded` with a scoped reas
re-establish native notifications after the Core re-runs `DiscoverAsync`. This is explicitly
part of the documented driver contract, not optional.
**Resolution:** _(open)_
**Resolution:** Resolved 2026-05-22 — TwinCATDriver implements IRediscoverable; AdsTwinCATClient detects ADS 0x0702 on read/write paths and raises OnSymbolVersionChanged, which the driver forwards as OnRediscoveryNeeded so Core rebuilds the address space.
### Driver.TwinCAT-014