Files
lmxopcua/code-reviews/Driver.Modbus.Cli/findings.md
Joseph Doherty 63e4a6baab fix(driver-modbus-cli): resolve Medium code-review finding (Driver.Modbus.Cli-001)
Add --bit-index, --string-length, and --string-byte-order options to
SubscribeCommand, mirroring ReadCommand, and pass them into ModbusTagDefinition
so that BitInRegister and String type subscriptions use the correct bit index and
string length rather than silently defaulting to bit-0 / zero-length.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:55:10 -04:00

235 lines
11 KiB
Markdown

# Code Review — Driver.Modbus.Cli
| Field | Value |
|---|---|
| Module | `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli` |
| Reviewer | Claude Code |
| Review date | 2026-05-22 |
| Commit reviewed | `76d35d1` |
| Status | Reviewed |
| Open findings | 6 |
## Checklist coverage
A comprehensive review completes every category, recording "No issues found" where
a category produced nothing rather than leaving it blank.
| # | Category | Result |
|---|---|---|
| 1 | Correctness & logic bugs | Driver.Modbus.Cli-001, Driver.Modbus.Cli-002, Driver.Modbus.Cli-003 |
| 2 | OtOpcUa conventions | No issues found |
| 3 | Concurrency & thread safety | Driver.Modbus.Cli-004 |
| 4 | Error handling & resilience | Driver.Modbus.Cli-005, Driver.Modbus.Cli-006 |
| 5 | Security | No issues found |
| 6 | Performance & resource management | No issues found |
| 7 | Design-document adherence | Driver.Modbus.Cli-007 |
| 8 | Code organization & conventions | No issues found |
| 9 | Testing coverage | Driver.Modbus.Cli-008 |
| 10 | Documentation & comments | No issues found |
## Findings
### Driver.Modbus.Cli-001
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Correctness & logic bugs |
| Location | `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/SubscribeCommand.cs:43-51` |
| Status | Resolved |
**Description:** `SubscribeCommand` synthesises its `ModbusTagDefinition` with only
`Name`, `Region`, `Address`, `DataType`, `Writable`, and `ByteOrder` — it never
exposes or passes `--bit-index`, `--string-length`, or `--string-byte-order`.
A user running `subscribe -t BitInRegister` always watches bit 0 regardless of
intent, and `subscribe -t String` runs with `StringLength = 0`. The doc
(`docs/Driver.Modbus.Cli.md`) lists `BitInRegister`, `String`, `Bcd16`, `Bcd32`
in the `subscribe` `--type` help text, so these types are advertised as supported
but cannot be used correctly. `read` and `write` both expose all three flags;
`subscribe` is the odd one out.
**Recommendation:** Add `--bit-index`, `--string-length`, and `--string-byte-order`
options to `SubscribeCommand` (mirroring `ReadCommand`) and pass them into the
`ModbusTagDefinition`, or trim the `--type` help text to the types `subscribe`
actually supports and reject `BitInRegister` / `String` at command entry with a
clear message.
**Resolution:** Resolved 2026-05-22 — added `--bit-index`, `--string-length`, and `--string-byte-order` options to `SubscribeCommand`, mirroring `ReadCommand`, and passed them through to `ModbusTagDefinition` so `BitInRegister` and `String` types subscribe correctly.
### Driver.Modbus.Cli-002
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Correctness & logic bugs |
| Location | `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/WriteCommand.cs:54-89` |
| Status | Resolved |
**Description:** `WriteCommand` rejects read-only regions (`DiscreteInputs` /
`InputRegisters`) but does not validate that `--type` is meaningful for the
`Coils` region. `write -r Coils -a 5 -t UInt16 -v 42` builds a `Coils` tag with
`DataType = UInt16`; the value parses to a boxed `ushort`, and the driver's
`WriteOneAsync` coil branch calls `Convert.ToBoolean(value)` which succeeds for
any non-zero `ushort` (yields `true`). The write silently lands as a coil ON with
no diagnostic, even though the operator asked for a 16-bit register write. A coil
region only supports `Bool`-style boolean values.
**Recommendation:** After the read-only-region check, reject `Region == Coils`
combined with any non-boolean `--type` (anything other than `Bool`), with a
message explaining coils carry a single bit.
**Resolution:** Resolved 2026-05-22 — added a `Region == Coils && DataType != Bool` check immediately after the read-only-region guard, throwing `CommandException` with a message explaining that coils carry a single bit and only `--type Bool` is valid.
### Driver.Modbus.Cli-003
| Field | Value |
|---|---|
| Severity | Low |
| Category | Correctness & logic bugs |
| Location | `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ModbusCommandBase.cs:14-24` |
| Status | Open |
**Description:** `Port` (`int`) and `TimeoutMs` (`int`) accept any 32-bit value,
including negatives and ports above 65535. `UnitId` is a `byte`, so it accepts
0-255 even though the option description and `docs/Driver.Modbus.Cli.md` both say
the valid range is 1-247 (0 is the Modbus broadcast address; 248-255 are
reserved). A negative `--timeout-ms` becomes a negative `TimeSpan` passed straight
into the driver; an out-of-range `--port` fails later with an opaque socket
error. None of these are validated at parse time.
**Recommendation:** Validate `Port` (1-65535), `TimeoutMs` (greater than 0), and
`UnitId` (1-247) at the top of each command's `ExecuteAsync` (or in
`ModbusCommandBase`), throwing `CliFx.Exceptions.CommandException` with a clear
message — consistent with how `WriteCommand` already rejects bad regions and
boolean strings.
**Resolution:** _(open)_
### Driver.Modbus.Cli-004
| Field | Value |
|---|---|
| Severity | Low |
| Category | Concurrency & thread safety |
| Location | `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/SubscribeCommand.cs:61-67` |
| Status | Open |
**Description:** The `OnDataChange` handler is invoked from the driver's
`PollGroupEngine` background thread and calls `console.Output.WriteLine`
synchronously. An exception thrown inside this handler (e.g. an `IOException` on a
redirected or closed stdout) propagates on the poll-engine thread and is not
caught — it could fault the background loop. For a long-running `subscribe` this
is a real, if low-probability, crash path. Output lines are also written without
any synchronization, so overlapping poll ticks could interleave partial lines.
**Recommendation:** Wrap the handler body in a `try/catch` that swallows or logs
write failures so a transient console-write error cannot tear down the poll loop.
A single `lock` around the write also removes the interleave risk.
**Resolution:** _(open)_
### Driver.Modbus.Cli-005
| Field | Value |
|---|---|
| Severity | Low |
| Category | Error handling & resilience |
| Location | `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ProbeCommand.cs:21-54`; `Commands/ReadCommand.cs:46-75`; `Commands/WriteCommand.cs:54-89` |
| Status | Open |
**Description:** All three commands call `ConfigureLogging()` then
`console.RegisterCancellationHandler()`, but if the operator presses Ctrl+C
before `InitializeAsync` completes, the resulting `OperationCancelledException`
propagates out of `ExecuteAsync` unhandled. CliFx renders unhandled non-
`CommandException` exceptions as a full stack trace, which is noisy for what is
just a user-cancelled run. `SubscribeCommand` correctly catches
`OperationCancelledException` around its `Task.Delay`, but the connect/read/write
commands do not catch it around their driver calls.
**Recommendation:** Either let cancellation surface a clean message (catch
`OperationCancelledException` in each command and exit quietly) or document that
the noisy trace on Ctrl+C-during-connect is acceptable. Consistency with
`SubscribeCommand`'s handling is the cleaner choice.
**Resolution:** _(open)_
### Driver.Modbus.Cli-006
| Field | Value |
|---|---|
| Severity | Low |
| Category | Error handling & resilience |
| Location | `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ProbeCommand.cs:35-53` |
| Status | Open |
**Description:** `probe` reports `Health: {health.State}` from `GetHealth()`.
After a successful `InitializeAsync` the driver sets state to `Healthy`
regardless of whether the subsequent probe register read returns Good or a Bad
status code. `ReadAsync` does not throw on a Modbus exception response — it
returns a `DataValueSnapshot` with a Bad `StatusCode`. So `probe` against a host
that accepts the TCP connection but rejects FC03 at the probe address prints
`Health: Healthy` while the snapshot line below shows a Bad status. The two lines
disagree, and the headline `Health` value (the thing an operator scans first)
overstates success. The doc bills `probe` as the "is the PLC up + talking Modbus"
check, which the bare `Healthy` does not actually confirm.
**Recommendation:** Have `probe` derive its headline verdict from the probe
snapshot's `StatusCode` (Good vs Bad) rather than — or in addition to — the driver
`State`, or print a single combined verdict line so the two cannot contradict each
other.
**Resolution:** _(open)_
### Driver.Modbus.Cli-007
| Field | Value |
|---|---|
| Severity | Low |
| Category | Design-document adherence |
| Location | `docs/Driver.Modbus.Cli.md:124-156`; `src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ReadCommand.cs` |
| Status | Open |
**Description:** `docs/Driver.Modbus.Cli.md` devotes a whole "v2 addressing
grammar" section to the industry-standard tag-address strings (`40001:F:CDAB`,
`HR1:I`, `C100`, `V2000:F:CDAB`, etc.) and says "set the per-tag `addressString`
field instead of the structured `region` + `address` + `dataType` fields." None of
the CLI commands expose an `--address-string` (or equivalent) flag — `read`,
`write`, and `subscribe` only accept the structured `--region` + `--address` +
`--type` triple. The documented address-string grammar is reachable only through a
hand-written `DriverConfig` JSON, not through this CLI. The doc reads as if the CLI
supports it.
**Recommendation:** Either add an `--address-string` option that feeds the
driver's address-string parser (and `--family` for the DL205/MELSEC native
syntax), or scope the "v2 addressing grammar" section of the doc to note it
applies to `DriverConfig` JSON and is not a CLI flag.
**Resolution:** _(open)_
### Driver.Modbus.Cli-008
| Field | Value |
|---|---|
| Severity | Low |
| Category | Testing coverage |
| Location | `tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli.Tests/` |
| Status | Open |
**Description:** The test project covers only the two pure-function seams:
`ReadCommand.SynthesiseTagName` and `WriteCommand.ParseValue`. There is no coverage
for `WriteCommand`'s read-only-region rejection (`Region is not (Coils or
HoldingRegisters)`), no test for `ModbusCommandBase.BuildOptions` (e.g. that
`Probe.Enabled` is `false` and `AutoReconnect` tracks `--disable-reconnect`), and
no test asserting unsupported write types throw. The branch logic in
`WriteCommand.ExecuteAsync` and `ModbusCommandBase.BuildOptions` is the part most
likely to regress and is currently untested. The validation gaps in findings
002/003 are also untested precisely because no test exercises that path.
**Recommendation:** Add tests for `WriteCommand`'s region-validation branch and for
`ModbusCommandBase.BuildOptions` (construct a command instance via the `init`
setters and assert the produced `ModbusDriverOptions`). Once findings 002/003 are
fixed, add tests for the new validation paths.
**Resolution:** _(open)_