LmxProxy is no longer needed. Moved the entire lmxproxy/ workspace, DCL adapter files, and related docs to deprecated/. Removed LmxProxy registration from DataConnectionFactory, project reference from DCL, protocol option from UI, and cleaned up all requirement docs.
299 lines
10 KiB
C#
299 lines
10 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using ArchestrA.MxAccess;
|
|
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
|
|
|
namespace ZB.MOM.WW.LmxProxy.Host.Implementation
|
|
{
|
|
/// <summary>
|
|
/// Connection management for MxAccessClient.
|
|
/// </summary>
|
|
public sealed partial class MxAccessClient
|
|
{
|
|
/// <summary>
|
|
/// Asynchronously connects to the MxAccess server.
|
|
/// </summary>
|
|
/// <param name="ct">A cancellation token to observe while waiting for the task to complete.</param>
|
|
/// <returns>A task that represents the asynchronous connect operation.</returns>
|
|
/// <exception cref="ObjectDisposedException">Thrown if the client has been disposed.</exception>
|
|
/// <exception cref="InvalidOperationException">Thrown if registration with MxAccess fails.</exception>
|
|
/// <exception cref="Exception">Thrown if any other error occurs during connection.</exception>
|
|
public async Task ConnectAsync(CancellationToken ct = default)
|
|
{
|
|
// COM operations must run on STA thread, so we use Task.Run here
|
|
await Task.Run(ConnectInternal, ct);
|
|
|
|
// Recreate stored subscriptions after successful connection
|
|
await RecreateStoredSubscriptionsAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asynchronously disconnects from the MxAccess server and cleans up resources.
|
|
/// </summary>
|
|
/// <param name="ct">A cancellation token to observe while waiting for the task to complete.</param>
|
|
/// <returns>A task that represents the asynchronous disconnect operation.</returns>
|
|
public async Task DisconnectAsync(CancellationToken ct = default)
|
|
{
|
|
// COM operations must run on STA thread, so we use Task.Run here
|
|
await Task.Run(() => DisconnectInternal(), ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal synchronous connection logic.
|
|
/// </summary>
|
|
private void ConnectInternal()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
ValidateNotDisposed();
|
|
|
|
if (IsConnected)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Logger.Information("Attempting to connect to MxAccess");
|
|
SetConnectionState(ConnectionState.Connecting);
|
|
|
|
InitializeMxAccessConnection();
|
|
RegisterWithMxAccess();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Error(ex, "Failed to connect to MxAccess");
|
|
Cleanup();
|
|
SetConnectionState(ConnectionState.Disconnected, ex.Message);
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates that the client has not been disposed.
|
|
/// </summary>
|
|
private void ValidateNotDisposed()
|
|
{
|
|
if (_disposed)
|
|
{
|
|
throw new ObjectDisposedException(nameof(MxAccessClient));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes the MxAccess COM connection and event handlers.
|
|
/// </summary>
|
|
private void InitializeMxAccessConnection()
|
|
{
|
|
// Create the COM object
|
|
_lmxProxy = new LMXProxyServer();
|
|
|
|
// Wire up event handlers
|
|
_lmxProxy.OnDataChange += OnDataChange;
|
|
_lmxProxy.OnWriteComplete += OnWriteComplete;
|
|
_lmxProxy.OperationComplete += OnOperationComplete;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Registers with the MxAccess server.
|
|
/// </summary>
|
|
private void RegisterWithMxAccess()
|
|
{
|
|
// Register with the server
|
|
if (_lmxProxy == null)
|
|
{
|
|
throw new InvalidOperationException("MxAccess proxy is not initialized");
|
|
}
|
|
|
|
_connectionHandle = _lmxProxy.Register("ZB.MOM.WW.LmxProxy.Host");
|
|
|
|
if (_connectionHandle > 0)
|
|
{
|
|
SetConnectionState(ConnectionState.Connected);
|
|
Logger.Information("Successfully connected to MxAccess with handle {Handle}", _connectionHandle);
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException("Failed to register with MxAccess - invalid handle returned");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal synchronous disconnection logic.
|
|
/// </summary>
|
|
private void DisconnectInternal()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (!IsConnected || _lmxProxy == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Logger.Information("Disconnecting from MxAccess");
|
|
SetConnectionState(ConnectionState.Disconnecting);
|
|
|
|
RemoveAllSubscriptions();
|
|
UnregisterFromMxAccess();
|
|
|
|
Cleanup();
|
|
SetConnectionState(ConnectionState.Disconnected);
|
|
Logger.Information("Successfully disconnected from MxAccess");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Error(ex, "Error during disconnect");
|
|
Cleanup();
|
|
SetConnectionState(ConnectionState.Disconnected, ex.Message);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes all active subscriptions.
|
|
/// </summary>
|
|
private void RemoveAllSubscriptions()
|
|
{
|
|
var subscriptionsToRemove = _subscriptions.Values.ToList();
|
|
var failedRemovals = new List<string>();
|
|
|
|
foreach (SubscriptionInfo? sub in subscriptionsToRemove)
|
|
{
|
|
if (!TryRemoveSubscription(sub))
|
|
{
|
|
failedRemovals.Add(sub.Address);
|
|
}
|
|
}
|
|
|
|
if (failedRemovals.Any())
|
|
{
|
|
Logger.Warning("Failed to cleanly remove {Count} subscriptions: {Addresses}",
|
|
failedRemovals.Count, string.Join(", ", failedRemovals));
|
|
}
|
|
|
|
_subscriptions.Clear();
|
|
_subscriptionsByHandle.Clear();
|
|
// Note: We intentionally keep _storedSubscriptions to recreate them on reconnect
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to remove a single subscription.
|
|
/// </summary>
|
|
private bool TryRemoveSubscription(SubscriptionInfo subscription)
|
|
{
|
|
try
|
|
{
|
|
if (_lmxProxy == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_lmxProxy.UnAdvise(_connectionHandle, subscription.ItemHandle);
|
|
_lmxProxy.RemoveItem(_connectionHandle, subscription.ItemHandle);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Warning(ex, "Error removing subscription for {Address}", subscription.Address);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unregisters from the MxAccess server.
|
|
/// </summary>
|
|
private void UnregisterFromMxAccess()
|
|
{
|
|
if (_connectionHandle > 0 && _lmxProxy != null)
|
|
{
|
|
_lmxProxy.Unregister(_connectionHandle);
|
|
_connectionHandle = 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleans up resources and releases the COM object.
|
|
/// Removes event handlers and releases the proxy COM object if present.
|
|
/// </summary>
|
|
private void Cleanup()
|
|
{
|
|
try
|
|
{
|
|
if (_lmxProxy != null)
|
|
{
|
|
// Remove event handlers
|
|
_lmxProxy.OnDataChange -= OnDataChange;
|
|
_lmxProxy.OnWriteComplete -= OnWriteComplete;
|
|
_lmxProxy.OperationComplete -= OnOperationComplete;
|
|
|
|
// Release COM object
|
|
int refCount = Marshal.ReleaseComObject(_lmxProxy);
|
|
if (refCount > 0)
|
|
{
|
|
Logger.Warning("COM object reference count after release: {RefCount}", refCount);
|
|
// Force final release
|
|
while (refCount > 0)
|
|
{
|
|
refCount = Marshal.ReleaseComObject(_lmxProxy);
|
|
}
|
|
}
|
|
|
|
_lmxProxy = null;
|
|
}
|
|
|
|
_connectionHandle = 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Warning(ex, "Error during cleanup");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Recreates all stored subscriptions after reconnection.
|
|
/// </summary>
|
|
private async Task RecreateStoredSubscriptionsAsync()
|
|
{
|
|
List<StoredSubscription> subscriptionsToRecreate;
|
|
|
|
lock (_lock)
|
|
{
|
|
// Create a copy to avoid holding the lock during async operations
|
|
subscriptionsToRecreate = new List<StoredSubscription>(_storedSubscriptions);
|
|
}
|
|
|
|
if (subscriptionsToRecreate.Count == 0)
|
|
{
|
|
Logger.Debug("No stored subscriptions to recreate");
|
|
return;
|
|
}
|
|
|
|
Logger.Information("Recreating {Count} stored subscription groups after reconnection",
|
|
subscriptionsToRecreate.Count);
|
|
|
|
foreach (StoredSubscription? storedSub in subscriptionsToRecreate)
|
|
{
|
|
try
|
|
{
|
|
// Recreate the subscription without storing it again
|
|
await SubscribeInternalAsync(storedSub.Addresses, storedSub.Callback, false);
|
|
|
|
Logger.Information("Successfully recreated subscription group {GroupId} with {Count} addresses",
|
|
storedSub.GroupId, storedSub.Addresses.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Error(ex, "Failed to recreate subscription group {GroupId}", storedSub.GroupId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|