fix(driver-ablegacy-cli): resolve Low code-review findings (Driver.AbLegacy.Cli-002,003,004,005,006,007)

- 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>
This commit is contained in:
Joseph Doherty
2026-05-23 08:34:32 -04:00
parent 759af8c1bb
commit f46e126208
8 changed files with 295 additions and 23 deletions

View File

@@ -17,7 +17,7 @@ public sealed class ProbeCommand : AbLegacyCommandBase
"the pre-populated register every SLC / MicroLogix / PLC-5 ships with.")]
public string Address { get; init; } = "N7:0";
[CommandOption("type", Description =
[CommandOption("type", 't', Description =
"PCCC data type of the probe address (default Int — matches N files).")]
public AbLegacyDataType DataType { get; init; } = AbLegacyDataType.Int;
@@ -34,7 +34,10 @@ public sealed class ProbeCommand : AbLegacyCommandBase
Writable: false);
var options = BuildOptions([probeTag]);
await using var driver = new AbLegacyDriver(options, DriverInstanceId);
// Plain `var driver`: explicit ShutdownAsync(CancellationToken.None) in the
// finally is the deliberate teardown path; combining it with `await using`
// (which itself calls ShutdownAsync) would tear the driver down twice.
var driver = new AbLegacyDriver(options, DriverInstanceId);
try
{
await driver.InitializeAsync("{}", ct);

View File

@@ -36,7 +36,10 @@ public sealed class ReadCommand : AbLegacyCommandBase
Writable: false);
var options = BuildOptions([tag]);
await using var driver = new AbLegacyDriver(options, DriverInstanceId);
// Plain `var driver`: explicit ShutdownAsync(CancellationToken.None) in the
// finally is the deliberate teardown path; combining it with `await using`
// (which itself calls ShutdownAsync) would tear the driver down twice.
var driver = new AbLegacyDriver(options, DriverInstanceId);
try
{
await driver.InitializeAsync("{}", ct);

View File

@@ -21,7 +21,8 @@ public sealed class SubscribeCommand : AbLegacyCommandBase
public AbLegacyDataType DataType { get; init; } = AbLegacyDataType.Int;
[CommandOption("interval-ms", 'i', Description =
"Publishing interval in milliseconds (default 1000).")]
"Publishing interval in milliseconds (default 1000). PollGroupEngine floors " +
"sub-250ms values.")]
public int IntervalMs { get; init; } = 1000;
public override async ValueTask ExecuteAsync(IConsole console)
@@ -38,8 +39,17 @@ public sealed class SubscribeCommand : AbLegacyCommandBase
Writable: false);
var options = BuildOptions([tag]);
await using var driver = new AbLegacyDriver(options, DriverInstanceId);
// Plain `var driver` (no `await using`): driver.DisposeAsync internally calls
// ShutdownAsync, so combining `await using` with an explicit finally-shutdown
// would tear the driver down twice. The explicit teardown is preferred because
// it deliberately passes CancellationToken.None — `await using` would otherwise
// happen on a cancelled `ct` path which can cut teardown short.
var driver = new AbLegacyDriver(options, DriverInstanceId);
ISubscriptionHandle? handle = null;
// Serialise console writes from the poll-thread OnDataChange callback against
// the command-thread "Subscribed to ..." line and against each other; the
// PollGroupEngine raises change events on a background timer/loop thread.
var consoleGate = new object();
try
{
await driver.InitializeAsync("{}", ct);
@@ -49,13 +59,19 @@ public sealed class SubscribeCommand : AbLegacyCommandBase
var line = $"[{DateTime.UtcNow:HH:mm:ss.fff}] " +
$"{e.FullReference} = {SnapshotFormatter.FormatValue(e.Snapshot.Value)} " +
$"({SnapshotFormatter.FormatStatus(e.Snapshot.StatusCode)})";
console.Output.WriteLine(line);
lock (consoleGate)
{
console.Output.WriteLine(line);
}
};
handle = await driver.SubscribeAsync([tagName], TimeSpan.FromMilliseconds(IntervalMs), ct);
await console.Output.WriteLineAsync(
$"Subscribed to {Address} @ {IntervalMs}ms. Ctrl+C to stop.");
lock (consoleGate)
{
console.Output.WriteLine(
$"Subscribed to {Address} @ {IntervalMs}ms. Ctrl+C to stop.");
}
try
{
await Task.Delay(System.Threading.Timeout.InfiniteTimeSpan, ct);

View File

@@ -25,7 +25,7 @@ public sealed class WriteCommand : AbLegacyCommandBase
public AbLegacyDataType DataType { get; init; } = AbLegacyDataType.Int;
[CommandOption("value", 'v', Description =
"Value to write. Parsed per --type (booleans accept true/false/1/0).",
"Value to write. Parsed per --type (booleans accept true/false, 1/0, on/off, yes/no).",
IsRequired = true)]
public string Value { get; init; } = default!;
@@ -45,7 +45,10 @@ public sealed class WriteCommand : AbLegacyCommandBase
var parsed = ParseValue(Value, DataType);
await using var driver = new AbLegacyDriver(options, DriverInstanceId);
// Plain `var driver`: explicit ShutdownAsync(CancellationToken.None) in the
// finally is the deliberate teardown path; combining it with `await using`
// (which itself calls ShutdownAsync) would tear the driver down twice.
var driver = new AbLegacyDriver(options, DriverInstanceId);
try
{
await driver.InitializeAsync("{}", ct);