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 { /// /// Connects to MxAccess on the STA thread. /// 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; } } /// /// Disconnects from MxAccess on the STA thread. /// 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); } } /// /// Starts the auto-reconnect monitor loop. /// Call this after initial ConnectAsync succeeds. /// public void StartMonitorLoop() { if (!_autoReconnect) return; _reconnectCts = new CancellationTokenSource(); Task.Run(() => MonitorConnectionAsync(_reconnectCts.Token)); } /// /// Stops the auto-reconnect monitor loop. /// public void StopMonitorLoop() { _reconnectCts?.Cancel(); } /// /// Auto-reconnect monitor loop. Checks connection every monitorInterval. /// On disconnect, attempts reconnect. On failure, retries at next interval. /// 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"); } /// /// Cleans up COM objects on the STA thread after a failed connection. /// 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"); } } /// Gets the UTC time when the connection was established. public DateTime ConnectedSince { get { lock (_lock) { return _connectedSince; } } } } }