fix(client-shared): resolve Medium code-review findings (Client.Shared-001, -002, -007, -008)
Client.Shared-001: lowered the OnAlarmEventNotification early-return guard from <6 to <1; per-index field guards already default missing fields safely. Client.Shared-002: GetRedundancyInfoAsync replaces unguarded unboxing casts with StatusCode.IsGood + Convert.ToInt32/ToByte, defaulting on bad reads. Client.Shared-007: alarm fallback Task.Run guards on ReferenceEquals(session, _session) and drops stale alarms on ObjectDisposedException after failover. Client.Shared-008: WriteValueAsync rejects type inference from bad/null reads; ValueConverter wraps parse failures in a descriptive FormatException. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -187,6 +187,9 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
if (value is string rawString)
|
||||
{
|
||||
var currentDataValue = await _session!.ReadValueAsync(nodeId, ct);
|
||||
if (!StatusCode.IsGood(currentDataValue.StatusCode) || currentDataValue.Value == null)
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot infer target type for node {nodeId}: read returned status {currentDataValue.StatusCode} with no value. Provide a typed value instead of a string.");
|
||||
typedValue = ValueConverter.ConvertValue(rawString, currentDataValue.Value);
|
||||
}
|
||||
|
||||
@@ -388,10 +391,17 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
|
||||
var redundancySupportValue =
|
||||
await _session!.ReadValueAsync(VariableIds.Server_ServerRedundancy_RedundancySupport, ct);
|
||||
var redundancyMode = ((RedundancySupport)(int)redundancySupportValue.Value).ToString();
|
||||
RedundancySupport redundancySupport;
|
||||
if (StatusCode.IsGood(redundancySupportValue.StatusCode) && redundancySupportValue.Value != null)
|
||||
redundancySupport = (RedundancySupport)Convert.ToInt32(redundancySupportValue.Value);
|
||||
else
|
||||
redundancySupport = RedundancySupport.None;
|
||||
var redundancyMode = redundancySupport.ToString();
|
||||
|
||||
var serviceLevelValue = await _session.ReadValueAsync(VariableIds.Server_ServiceLevel, ct);
|
||||
var serviceLevel = (byte)serviceLevelValue.Value;
|
||||
var serviceLevel = StatusCode.IsGood(serviceLevelValue.StatusCode) && serviceLevelValue.Value != null
|
||||
? Convert.ToByte(serviceLevelValue.Value)
|
||||
: (byte)0;
|
||||
|
||||
string[] serverUris = [];
|
||||
try
|
||||
@@ -627,7 +637,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
private void OnAlarmEventNotification(EventFieldList eventFields)
|
||||
{
|
||||
var fields = eventFields.EventFields;
|
||||
if (fields == null || fields.Count < 6)
|
||||
if (fields == null || fields.Count < 1)
|
||||
return;
|
||||
|
||||
var eventId = fields.Count > 0 ? fields[0].Value as byte[] : null;
|
||||
@@ -656,6 +666,8 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
// Fallback: read InAlarm/Acked from condition node Galaxy attributes
|
||||
// when the server doesn't populate standard event fields.
|
||||
// Must run on a background thread to avoid deadlocking the notification thread.
|
||||
// Capture the session reference now; skip supplemental reads if the session has
|
||||
// been replaced by a concurrent failover before the Task.Run body executes.
|
||||
if (ackedField == null && activeField == null && conditionNodeId != null && _session != null)
|
||||
{
|
||||
var session = _session;
|
||||
@@ -663,6 +675,11 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
var capturedMessage = message;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// If the session was replaced by a failover before we started reading,
|
||||
// skip the supplemental reads to avoid hitting a disposed session.
|
||||
if (!ReferenceEquals(session, _session))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var inAlarmValue = await session.ReadValueAsync(NodeId.Parse($"{capturedConditionNodeId}.InAlarm"));
|
||||
@@ -687,9 +704,16 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
}
|
||||
catch { /* DescAttrName may not exist */ }
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Session was disposed during supplemental reads (concurrent failover);
|
||||
// skip the event rather than delivering stale/default states.
|
||||
Logger.Debug("Supplemental alarm read skipped — session disposed during failover for {ConditionNodeId}", capturedConditionNodeId);
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Supplemental read failed; use defaults
|
||||
// Other supplemental read failure; deliver event with defaults
|
||||
}
|
||||
|
||||
AlarmEvent?.Invoke(this, new AlarmEventArgs(
|
||||
|
||||
Reference in New Issue
Block a user