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,7 +1,9 @@
|
||||
using System.Threading.Channels;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Infrastructure;
|
||||
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;
|
||||
|
||||
@@ -49,19 +51,33 @@ public class AlarmsCommand : CommandBase
|
||||
|
||||
var sourceNodeId = NodeIdParser.Parse(NodeId);
|
||||
|
||||
service.AlarmEvent += (_, e) =>
|
||||
// Channel serialises SDK notification-thread writes to the main async loop so
|
||||
// that concurrent alarm callbacks never interleave on the shared TextWriter.
|
||||
var outputChannel = Channel.CreateUnbounded<string>(
|
||||
new UnboundedChannelOptions { SingleReader = true });
|
||||
|
||||
void AlarmEventHandler(object? sender, AlarmEventArgs e)
|
||||
{
|
||||
console.Output.WriteLine($"[{e.Time:O}] ALARM {e.SourceName}");
|
||||
console.Output.WriteLine($" Condition: {e.ConditionName}");
|
||||
var activeStr = e.ActiveState ? "Active" : "Inactive";
|
||||
var ackedStr = e.AckedState ? "Acknowledged" : "Unacknowledged";
|
||||
console.Output.WriteLine($" State: {activeStr}, {ackedStr}");
|
||||
console.Output.WriteLine($" Severity: {e.Severity}");
|
||||
if (!string.IsNullOrEmpty(e.Message))
|
||||
console.Output.WriteLine($" Message: {e.Message}");
|
||||
console.Output.WriteLine($" Retain: {e.Retain}");
|
||||
console.Output.WriteLine();
|
||||
};
|
||||
try
|
||||
{
|
||||
var activeStr = e.ActiveState ? "Active" : "Inactive";
|
||||
var ackedStr = e.AckedState ? "Acknowledged" : "Unacknowledged";
|
||||
outputChannel.Writer.TryWrite($"[{e.Time:O}] ALARM {e.SourceName}");
|
||||
outputChannel.Writer.TryWrite($" Condition: {e.ConditionName}");
|
||||
outputChannel.Writer.TryWrite($" State: {activeStr}, {ackedStr}");
|
||||
outputChannel.Writer.TryWrite($" Severity: {e.Severity}");
|
||||
if (!string.IsNullOrEmpty(e.Message))
|
||||
outputChannel.Writer.TryWrite($" Message: {e.Message}");
|
||||
outputChannel.Writer.TryWrite($" Retain: {e.Retain}");
|
||||
outputChannel.Writer.TryWrite(string.Empty);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Never let handler exceptions escape into the SDK callback.
|
||||
}
|
||||
}
|
||||
|
||||
service.AlarmEvent += AlarmEventHandler;
|
||||
|
||||
await service.SubscribeAlarmsAsync(sourceNodeId, Interval, ct);
|
||||
await console.Output.WriteLineAsync(
|
||||
@@ -78,6 +94,14 @@ public class AlarmsCommand : CommandBase
|
||||
await console.Output.WriteLineAsync($"Condition refresh not supported: {ex.Message}");
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Wait until cancellation
|
||||
try
|
||||
{
|
||||
@@ -88,6 +112,12 @@ public class AlarmsCommand : CommandBase
|
||||
// Expected on Ctrl+C
|
||||
}
|
||||
|
||||
// Stop accepting new notifications before writing final output.
|
||||
service.AlarmEvent -= AlarmEventHandler;
|
||||
outputChannel.Writer.Complete();
|
||||
await drainCts.CancelAsync();
|
||||
try { await drainTask; } catch (OperationCanceledException) { }
|
||||
|
||||
await service.UnsubscribeAlarmsAsync();
|
||||
await console.Output.WriteLineAsync("Unsubscribed.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user