worker: aaAlarmManagedClient discovery + reference (alarm-helper foundation)
Discovers the surface of aaAlarmManagedClient.dll and stages the worker csproj reference so subsequent PRs can wire native MxAccess alarm subscription. Replaces the speculative "operator decision needed between path 1 and path 2" framing in MxAccessAlarmEventSink with the validated architecture. Key findings from the discovery probe: 1. aaAlarmManagedClient.dll is x86 + .NET Framework (mixed-mode C++/CLI; PE Machine = i386, NativeEntryPoint flag set). The "x64-only" framing in the prior follow-up was wrong — confused by the file path under Wonderware\Historian\x64\. The assembly is bitness- and runtime-compatible with the worker (net48 x86), so it loads in the existing process. No sub-process needed. 2. AlarmClient is the public class. Its model mirrors MxAccess: RegisterConsumer takes a Windows hWnd and the AVEVA alarm service WM_APP-pokes that hwnd when alarms change. The worker's existing STA + WM_APP pump can drive both the data-change COM subscriber and the alarm-client consumer. 3. AlarmAckByGUID(alarmGuid, ackComment, oprName, oprNode, oprDomain, oprFullName) — the native ack carries the operator's full identity atomically with the comment. Closes the v1 operator-comment fidelity gap completely. This PR: - Adds the aaAlarmManagedClient.dll reference to MxGateway.Worker. csproj. Worker still builds clean. - Adds AlarmClientDiscoveryTests as a Skip-gated reflection probe; flip the Skip parameter to dump the public type surface for reference. Captured the dump into MxAccessAlarmEventSink documentation so it doesn't have to be re-run. - Replaces MxAccessAlarmEventSink's "two paths forward" doc with the actual wiring plan against AlarmClient's RegisterConsumer + Subscribe + AlarmAckByGUID surface. Subsequent PRs (gated on STA + WM_APP integration testing on the dev rig): - Wire RegisterConsumer + Subscribe at session-startup; route WM_APP messages through GetStatistics + GetAlarmExtendedRec into EnqueueTransition. - Translate gateway-side AcknowledgeAlarm RPC to a worker command that calls AlarmAckByGUID with the OPC UA operator's identity; replaces the worker-pending diagnostic from PR A.3. - Translate gateway-side QueryActiveAlarms to a worker command that walks GetStatistics's reported handles via GetAlarmExtendedRec. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace MxGateway.Worker.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// One-shot reflection probe — discovers the public surface of
|
||||
/// <c>aaAlarmManagedClient.dll</c> so we can design the alarm-helper
|
||||
/// wiring in the worker. Marked Skip so it doesn't run as part of the
|
||||
/// normal suite; flip the Skip parameter to see the output.
|
||||
/// </summary>
|
||||
public sealed class AlarmClientDiscoveryTests
|
||||
{
|
||||
private readonly ITestOutputHelper output;
|
||||
|
||||
public AlarmClientDiscoveryTests(ITestOutputHelper output)
|
||||
{
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
[Fact(Skip = "Discovery probe — flip Skip=null to dump aaAlarmManagedClient surface")]
|
||||
public void DumpAlarmClientPublicSurface()
|
||||
{
|
||||
Assembly asm = Assembly.LoadFrom(@"C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll");
|
||||
|
||||
output.WriteLine($"Assembly: {asm.FullName}");
|
||||
output.WriteLine("Public types:");
|
||||
|
||||
Type[] types = asm.GetExportedTypes()
|
||||
.OrderBy(t => t.FullName, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
foreach (Type t in types)
|
||||
{
|
||||
output.WriteLine($" {t.FullName} ({(t.IsClass ? "class" : t.IsInterface ? "interface" : t.IsEnum ? "enum" : "other")})");
|
||||
}
|
||||
|
||||
output.WriteLine("");
|
||||
output.WriteLine("Public events / methods on alarm-named types:");
|
||||
foreach (Type t in types.Where(x => x.Name.IndexOf("Alarm", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| x.Name.IndexOf("Subscription", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| x.Name.IndexOf("Event", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
output.WriteLine($" {t.FullName}");
|
||||
foreach (EventInfo e in t.GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
|
||||
{
|
||||
output.WriteLine($" event {e.EventHandlerType?.Name} {e.Name}");
|
||||
}
|
||||
foreach (MethodInfo m in t.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
|
||||
{
|
||||
if (m.IsSpecialName) continue;
|
||||
string parms = string.Join(", ", m.GetParameters().Select(p => $"{p.ParameterType.Name} {p.Name}"));
|
||||
output.WriteLine($" method {m.ReturnType.Name} {m.Name}({parms})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,80 +4,49 @@ using MxGateway.Contracts.Proto;
|
||||
namespace MxGateway.Worker.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// PR A.2 sink intended to register against an MXAccess alarm event source
|
||||
/// and forward each alarm transition into the worker's event queue as an
|
||||
/// <see cref="OnAlarmTransitionEvent"/>. The mapper bridge is fully
|
||||
/// implemented + unit-tested via
|
||||
/// <see cref="MxAccessEventMapper.CreateOnAlarmTransition"/>; the
|
||||
/// <see cref="Attach"/> path is intentionally a no-op pending the
|
||||
/// architectural decision documented below.
|
||||
/// PR A.2 sink for native MxAccess alarm transitions. Bridges the
|
||||
/// <c>aaAlarmManagedClient.AlarmClient</c> consumer to the worker's
|
||||
/// event queue, producing <see cref="OnAlarmTransitionEvent"/> messages
|
||||
/// via <see cref="MxAccessEventMapper.CreateOnAlarmTransition"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// <strong>2026-04-30 dev-rig finding:</strong> the MXAccess COM
|
||||
/// Toolkit installed at <c>C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll</c>
|
||||
/// does <strong>not</strong> expose any alarm event family. Reflection
|
||||
/// enumeration of the assembly (which exports a single COM interop
|
||||
/// module containing <c>ILMXProxyServerEvents</c> and
|
||||
/// <c>ILMXProxyServerEvents2</c>) confirms the only available events
|
||||
/// are <c>OnDataChange</c>, <c>OnWriteComplete</c>,
|
||||
/// <c>OperationComplete</c>, and <c>OnBufferedDataChange</c>. There is
|
||||
/// no <c>OnAlarmTransition</c>, no <c>IAlarmEventSink</c>, and no
|
||||
/// <c>Alarms</c> collection on the COM server.
|
||||
/// <strong>Architecture (pinned 2026-04-30):</strong> the worker hosts
|
||||
/// <c>aaAlarmManagedClient.AlarmClient</c> alongside the existing
|
||||
/// <c>ArchestrA.MxAccess</c> COM consumer. Both are x86 .NET Framework
|
||||
/// 4.8 — the worker's existing runtime — and both use the same Windows
|
||||
/// STA + WM_APP message pump. The MxAccess COM Toolkit at
|
||||
/// <c>C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll</c>
|
||||
/// exposes no alarm events; the alarm provider lives in a separate
|
||||
/// AVEVA service that <c>aaAlarmManagedClient</c> subscribes to.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// AVEVA's separate alarm-subscription managed assemblies
|
||||
/// (<c>aaAlarmManagedClient.dll</c> under
|
||||
/// <c>InTouch\ViewAppFramework\Content\MA\</c>,
|
||||
/// <c>ArchestrAAlarmsAndEvents.SDK.Common.dll</c> under
|
||||
/// <c>Wonderware\Historian\x64\</c>) are present on this box but are
|
||||
/// <strong>x64-only</strong>; they cannot load into the worker process,
|
||||
/// which is x86 because of the MXAccess COM bitness constraint that
|
||||
/// the <c>mxaccessgw</c> architecture exists to isolate. Loading them
|
||||
/// in a separate x64 helper process would add meaningful operational
|
||||
/// complexity (a third process tier alongside worker + gateway) and is
|
||||
/// not in the current architecture.
|
||||
/// <strong>Discovered API surface</strong> (see
|
||||
/// <c>AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface</c> in
|
||||
/// <c>MxGateway.Worker.Tests</c> — Skip-gated reflection probe):
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <strong>Two paths forward — operator decision needed before the
|
||||
/// sink can be wired:</strong>
|
||||
/// </para>
|
||||
/// <list type="number">
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <strong>Stay on the value-driven sub-attribute path</strong>
|
||||
/// (current production behaviour). The lmxopcua server's
|
||||
/// <c>AlarmConditionService</c> already synthesizes Part 9
|
||||
/// transitions from the four MXAccess sub-attributes
|
||||
/// (<c>InAlarm</c>, <c>Acked</c>, <c>Priority</c>,
|
||||
/// <c>Description</c>) via the data-change subscription.
|
||||
/// Operator-comment fidelity is the only regression vs. v1; if
|
||||
/// acceptable, this row stays the production path and the
|
||||
/// <see cref="MxEventFamily.OnAlarmTransition"/> family stays
|
||||
/// reserved-but-dormant on the wire.
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <description>
|
||||
/// <strong>Add an x64 alarm-helper sub-process</strong> alongside
|
||||
/// the worker that loads <c>aaAlarmManagedClient</c>,
|
||||
/// subscribes to alarms, and forwards transitions to the worker
|
||||
/// over a small named-pipe IPC. Then this sink's
|
||||
/// <see cref="Attach"/> connects to that helper instead of to
|
||||
/// the COM server, and routes each transition through
|
||||
/// <see cref="EnqueueTransition"/>. Adds operational complexity
|
||||
/// but recovers full v1 fidelity (operator user, comment,
|
||||
/// original raise time, category).
|
||||
/// </description>
|
||||
/// </item>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><c>RegisterConsumer(hWnd, productName, applicationName, version, retainHidden)</c> — registers a Windows-message-pump consumer; the AVEVA alarm service WM_APP-pokes the hWnd when alarms change.</description></item>
|
||||
/// <item><description><c>Subscribe(provider, fromPri, toPri, queryType, sortFlags, filterMask, filterSpec)</c> — subscribes to a Galaxy alarm provider with priority + filter scoping.</description></item>
|
||||
/// <item><description><c>GetStatistics(out percentQuery, totalAlarms, activeAlarms, …, out int[] changeCodes, out int[] changePos, out int[] hAlarm)</c> — called on each WM_APP poke; enumerates which alarms changed.</description></item>
|
||||
/// <item><description><c>GetAlarmExtendedRec(index, out AlarmRecord)</c> — pulls the full alarm record (operator, comment, original raise, category, severity).</description></item>
|
||||
/// <item><description><c>AlarmAckByGUID(alarmGuid, ackComment, oprName, oprNode, oprDomain, oprFullName)</c> — full-fidelity native Acknowledge: comment + four operator-identity fields are atomic with the ack transition.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Until that decision is made, this sink's <see cref="Attach"/> is a
|
||||
/// no-op. The worker continues to function for data subscriptions, and
|
||||
/// the gateway's <see cref="MxEventFamily.OnAlarmTransition"/> family
|
||||
/// is reserved on the wire but never emitted. lmxopcua-side
|
||||
/// <c>AlarmConditionService</c> keeps the sub-attribute synthesis
|
||||
/// active and continues to surface alarms to OPC UA Part 9 clients.
|
||||
/// <strong>Wiring plan (subsequent PRs):</strong>
|
||||
/// </para>
|
||||
/// <list type="number">
|
||||
/// <item><description>Worker session-startup wires <c>AlarmClient.RegisterConsumer</c> against the worker's existing STA hWnd; <c>Subscribe</c> with the Galaxy provider name + a permissive priority/filter range.</description></item>
|
||||
/// <item><description>The STA's WM_APP handler routes alarm-changed messages into <see cref="EnqueueTransition"/>; the message ID is established at runtime via the consumer's reported handler (verify on dev rig).</description></item>
|
||||
/// <item><description>Gateway-side <c>AcknowledgeAlarm</c> RPC translates to a worker command that calls <c>AlarmClient.AlarmAckByGUID</c> with the OPC UA operator's resolved identity — replaces the worker-pending diagnostic from PR A.3.</description></item>
|
||||
/// </list>
|
||||
/// <para>
|
||||
/// Until those PRs land, <see cref="Attach"/> is a no-op. The worker
|
||||
/// continues to function for data subscriptions, and the gateway's
|
||||
/// <see cref="MxEventFamily.OnAlarmTransition"/> family is reserved on
|
||||
/// the wire but never emitted. lmxopcua-side <c>AlarmConditionService</c>
|
||||
/// keeps the sub-attribute synthesis active and continues to surface
|
||||
/// alarms to OPC UA Part 9 clients in the meantime.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class MxAccessAlarmEventSink : IMxAccessEventSink
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
<Private>false</Private>
|
||||
<SpecificVersion>false</SpecificVersion>
|
||||
</Reference>
|
||||
<Reference Include="aaAlarmManagedClient">
|
||||
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll</HintPath>
|
||||
<Private>false</Private>
|
||||
<SpecificVersion>false</SpecificVersion>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user