fix(driver-twincat-cli): resolve Low code-review findings (Driver.TwinCAT.Cli-001,002,003,004,005,006,007)
- Driver.TwinCAT.Cli-001: TwinCATCommandBase.Validate rejects non-positive TimeoutMs / IntervalMs and AmsPort outside 1..65535; ExecuteAsync calls it first. - Driver.TwinCAT.Cli-002: SubscribeCommand serialises every WriteLine through a writeLock to remove the notification-callback vs banner interleave risk. - Driver.TwinCAT.Cli-003: SubscribeCommand.DescribeMechanism derives the banner label from the returned ISubscriptionHandle.DiagnosticId so it can't disagree with what the driver actually did. - Driver.TwinCAT.Cli-004: introduced TwinCATTagCommandBase carrying --poll-only + BuildOptions; BrowseCommand stays on the slimmer TwinCATCommandBase so --poll-only no longer surfaces in browse --help. - Driver.TwinCAT.Cli-005: ProbeCommand --type now carries the 't' short alias to match the other commands. - Driver.TwinCAT.Cli-006: 35 new tests covering Gateway / AmsAddress parse / BuildOptions / PollOnly / browse-helpers / probe-alias / mechanism derivation. - Driver.TwinCAT.Cli-007: replaced the empty-init <inheritdoc/> with an explicit summary warning future maintainers about the no-op init. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands;
|
||||
/// when <c>EnableControllerBrowse = true</c> — structured UDTs / function-block instances
|
||||
/// won't appear because the driver filters to the supported primitive surface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Inherits from <see cref="TwinCATCommandBase"/> rather than
|
||||
/// <see cref="TwinCATTagCommandBase"/> so the <c>--poll-only</c> flag does NOT surface in
|
||||
/// <c>browse --help</c>: browse never subscribes, the flag would be a no-op, and the help
|
||||
/// text would mislead users (Driver.TwinCAT.Cli-004).
|
||||
/// </remarks>
|
||||
[Command("browse", Description = "Enumerate controller symbols via the driver's DiscoverAsync walk.")]
|
||||
public sealed class BrowseCommand : TwinCATCommandBase
|
||||
{
|
||||
@@ -25,18 +31,21 @@ public sealed class BrowseCommand : TwinCATCommandBase
|
||||
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
Validate();
|
||||
ConfigureLogging();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
// Browse-only — no declared tags. EnableControllerBrowse=true flips DiscoverAsync's
|
||||
// symbol-walk on so every recognized primitive surfaces through the builder.
|
||||
// symbol-walk on so every recognized primitive surfaces through the builder. Native
|
||||
// ADS notifications are irrelevant here (DiscoverAsync never subscribes); leave the
|
||||
// default on so the options record matches the production wiring.
|
||||
var options = new TwinCATDriverOptions
|
||||
{
|
||||
Devices = [new TwinCATDeviceOptions(Gateway, $"cli-{AmsNetId}:{AmsPort}")],
|
||||
Tags = [],
|
||||
Timeout = Timeout,
|
||||
Probe = new TwinCATProbeOptions { Enabled = false },
|
||||
UseNativeNotifications = !PollOnly,
|
||||
UseNativeNotifications = true,
|
||||
EnableControllerBrowse = true,
|
||||
};
|
||||
|
||||
@@ -52,10 +61,8 @@ public sealed class BrowseCommand : TwinCATCommandBase
|
||||
await driver.ShutdownAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
var matched = builder.Variables
|
||||
.Where(v => string.IsNullOrEmpty(Prefix) || v.BrowseName.StartsWith(Prefix, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
var printLimit = Max <= 0 ? matched.Count : Math.Min(Max, matched.Count);
|
||||
var matched = FilterByPrefix(builder.Variables, Prefix);
|
||||
var printLimit = PrintLimit(matched.Count, Max);
|
||||
|
||||
await console.Output.WriteLineAsync($"AMS: {AmsNetId}:{AmsPort}");
|
||||
await console.Output.WriteLineAsync(
|
||||
@@ -64,8 +71,7 @@ public sealed class BrowseCommand : TwinCATCommandBase
|
||||
|
||||
foreach (var v in matched.Take(printLimit))
|
||||
{
|
||||
var access = v.Info.SecurityClass == SecurityClassification.ViewOnly ? "RO" : "RW";
|
||||
await console.Output.WriteLineAsync($" [{access}] {v.Info.DriverDataType,-8} {v.BrowseName}");
|
||||
await console.Output.WriteLineAsync($" [{AccessTag(v.Info)}] {v.Info.DriverDataType,-8} {v.BrowseName}");
|
||||
}
|
||||
|
||||
if (matched.Count > printLimit)
|
||||
@@ -73,7 +79,35 @@ public sealed class BrowseCommand : TwinCATCommandBase
|
||||
$" … {matched.Count - printLimit} more — raise --max or tighten --prefix");
|
||||
}
|
||||
|
||||
private sealed class CollectingAddressSpaceBuilder : IAddressSpaceBuilder
|
||||
/// <summary>
|
||||
/// Case-sensitive prefix filter. A null/empty prefix keeps everything; otherwise we
|
||||
/// keep symbols whose browse name starts with <paramref name="prefix"/> under
|
||||
/// <see cref="StringComparison.Ordinal"/> — TwinCAT identifiers are case-sensitive on
|
||||
/// the wire, so a relaxed match would be misleading.
|
||||
/// </summary>
|
||||
internal static List<(string BrowseName, DriverAttributeInfo Info)> FilterByPrefix(
|
||||
IReadOnlyList<(string BrowseName, DriverAttributeInfo Info)> source, string? prefix)
|
||||
=> source
|
||||
.Where(v => string.IsNullOrEmpty(prefix) || v.BrowseName.StartsWith(prefix, StringComparison.Ordinal))
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Cap-to-max projection. <paramref name="max"/> <= 0 means unbounded, otherwise the
|
||||
/// min of <paramref name="matchedCount"/> and <paramref name="max"/>.
|
||||
/// </summary>
|
||||
internal static int PrintLimit(int matchedCount, int max)
|
||||
=> max <= 0 ? matchedCount : Math.Min(max, matchedCount);
|
||||
|
||||
/// <summary>
|
||||
/// Coarse RO/RW label used in the browse output. <see cref="SecurityClassification.ViewOnly"/>
|
||||
/// is the only classification that is unconditionally read-only; everything else can be
|
||||
/// written from at least one ACL tier, so the CLI labels it RW. The real per-tier
|
||||
/// authorization is enforced server-side.
|
||||
/// </summary>
|
||||
internal static string AccessTag(DriverAttributeInfo info)
|
||||
=> info.SecurityClass == SecurityClassification.ViewOnly ? "RO" : "RW";
|
||||
|
||||
internal sealed class CollectingAddressSpaceBuilder : IAddressSpaceBuilder
|
||||
{
|
||||
public List<(string BrowseName, DriverAttributeInfo Info)> Variables { get; } = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user