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,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using ArchestrA.MxAccess;
|
||||
using Serilog;
|
||||
using ZB.MOM.WW.LmxProxy.Host.Domain;
|
||||
@@ -16,7 +17,6 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
|
||||
/// <summary>
|
||||
/// COM event handler for MxAccess OnDataChange events.
|
||||
/// Called on the STA thread when a subscribed tag value changes.
|
||||
/// Signature matches the ArchestrA.MxAccess ILMXProxyServerEvents interface.
|
||||
/// </summary>
|
||||
private void OnDataChange(
|
||||
@@ -33,14 +33,32 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
var timestamp = ConvertTimestamp(pftItemTimeStamp);
|
||||
var vtq = new Vtq(pvItemValue, timestamp, quality);
|
||||
|
||||
// We don't have the address from the COM callback — the reference code
|
||||
// looks it up from _subscriptionsByHandle. For the v2 design, the
|
||||
// SubscriptionManager's global handler receives (address, vtq) via
|
||||
// OnTagValueChanged. The actual address resolution will be implemented
|
||||
// when the full subscription tracking is wired up on windev.
|
||||
// Resolve address from handle map
|
||||
string address;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_handleToAddress.TryGetValue(phItemHandle, out address))
|
||||
{
|
||||
Log.Debug("OnDataChange for unknown handle {Handle}, ignoring", phItemHandle);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Route to the SubscriptionManager's global handler
|
||||
OnTagValueChanged?.Invoke(phItemHandle.ToString(), vtq);
|
||||
// Invoke the stored subscription callback
|
||||
Action<string, Vtq> callback;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_storedSubscriptions.TryGetValue(address, out callback))
|
||||
{
|
||||
Log.Debug("OnDataChange for {Address} but no callback registered", address);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
callback.Invoke(address, vtq);
|
||||
|
||||
// Also route to the SubscriptionManager's global handler
|
||||
OnTagValueChanged?.Invoke(address, vtq);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -57,11 +75,60 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
int phItemHandle,
|
||||
ref MXSTATUS_PROXY[] ItemStatus)
|
||||
{
|
||||
// Write completion is currently fire-and-forget.
|
||||
// Log for diagnostics.
|
||||
try
|
||||
{
|
||||
Log.Debug("WriteCompleted: handle {Handle}", phItemHandle);
|
||||
TaskCompletionSource<bool> tcs;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_pendingWrites.TryGetValue(phItemHandle, out tcs))
|
||||
{
|
||||
Log.Debug("WriteComplete for unknown handle {Handle}", phItemHandle);
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingWrites.Remove(phItemHandle);
|
||||
}
|
||||
|
||||
if (ItemStatus != null && ItemStatus.Length > 0)
|
||||
{
|
||||
var status = ItemStatus[0];
|
||||
if (status.success == 0)
|
||||
{
|
||||
string errorMsg = GetWriteErrorMessage(status.detail);
|
||||
Log.Warning("Write failed for handle {Handle}: {Error} (Category={Category}, Detail={Detail})",
|
||||
phItemHandle, errorMsg, status.category, status.detail);
|
||||
tcs.TrySetException(new InvalidOperationException(
|
||||
string.Format("Write failed: {0}", errorMsg)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug("Write completed successfully for handle {Handle}", phItemHandle);
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// No status means success
|
||||
Log.Debug("Write completed for handle {Handle} with no status", phItemHandle);
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
|
||||
// Clean up the item after write completes
|
||||
lock (_lock)
|
||||
{
|
||||
if (_lmxProxy != null && phItemHandle > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
_lmxProxy.UnAdvise(_connectionHandle, phItemHandle);
|
||||
_lmxProxy.RemoveItem(_connectionHandle, phItemHandle);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Debug(ex, "Error cleaning up after write for handle {Handle}", phItemHandle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -69,6 +136,20 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a human-readable error message for a write error code.
|
||||
/// </summary>
|
||||
private static string GetWriteErrorMessage(int errorCode)
|
||||
{
|
||||
switch (errorCode)
|
||||
{
|
||||
case 1008: return "User lacks proper security for write operation";
|
||||
case 1012: return "Secured write required";
|
||||
case 1013: return "Verified write required";
|
||||
default: return string.Format("Unknown error code: {0}", errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a timestamp object to DateTime in UTC.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user