- Replaced STA dispatch thread with Task.Run pattern for COM interop - Fixed TypedValue oneof tracking with property-level _setCase field - Added x-api-key DelegatingHandler for gRPC metadata authentication - Fixed CheckApiKey RPC to validate request body key (not header) - Integration tests: 15/17 pass (reads, subscribes, API keys, connections) - 2 write tests pending (OnWriteComplete callback timing issue) - v2 service deployed on windev port 50100 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
140 lines
4.8 KiB
C#
140 lines
4.8 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using ArchestrA.MxAccess;
|
|
using Serilog;
|
|
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
|
|
|
namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
|
{
|
|
/// <summary>
|
|
/// Wraps the ArchestrA MXAccess COM API. All COM operations
|
|
/// execute via Task.Run (thread pool / MTA), relying on COM
|
|
/// marshaling to handle cross-apartment calls.
|
|
/// </summary>
|
|
public sealed partial class MxAccessClient : IScadaClient
|
|
{
|
|
private static readonly ILogger Log = Serilog.Log.ForContext<MxAccessClient>();
|
|
|
|
private readonly object _lock = new object();
|
|
private readonly int _maxConcurrentOperations;
|
|
private readonly int _readTimeoutMs;
|
|
private readonly int _writeTimeoutMs;
|
|
private readonly int _monitorIntervalMs;
|
|
private readonly bool _autoReconnect;
|
|
private readonly string? _nodeName;
|
|
private readonly string? _galaxyName;
|
|
|
|
private readonly SemaphoreSlim _readSemaphore;
|
|
private readonly SemaphoreSlim _writeSemaphore;
|
|
|
|
// COM objects
|
|
private LMXProxyServer? _lmxProxy;
|
|
private int _connectionHandle;
|
|
|
|
// State
|
|
private ConnectionState _connectionState = ConnectionState.Disconnected;
|
|
private DateTime _connectedSince;
|
|
private bool _disposed;
|
|
|
|
// Reconnect
|
|
private CancellationTokenSource? _reconnectCts;
|
|
|
|
// Stored subscriptions for reconnect replay
|
|
private readonly Dictionary<string, Action<string, Vtq>> _storedSubscriptions
|
|
= new Dictionary<string, Action<string, Vtq>>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Handle-to-address mapping for resolving COM callbacks
|
|
private readonly Dictionary<int, string> _handleToAddress = new Dictionary<int, string>();
|
|
|
|
// Address-to-handle mapping for unsubscribe by address
|
|
private readonly Dictionary<string, int> _addressToHandle
|
|
= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
// Pending write operations tracked by item handle
|
|
private readonly Dictionary<int, TaskCompletionSource<bool>> _pendingWrites
|
|
= new Dictionary<int, TaskCompletionSource<bool>>();
|
|
|
|
public MxAccessClient(
|
|
int maxConcurrentOperations = 10,
|
|
int readTimeoutSeconds = 5,
|
|
int writeTimeoutSeconds = 5,
|
|
int monitorIntervalSeconds = 5,
|
|
bool autoReconnect = true,
|
|
string? nodeName = null,
|
|
string? galaxyName = null)
|
|
{
|
|
_maxConcurrentOperations = maxConcurrentOperations;
|
|
_readTimeoutMs = readTimeoutSeconds * 1000;
|
|
_writeTimeoutMs = writeTimeoutSeconds * 1000;
|
|
_monitorIntervalMs = monitorIntervalSeconds * 1000;
|
|
_autoReconnect = autoReconnect;
|
|
_nodeName = nodeName;
|
|
_galaxyName = galaxyName;
|
|
|
|
_readSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
|
_writeSemaphore = new SemaphoreSlim(maxConcurrentOperations, maxConcurrentOperations);
|
|
}
|
|
|
|
public bool IsConnected
|
|
{
|
|
get
|
|
{
|
|
lock (_lock)
|
|
{
|
|
return _lmxProxy != null
|
|
&& _connectionState == ConnectionState.Connected
|
|
&& _connectionHandle > 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
public ConnectionState ConnectionState
|
|
{
|
|
get { lock (_lock) { return _connectionState; } }
|
|
}
|
|
|
|
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
|
|
|
private void SetState(ConnectionState newState, string? message = null)
|
|
{
|
|
ConnectionState previousState;
|
|
lock (_lock)
|
|
{
|
|
previousState = _connectionState;
|
|
_connectionState = newState;
|
|
}
|
|
|
|
if (previousState != newState)
|
|
{
|
|
Log.Information("Connection state changed: {Previous} -> {Current} {Message}",
|
|
previousState, newState, message ?? "");
|
|
ConnectionStateChanged?.Invoke(this,
|
|
new ConnectionStateChangedEventArgs(previousState, newState, message));
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
_reconnectCts?.Cancel();
|
|
|
|
try
|
|
{
|
|
await DisconnectAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Warning(ex, "Error during disposal disconnect");
|
|
}
|
|
|
|
_readSemaphore.Dispose();
|
|
_writeSemaphore.Dispose();
|
|
_reconnectCts?.Dispose();
|
|
}
|
|
}
|
|
}
|