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:
@@ -0,0 +1,199 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
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
|
||||
{
|
||||
public sealed partial class MxAccessClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Connects to MxAccess on the STA thread.
|
||||
/// </summary>
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(MxAccessClient));
|
||||
if (IsConnected) return;
|
||||
|
||||
SetState(ConnectionState.Connecting);
|
||||
|
||||
try
|
||||
{
|
||||
await _staThread.DispatchAsync(() =>
|
||||
{
|
||||
// Create COM object
|
||||
_lmxProxy = new LMXProxyServer();
|
||||
|
||||
// Wire event handlers
|
||||
_lmxProxy.OnDataChange += OnDataChange;
|
||||
_lmxProxy.OnWriteComplete += OnWriteComplete;
|
||||
|
||||
// Register with MxAccess
|
||||
_connectionHandle = _lmxProxy.Register("ZB.MOM.WW.LmxProxy.Host");
|
||||
});
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_connectedSince = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
SetState(ConnectionState.Connected);
|
||||
Log.Information("Connected to MxAccess (handle={Handle})", _connectionHandle);
|
||||
|
||||
// Recreate any stored subscriptions from a previous connection
|
||||
await RecreateStoredSubscriptionsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to connect to MxAccess");
|
||||
await CleanupComObjectsAsync();
|
||||
SetState(ConnectionState.Error, ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from MxAccess on the STA thread.
|
||||
/// </summary>
|
||||
public async Task DisconnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (!IsConnected) return;
|
||||
|
||||
SetState(ConnectionState.Disconnecting);
|
||||
|
||||
try
|
||||
{
|
||||
await _staThread.DispatchAsync(() =>
|
||||
{
|
||||
if (_lmxProxy != null && _connectionHandle > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Remove event handlers first
|
||||
_lmxProxy.OnDataChange -= OnDataChange;
|
||||
_lmxProxy.OnWriteComplete -= OnWriteComplete;
|
||||
|
||||
// Unregister
|
||||
_lmxProxy.Unregister(_connectionHandle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error during MxAccess unregister");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Force-release COM object
|
||||
Marshal.ReleaseComObject(_lmxProxy);
|
||||
_lmxProxy = null;
|
||||
_connectionHandle = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
SetState(ConnectionState.Disconnected);
|
||||
Log.Information("Disconnected from MxAccess");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error during disconnect");
|
||||
SetState(ConnectionState.Error, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts the auto-reconnect monitor loop.
|
||||
/// Call this after initial ConnectAsync succeeds.
|
||||
/// </summary>
|
||||
public void StartMonitorLoop()
|
||||
{
|
||||
if (!_autoReconnect) return;
|
||||
|
||||
_reconnectCts = new CancellationTokenSource();
|
||||
Task.Run(() => MonitorConnectionAsync(_reconnectCts.Token));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the auto-reconnect monitor loop.
|
||||
/// </summary>
|
||||
public void StopMonitorLoop()
|
||||
{
|
||||
_reconnectCts?.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-reconnect monitor loop. Checks connection every monitorInterval.
|
||||
/// On disconnect, attempts reconnect. On failure, retries at next interval.
|
||||
/// </summary>
|
||||
private async Task MonitorConnectionAsync(CancellationToken ct)
|
||||
{
|
||||
Log.Information("Connection monitor loop started (interval={IntervalMs}ms)", _monitorIntervalMs);
|
||||
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_monitorIntervalMs, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (IsConnected) continue;
|
||||
|
||||
Log.Information("MxAccess disconnected, attempting reconnect...");
|
||||
SetState(ConnectionState.Reconnecting);
|
||||
|
||||
try
|
||||
{
|
||||
await ConnectAsync(ct);
|
||||
Log.Information("Reconnected to MxAccess successfully");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Reconnect attempt failed, will retry in {IntervalMs}ms", _monitorIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Information("Connection monitor loop exited");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up COM objects on the STA thread after a failed connection.
|
||||
/// </summary>
|
||||
private async Task CleanupComObjectsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _staThread.DispatchAsync(() =>
|
||||
{
|
||||
if (_lmxProxy != null)
|
||||
{
|
||||
try { _lmxProxy.OnDataChange -= OnDataChange; } catch { }
|
||||
try { _lmxProxy.OnWriteComplete -= OnWriteComplete; } catch { }
|
||||
try { Marshal.ReleaseComObject(_lmxProxy); } catch { }
|
||||
_lmxProxy = null;
|
||||
}
|
||||
_connectionHandle = 0;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error during COM object cleanup");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the UTC time when the connection was established.</summary>
|
||||
public DateTime ConnectedSince
|
||||
{
|
||||
get { lock (_lock) { return _connectedSince; } }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user