review(Driver.S7.Cli): endpoint validation + cancellation/flush/write-lock consistency
Re-review at 7286d320. -008 (Medium): S7CommandBase.ValidateEndpoint (port range + timeout>0)
in all commands +tests. -009 clean OperationCanceledException handling; -010 FlushLogging()
in subscribe finally; -011 lock console writes in OnDataChange. -012 (Verdict headline) deferred.
This commit is contained in:
@@ -27,6 +27,7 @@ public sealed class ProbeCommand : S7CommandBase
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
ValidateEndpoint();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var probeTag = new S7TagDefinition(
|
||||
@@ -58,9 +59,12 @@ public sealed class ProbeCommand : S7CommandBase
|
||||
await console.Output.WriteLineAsync();
|
||||
await console.Output.WriteLineAsync(SnapshotFormatter.Format(Address, snapshot[0]));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
throw; // Ctrl+C — let CliFx handle it normally.
|
||||
// Ctrl+C — re-throw so CliFx exits cleanly. The when filter ensures a
|
||||
// driver-internal timeout (different CancellationToken) is not mis-classified
|
||||
// as user cancellation and falls through to the structured-report catch below.
|
||||
throw;
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -38,6 +38,7 @@ public sealed class ReadCommand : S7CommandBase
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
ValidateEndpoint();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var tagName = SynthesiseTagName(Address, DataType);
|
||||
@@ -53,9 +54,18 @@ public sealed class ReadCommand : S7CommandBase
|
||||
// already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None)
|
||||
// in a finally block ran shutdown twice. The await-using on the next line is enough.
|
||||
await using var driver = new S7Driver(options, DriverInstanceId);
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
var snapshot = await driver.ReadAsync([tagName], ct);
|
||||
await console.Output.WriteLineAsync(SnapshotFormatter.Format(Address, snapshot[0]));
|
||||
try
|
||||
{
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
var snapshot = await driver.ReadAsync([tagName], ct);
|
||||
await console.Output.WriteLineAsync(SnapshotFormatter.Format(Address, snapshot[0]));
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Driver.S7.Cli-009: Ctrl+C during driver connect/read — exit quietly so
|
||||
// CliFx does not render a full stack trace for a user-initiated cancellation.
|
||||
await console.Output.WriteLineAsync("Cancelled.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Tag-name key used internally. Address + type is already unique.</summary>
|
||||
|
||||
@@ -33,6 +33,7 @@ public sealed class SubscribeCommand : S7CommandBase
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
ValidateEndpoint();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var tagName = ReadCommand.SynthesiseTagName(Address, DataType);
|
||||
@@ -48,6 +49,9 @@ public sealed class SubscribeCommand : S7CommandBase
|
||||
// ran shutdown twice. Only UnsubscribeAsync stays in the finally block — that's a subscription
|
||||
// lifecycle concern that is not part of driver disposal.
|
||||
await using var driver = new S7Driver(options, DriverInstanceId);
|
||||
// Driver.S7.Cli-011: serialize console writes from the PollGroupEngine background
|
||||
// thread so overlapping poll ticks cannot interleave partial lines on the output.
|
||||
var writeLock = new object();
|
||||
ISubscriptionHandle? handle = null;
|
||||
try
|
||||
{
|
||||
@@ -62,7 +66,10 @@ public sealed class SubscribeCommand : S7CommandBase
|
||||
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 (writeLock)
|
||||
{
|
||||
console.Output.WriteLine(line);
|
||||
}
|
||||
};
|
||||
|
||||
handle = await driver.SubscribeAsync([tagName], TimeSpan.FromMilliseconds(IntervalMs), ct);
|
||||
@@ -85,6 +92,11 @@ public sealed class SubscribeCommand : S7CommandBase
|
||||
try { await driver.UnsubscribeAsync(handle, CancellationToken.None); }
|
||||
catch { /* teardown best-effort */ }
|
||||
}
|
||||
// Driver.S7.Cli-010: flush Serilog before process exit so buffered log lines
|
||||
// emitted just before Ctrl+C (e.g. reconnect warnings from the PollGroupEngine)
|
||||
// are not lost on abrupt termination. DriverCommandBase.ConfigureLogging() docs
|
||||
// require this call in a finally block.
|
||||
FlushLogging();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ public sealed class WriteCommand : S7CommandBase
|
||||
public override async ValueTask ExecuteAsync(IConsole console)
|
||||
{
|
||||
ConfigureLogging();
|
||||
ValidateEndpoint();
|
||||
var ct = console.RegisterCancellationHandler();
|
||||
|
||||
var tagName = ReadCommand.SynthesiseTagName(Address, DataType);
|
||||
@@ -60,9 +61,18 @@ public sealed class WriteCommand : S7CommandBase
|
||||
// already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None)
|
||||
// in a finally block ran shutdown twice. The await-using on the next line is enough.
|
||||
await using var driver = new S7Driver(options, DriverInstanceId);
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
var results = await driver.WriteAsync([new WriteRequest(tagName, parsed)], ct);
|
||||
await console.Output.WriteLineAsync(SnapshotFormatter.FormatWrite(Address, results[0]));
|
||||
try
|
||||
{
|
||||
await driver.InitializeAsync("{}", ct);
|
||||
var results = await driver.WriteAsync([new WriteRequest(tagName, parsed)], ct);
|
||||
await console.Output.WriteLineAsync(SnapshotFormatter.FormatWrite(Address, results[0]));
|
||||
}
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested)
|
||||
{
|
||||
// Driver.S7.Cli-009: Ctrl+C during driver connect/write — exit quietly so
|
||||
// CliFx does not render a full stack trace for a user-initiated cancellation.
|
||||
await console.Output.WriteLineAsync("Cancelled.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Parse <c>--value</c> per <see cref="S7DataType"/>, invariant culture throughout.</summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.S7.Cli;
|
||||
@@ -65,4 +66,25 @@ public abstract class S7CommandBase : DriverCommandBase
|
||||
|
||||
/// <summary>Gets the driver instance ID for this CLI session.</summary>
|
||||
protected string DriverInstanceId => $"s7-cli-{Host}:{Port}";
|
||||
|
||||
/// <summary>
|
||||
/// Driver.S7.Cli-008: validate the endpoint flags at parse time so the operator
|
||||
/// gets a clear <see cref="CommandException"/> instead of an opaque socket or
|
||||
/// argument exception thrown deep inside the S7.Net stack.
|
||||
/// <list type="bullet">
|
||||
/// <item><c>--port</c>: 1..65535 (IANA TCP port space).</item>
|
||||
/// <item><c>--timeout-ms</c>: strictly positive — a non-positive value would
|
||||
/// propagate as an immediate-timeout into S7.Net and surface as a confusing
|
||||
/// <see cref="TimeoutException"/>.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
protected void ValidateEndpoint()
|
||||
{
|
||||
if (Port < 1 || Port > 65535)
|
||||
throw new CommandException(
|
||||
$"--port must be in 1..65535 (got {Port}).");
|
||||
if (TimeoutMs <= 0)
|
||||
throw new CommandException(
|
||||
$"--timeout-ms must be strictly positive (got {TimeoutMs}).");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user