fix(client-cli): resolve Medium code-review findings (Client.CLI-001, Client.CLI-005)
Client.CLI-001: parse --start/--end with CultureInfo.InvariantCulture and DateTimeStyles.AssumeUniversal|AdjustToUniversal so dates are culture-stable. Client.CLI-005: SDK notification callbacks now hand off to an unbounded channel drained on the main thread; handlers are unsubscribed before the summary phase so no notification interleaves with console output. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading.Channels;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
using Opc.Ua;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.CLI.Helpers;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared;
|
||||
using ZB.MOM.WW.OtOpcUa.Client.Shared.Models;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Client.CLI.Commands;
|
||||
|
||||
@@ -63,19 +65,35 @@ public class SubscribeCommand : CommandBase
|
||||
var everBad = new ConcurrentDictionary<string, byte>();
|
||||
var displayNameByNodeId = targets.ToDictionary(t => t.nodeId.ToString(), t => t.displayPath);
|
||||
|
||||
service.DataChanged += (_, e) =>
|
||||
// Channel serialises notification-thread writes to the main async loop so that
|
||||
// concurrent SDK callbacks and main-thread summary output never interleave on
|
||||
// the shared TextWriter.
|
||||
var outputChannel = Channel.CreateUnbounded<string>(
|
||||
new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
void DataChangedHandler(object? sender, DataChangedEventArgs e)
|
||||
{
|
||||
var key = e.NodeId.ToString();
|
||||
lastStatus[key] = (e.Value.StatusCode, DateTime.UtcNow, e.Value.Value);
|
||||
updateCount.AddOrUpdate(key, 1, (_, v) => v + 1);
|
||||
if (!StatusCode.IsGood(e.Value.StatusCode))
|
||||
everBad.TryAdd(key, 0);
|
||||
if (!Quiet)
|
||||
try
|
||||
{
|
||||
console.Output.WriteLine(
|
||||
$"[{e.Value.SourceTimestamp:O}] {displayNameByNodeId.GetValueOrDefault(key, key)} = {e.Value.Value} ({e.Value.StatusCode})");
|
||||
var key = e.NodeId.ToString();
|
||||
lastStatus[key] = (e.Value.StatusCode, DateTime.UtcNow, e.Value.Value);
|
||||
updateCount.AddOrUpdate(key, 1, (_, v) => v + 1);
|
||||
if (!StatusCode.IsGood(e.Value.StatusCode))
|
||||
everBad.TryAdd(key, 0);
|
||||
if (!Quiet)
|
||||
{
|
||||
var line =
|
||||
$"[{e.Value.SourceTimestamp:O}] {displayNameByNodeId.GetValueOrDefault(key, key)} = {e.Value.Value} ({e.Value.StatusCode})";
|
||||
outputChannel.Writer.TryWrite(line);
|
||||
}
|
||||
}
|
||||
};
|
||||
catch
|
||||
{
|
||||
// Never let handler exceptions escape into the SDK callback.
|
||||
}
|
||||
}
|
||||
|
||||
service.DataChanged += DataChangedHandler;
|
||||
|
||||
var subscribed = 0;
|
||||
foreach (var (nodeId, _) in targets)
|
||||
@@ -94,6 +112,14 @@ public class SubscribeCommand : CommandBase
|
||||
await console.Output.WriteLineAsync(
|
||||
$"Subscribed to {subscribed}/{targets.Count} nodes (interval: {Interval}ms). Press Ctrl+C to stop and print summary.");
|
||||
|
||||
// Drain the output channel on the main thread until cancellation fires.
|
||||
using var drainCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
var drainTask = Task.Run(async () =>
|
||||
{
|
||||
await foreach (var line in outputChannel.Reader.ReadAllAsync(drainCts.Token))
|
||||
await console.Output.WriteLineAsync(line);
|
||||
}, CancellationToken.None);
|
||||
|
||||
try
|
||||
{
|
||||
if (DurationSeconds > 0)
|
||||
@@ -105,6 +131,12 @@ public class SubscribeCommand : CommandBase
|
||||
{
|
||||
}
|
||||
|
||||
// Stop accepting new notifications before writing the summary.
|
||||
service.DataChanged -= DataChangedHandler;
|
||||
outputChannel.Writer.Complete();
|
||||
await drainCts.CancelAsync();
|
||||
try { await drainTask; } catch (OperationCanceledException) { }
|
||||
|
||||
// Summary
|
||||
var summary = new List<string>();
|
||||
summary.Add("");
|
||||
@@ -127,10 +159,10 @@ public class SubscribeCommand : CommandBase
|
||||
}
|
||||
|
||||
var neverWentBad = targets
|
||||
.Where(t => !everBad.ContainsKey(t.nodeId.ToString()))
|
||||
.Where(t => lastStatus.ContainsKey(t.nodeId.ToString()) && !everBad.ContainsKey(t.nodeId.ToString()))
|
||||
.Select(t => t.displayPath)
|
||||
.ToList();
|
||||
var didGoBad = targets.Count - neverWentBad.Count;
|
||||
var didGoBad = targets.Count(t => everBad.ContainsKey(t.nodeId.ToString()));
|
||||
|
||||
summary.Add($"Total subscribed: {targets.Count}");
|
||||
summary.Add($" Ever went BAD during window: {didGoBad}");
|
||||
|
||||
Reference in New Issue
Block a user