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
{
///
/// 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.
///
public sealed partial class MxAccessClient : IScadaClient
{
private static readonly ILogger Log = Serilog.Log.ForContext();
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;
// Probe configuration
private readonly string? _probeTestTagAddress;
private readonly int _probeTimeoutMs;
private readonly int _maxConsecutiveTransportFailures;
private readonly int _degradedProbeIntervalMs;
// Probe state
private int _consecutiveTransportFailures;
private bool _isDegraded;
// Stored subscriptions for reconnect replay
private readonly Dictionary> _storedSubscriptions
= new Dictionary>(StringComparer.OrdinalIgnoreCase);
// Handle-to-address mapping for resolving COM callbacks
private readonly Dictionary _handleToAddress = new Dictionary();
// Address-to-handle mapping for unsubscribe by address
private readonly Dictionary _addressToHandle
= new Dictionary(StringComparer.OrdinalIgnoreCase);
// Pending write operations tracked by item handle
private readonly Dictionary> _pendingWrites
= new Dictionary>();
public MxAccessClient(
int maxConcurrentOperations = 10,
int readTimeoutSeconds = 5,
int writeTimeoutSeconds = 5,
int monitorIntervalSeconds = 5,
bool autoReconnect = true,
string? nodeName = null,
string? galaxyName = null,
string? probeTestTagAddress = null,
int probeTimeoutMs = 5000,
int maxConsecutiveTransportFailures = 3,
int degradedProbeIntervalMs = 30000)
{
_maxConcurrentOperations = maxConcurrentOperations;
_readTimeoutMs = readTimeoutSeconds * 1000;
_writeTimeoutMs = writeTimeoutSeconds * 1000;
_monitorIntervalMs = monitorIntervalSeconds * 1000;
_autoReconnect = autoReconnect;
_nodeName = nodeName;
_galaxyName = galaxyName;
_probeTestTagAddress = probeTestTagAddress;
_probeTimeoutMs = probeTimeoutMs;
_maxConsecutiveTransportFailures = maxConsecutiveTransportFailures;
_degradedProbeIntervalMs = degradedProbeIntervalMs;
_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? 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();
}
}
}