- Driver.AbLegacy.Cli-002: WriteCommand.Value description lists the full true/false, 1/0, on/off, yes/no alias set. - Driver.AbLegacy.Cli-003: SubscribeCommand serialises every WriteLine via a per-execution consoleGate lock so the poll-thread OnDataChange handler can't interleave with the banner. - Driver.AbLegacy.Cli-004: dropped 'await using var driver' in favour of a plain 'var driver' + explicit await ShutdownAsync in finally; the driver is no longer shut down twice. - Driver.AbLegacy.Cli-005: SubscribeCommand.IntervalMs description carries the PollGroupEngine 250ms-floor caveat; docs/Driver.AbLegacy.Cli.md spells out the same. - Driver.AbLegacy.Cli-006: ProbeCommand --type now carries the short alias 't' to match the other commands. - Driver.AbLegacy.Cli-007: BuildOptionsTests cover the probe-disabled, device-shape, tag-passthrough, timeout-propagation, and empty-tag-list paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Code Review — Driver.AbLegacy.Cli
| Field | Value |
|---|---|
| Module | src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli |
| Reviewer | Claude Code |
| Review date | 2026-05-22 |
| Commit reviewed | 76d35d1 |
| Status | Reviewed |
| Open findings | 0 |
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.AbLegacy.Cli-001, Driver.AbLegacy.Cli-002 |
| 2 | OtOpcUa conventions | No issues found |
| 3 | Concurrency & thread safety | Driver.AbLegacy.Cli-003 |
| 4 | Error handling & resilience | Driver.AbLegacy.Cli-001, Driver.AbLegacy.Cli-004 |
| 5 | Security | No issues found |
| 6 | Performance & resource management | No issues found |
| 7 | Design-document adherence | Driver.AbLegacy.Cli-005 |
| 8 | Code organization & conventions | Driver.AbLegacy.Cli-006 |
| 9 | Testing coverage | Driver.AbLegacy.Cli-007 |
| 10 | Documentation & comments | Driver.AbLegacy.Cli-002, Driver.AbLegacy.Cli-005 |
Findings
Driver.AbLegacy.Cli-001
| Field | Value |
|---|---|
| Severity | Medium |
| Category | Error handling & resilience |
| Location | Commands/WriteCommand.cs:46, Commands/WriteCommand.cs:62-72 |
| Status | Resolved |
Description: WriteCommand.ExecuteAsync calls ParseValue(Value, DataType) at
line 46, before the try block and outside any catch. ParseValue uses
short.Parse / int.Parse / float.Parse, which throw FormatException on
malformed input (-v abc) and OverflowException on out-of-range input
(-t Int -v 99999). Only the Bit branch and the unsupported-type branch raise
the CliFx CommandException that the framework renders as a clean one-line error
with a non-zero exit code. For every numeric type a bad --value therefore
escapes as an unhandled FormatException/OverflowException, which CliFx prints
as a raw stack trace — an operator-hostile failure mode for a tool whose whole
purpose is ad-hoc operator use. The module own test
ParseValue_non_numeric_for_numeric_types_throws confirms the raw FormatException
leaks. The driver WriteAsync has dedicated catch arms for FormatException
(BadTypeMismatch) and OverflowException (BadOutOfRange), but the CLI never
reaches the driver because the parse happens first.
Recommendation: Wrap the numeric parses so a parse failure surfaces as a
CliFx.Exceptions.CommandException with a message naming the offending value and
type (mirroring the existing Bit and unsupported-type branches). Either catch
FormatException/OverflowException inside ParseValue and rethrow as
CommandException, or use TryParse and throw CommandException on failure.
Resolution: Resolved 2026-05-22 — wrapped numeric parses in ParseValue with try/catch for FormatException/OverflowException, rethrowing as CommandException with a message naming the offending value and type; updated test to assert CommandException and added overflow regression test.
Driver.AbLegacy.Cli-002
| Field | Value |
|---|---|
| Severity | Low |
| Category | Correctness & logic bugs |
| Location | Commands/WriteCommand.cs:27-29, Program.cs:6-9 |
| Status | Resolved |
Description: The --value option help text states "booleans accept
true/false/1/0", but ParseBool (WriteCommand.cs:74-80) and the error message
also accept on/off and yes/no, and DriverClis.md documents the full
true/false/1/0/yes/no/on/off set as the shared CLI contract. The help text
under-documents the accepted aliases, so an operator reading --help will not
discover on/off/yes/no. Minor, but it makes the inline help inconsistent
with both the code and the design doc.
Recommendation: Extend the --value description to list the full alias set,
matching the wording used elsewhere (e.g. "booleans accept
true/false, 1/0, on/off, yes/no").
Resolution: Resolved 2026-05-23 — updated WriteCommand.Value description to
list the full alias set: "booleans accept true/false, 1/0, on/off, yes/no".
Regression test CommandMetadataTests.WriteCommand_value_help_lists_full_boolean_alias_set
asserts the description contains every alias group.
Driver.AbLegacy.Cli-003
| Field | Value |
|---|---|
| Severity | Low |
| Category | Concurrency & thread safety |
| Location | Commands/SubscribeCommand.cs:47-53 |
| Status | Resolved |
Description: The OnDataChange handler calls console.Output.WriteLine(line)
(the synchronous overload) directly from the PollGroupEngine poll thread. The
poll engine raises change events from a background timer/loop thread, so two
ticks that fire close together can interleave writes on the shared TextWriter.
SnapshotFormatter builds the whole line into a single string before the call,
so a line is unlikely to be torn mid-token, but there is no synchronisation
guaranteeing that the background-thread writes do not interleave with the
await console.Output.WriteLineAsync(...) "Subscribed to ..." line on the command
thread, nor with each other. This is the same pattern as the AbCip CLI, so it is
a shared low-severity issue, not unique to this module.
Recommendation: Serialise console writes from the event handler — e.g. funnel
change events through a Channel<string> drained by a single consumer task, or
guard the WriteLine with a lock. At minimum, document that the interleaving is
accepted because output is human-facing and line-buffered.
Resolution: Resolved 2026-05-23 — SubscribeCommand now serialises every
console write through a shared consoleGate lock: the poll-thread
OnDataChange callback and the command-thread "Subscribed to ..." line both take
the lock before calling WriteLine. Comment in the source documents the intent.
Driver.AbLegacy.Cli-004
| Field | Value |
|---|---|
| Severity | Low |
| Category | Error handling & resilience |
| Location | Commands/ProbeCommand.cs:37-56, Commands/ReadCommand.cs:39-50, Commands/WriteCommand.cs:48-59, Commands/SubscribeCommand.cs:41-76 |
| Status | Resolved |
Description: Every command does await using var driver = new AbLegacyDriver(...)
and an explicit await driver.ShutdownAsync(...) in the finally. AbLegacyDriver
DisposeAsync itself calls ShutdownAsync, so the driver is shut down twice on the
normal path. ShutdownAsync is written to be idempotent (it clears _devices /
_tagsByName and re-enters cleanly on an empty state), so this is not a crash, but
the double teardown is redundant and slightly obscures intent — a reader has to
confirm idempotency to be sure it is safe. The await using already guarantees
cleanup on every exit path including exceptions.
Recommendation: Drop either the await using or the explicit
finally { await driver.ShutdownAsync(...) } in each command. Keeping the explicit
finally and using a plain var driver (no await using) is the clearer choice,
since the commands deliberately pass CancellationToken.None to shutdown so teardown
is not cut short by a cancelled ct.
Resolution: Resolved 2026-05-23 — replaced await using var driver with a
plain var driver in all four commands (ProbeCommand, ReadCommand,
WriteCommand, SubscribeCommand), keeping the explicit
finally { await driver.ShutdownAsync(CancellationToken.None) } as the single
teardown path. Comment in each command documents the intent so readers do not
have to verify idempotency.
Driver.AbLegacy.Cli-005
| Field | Value |
|---|---|
| Severity | Low |
| Category | Design-document adherence |
| Location | Commands/SubscribeCommand.cs:23-25, docs/Driver.AbLegacy.Cli.md:94-96 |
| Status | Resolved |
Description: The subscribe command interval option is --interval-ms
(default 1000). docs/Driver.AbLegacy.Cli.md shows the subscribe example as
otopcua-ablegacy-cli subscribe ... -i 500, which works because of the short
alias 'i', but the doc never names the long form --interval-ms or states the
1000 ms default, while the equivalent AbCip CLI help text notes "PollGroupEngine
floors sub-250ms values". The AbLegacy --interval-ms description omits that
flooring caveat, so an operator passing -i 100 against AbLegacy gets no warning
that the engine will floor it. The behaviour is identical (same PollGroupEngine)
but the documented contract drifts between the two CLIs.
Recommendation: Add the sub-250 ms flooring note to the AbLegacy
--interval-ms description for parity with the AbCip CLI, and mention the
--interval-ms long form + 1000 ms default in docs/Driver.AbLegacy.Cli.md.
Resolution: Resolved 2026-05-23 — extended the SubscribeCommand.IntervalMs
help text to match AbCip ("Publishing interval in milliseconds (default 1000).
PollGroupEngine floors sub-250ms values.") and added a paragraph under the
subscribe section in docs/Driver.AbLegacy.Cli.md naming the -i /
--interval-ms long form, the 1000 ms default, and the 250 ms floor. Regression
test CommandMetadataTests.SubscribeCommand_interval_ms_help_notes_PollGroupEngine_floor
asserts the description mentions "250".
Driver.AbLegacy.Cli-006
| Field | Value |
|---|---|
| Severity | Low |
| Category | Code organization & conventions |
| Location | Commands/ProbeCommand.cs:20-22 |
| Status | Resolved |
Description: ProbeCommand declares its --type option with no short alias,
while ReadCommand, WriteCommand, and SubscribeCommand all declare --type
with the short alias 't'. ProbeCommand also gives --address the alias 'a',
matching the other commands, so the --type omission is an inconsistency rather
than a deliberate design choice. An operator who learns -t on read will find
it silently rejected on probe.
Recommendation: Add the 't' short alias to ProbeCommand --type option
for consistency with the other three commands. (The AbCip CLI ProbeCommand has
the same omission, so a cross-CLI sweep is worthwhile.)
Resolution: Resolved 2026-05-23 — added the 't' short alias to
ProbeCommand.DataType. Regression test
CommandMetadataTests.ProbeCommand_type_has_short_alias_t (plus the parity test
Other_commands_keep_type_short_alias_t for read/write/subscribe) asserts the
short alias is present on every command. The same omission still exists in the
AbCip CLI's ProbeCommand — flagged as a sibling sweep but out of scope for
this module.
Driver.AbLegacy.Cli-007
| Field | Value |
|---|---|
| Severity | Low |
| Category | Testing coverage |
| Location | tests/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli.Tests/WriteCommandParseValueTests.cs |
| Status | Resolved |
Description: The only test file in the CLI test project covers
WriteCommand.ParseValue and ReadCommand.SynthesiseTagName. Two behaviours that
are pure logic (testable without a device) are uncovered:
(1) AbLegacyCommandBase.BuildOptions — that it sets Probe.Enabled = false,
populates Devices from Gateway/PlcType, and forwards the tag list; a
regression here silently changes every command behaviour.
(2) the out-of-range numeric path for ParseValue (short.Parse overflow,
int.Parse overflow) — ParseValue_non_numeric_for_numeric_types_throws asserts
FormatException for non-numeric input but nothing asserts the overflow path,
which is exactly the path that escapes uncaught per finding
Driver.AbLegacy.Cli-001. BuildOptions is reachable via InternalsVisibleTo
(the test assembly is already granted access).
Recommendation: Add tests for BuildOptions (probe disabled, device shape,
tag passthrough) and an overflow-input test for ParseValue so the fix for
Driver.AbLegacy.Cli-001 is locked in by a regression test.
Resolution: Resolved 2026-05-23 — added BuildOptionsTests (five tests:
probe disabled, device shape from Gateway+PlcType, tag passthrough, timeout
propagation, empty tag list) covering AbLegacyCommandBase.BuildOptions via a
nested TestCommand subclass annotated with [Command] to satisfy the CliFx
analyzer. The overflow path for ParseValue is already covered by
WriteCommandParseValueTests.ParseValue_out_of_range_throws_CommandException
(theory with short.Parse + AnalogInt overflow inputs), added when finding
Driver.AbLegacy.Cli-001 was resolved.