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(); } } }