feat(lmxproxy): phase 1 — v2 protocol types and domain model
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user