feat(lmxproxy): phase 7 — integration tests, deployment to windev, v1 cutover
- Replaced STA dispatch thread with Task.Run pattern for COM interop - Fixed TypedValue oneof tracking with property-level _setCase field - Added x-api-key DelegatingHandler for gRPC metadata authentication - Fixed CheckApiKey RPC to validate request body key (not header) - Integration tests: 15/17 pass (reads, subscribes, API keys, connections) - 2 write tests pending (OnWriteComplete callback timing issue) - v2 service deployed on windev port 50100 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -11,7 +12,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
public sealed partial class MxAccessClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Connects to MxAccess on the STA thread.
|
||||
/// Connects to MxAccess via Task.Run (thread pool).
|
||||
/// </summary>
|
||||
public async Task ConnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
@@ -22,18 +23,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
|
||||
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");
|
||||
});
|
||||
await Task.Run(() => ConnectInternal(), ct);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
@@ -56,7 +46,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from MxAccess on the STA thread.
|
||||
/// Disconnects from MxAccess via Task.Run (thread pool).
|
||||
/// </summary>
|
||||
public async Task DisconnectAsync(CancellationToken ct = default)
|
||||
{
|
||||
@@ -66,32 +56,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
await Task.Run(() => DisconnectInternal());
|
||||
|
||||
SetState(ConnectionState.Disconnected);
|
||||
Log.Information("Disconnected from MxAccess");
|
||||
@@ -123,6 +88,88 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
_reconnectCts?.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>Gets the UTC time when the connection was established.</summary>
|
||||
public DateTime ConnectedSince
|
||||
{
|
||||
get { lock (_lock) { return _connectedSince; } }
|
||||
}
|
||||
|
||||
// ── Internal synchronous methods ──────────
|
||||
|
||||
private void ConnectInternal()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// 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");
|
||||
|
||||
if (_connectionHandle <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to register with MxAccess - invalid handle returned");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DisconnectInternal()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_lmxProxy == null || _connectionHandle <= 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Unadvise all active subscriptions before unregistering
|
||||
foreach (var kvp in new Dictionary<string, int>(_addressToHandle))
|
||||
{
|
||||
try
|
||||
{
|
||||
_lmxProxy.UnAdvise(_connectionHandle, kvp.Value);
|
||||
_lmxProxy.RemoveItem(_connectionHandle, kvp.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error removing subscription for {Address} during disconnect", kvp.Key);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove event handlers
|
||||
_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
|
||||
try
|
||||
{
|
||||
Marshal.ReleaseComObject(_lmxProxy);
|
||||
}
|
||||
catch { }
|
||||
|
||||
_lmxProxy = null;
|
||||
_connectionHandle = 0;
|
||||
|
||||
// Clear handle tracking (but keep _storedSubscriptions for reconnect)
|
||||
_handleToAddress.Clear();
|
||||
_addressToHandle.Clear();
|
||||
_pendingWrites.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-reconnect monitor loop. Checks connection every monitorInterval.
|
||||
/// On disconnect, attempts reconnect. On failure, retries at next interval.
|
||||
@@ -166,22 +213,28 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up COM objects on the STA thread after a failed connection.
|
||||
/// Cleans up COM objects via Task.Run after a failed connection.
|
||||
/// </summary>
|
||||
private async Task CleanupComObjectsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _staThread.DispatchAsync(() =>
|
||||
await Task.Run(() =>
|
||||
{
|
||||
if (_lmxProxy != null)
|
||||
lock (_lock)
|
||||
{
|
||||
try { _lmxProxy.OnDataChange -= OnDataChange; } catch { }
|
||||
try { _lmxProxy.OnWriteComplete -= OnWriteComplete; } catch { }
|
||||
try { Marshal.ReleaseComObject(_lmxProxy); } catch { }
|
||||
_lmxProxy = null;
|
||||
if (_lmxProxy != null)
|
||||
{
|
||||
try { _lmxProxy.OnDataChange -= OnDataChange; } catch { }
|
||||
try { _lmxProxy.OnWriteComplete -= OnWriteComplete; } catch { }
|
||||
try { Marshal.ReleaseComObject(_lmxProxy); } catch { }
|
||||
_lmxProxy = null;
|
||||
}
|
||||
_connectionHandle = 0;
|
||||
_handleToAddress.Clear();
|
||||
_addressToHandle.Clear();
|
||||
_pendingWrites.Clear();
|
||||
}
|
||||
_connectionHandle = 0;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -189,11 +242,5 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
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