feat(lmxproxy): phase 2 — host core (MxAccessClient, SessionManager, SubscriptionManager)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
130
lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.cs
Normal file
130
lmxproxy/src/ZB.MOM.WW.LmxProxy.Host/MxAccess/MxAccessClient.cs
Normal file
@@ -0,0 +1,130 @@
|
||||
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 on a dedicated STA thread via <see cref="StaDispatchThread"/>.
|
||||
/// </summary>
|
||||
public sealed partial class MxAccessClient : IScadaClient
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<MxAccessClient>();
|
||||
|
||||
private readonly StaDispatchThread _staThread;
|
||||
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 — only accessed on STA thread
|
||||
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);
|
||||
|
||||
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);
|
||||
_staThread = new StaDispatchThread();
|
||||
}
|
||||
|
||||
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();
|
||||
_staThread.Dispose();
|
||||
_reconnectCts?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user