From a14098468bb46961b9866aa2a3e5c526d59136e7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 30 Apr 2026 22:17:38 -0400 Subject: [PATCH] worker: aaAlarmManagedClient discovery + reference (alarm-helper foundation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../AlarmClientDiscoveryTests.cs | 59 ++++++++++ .../MxAccess/MxAccessAlarmEventSink.cs | 101 ++++++------------ src/MxGateway.Worker/MxGateway.Worker.csproj | 5 + 3 files changed, 99 insertions(+), 66 deletions(-) create mode 100644 src/MxGateway.Worker.Tests/AlarmClientDiscoveryTests.cs diff --git a/src/MxGateway.Worker.Tests/AlarmClientDiscoveryTests.cs b/src/MxGateway.Worker.Tests/AlarmClientDiscoveryTests.cs new file mode 100644 index 0000000..6f4cc2d --- /dev/null +++ b/src/MxGateway.Worker.Tests/AlarmClientDiscoveryTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using System.Reflection; +using Xunit; +using Xunit.Abstractions; + +namespace MxGateway.Worker.Tests; + +/// +/// One-shot reflection probe — discovers the public surface of +/// aaAlarmManagedClient.dll 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. +/// +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})"); + } + } + } +} diff --git a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs index ebf0f27..5b1f430 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs @@ -4,80 +4,49 @@ using MxGateway.Contracts.Proto; namespace MxGateway.Worker.MxAccess; /// -/// 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 -/// . The mapper bridge is fully -/// implemented + unit-tested via -/// ; the -/// path is intentionally a no-op pending the -/// architectural decision documented below. +/// PR A.2 sink for native MxAccess alarm transitions. Bridges the +/// aaAlarmManagedClient.AlarmClient consumer to the worker's +/// event queue, producing messages +/// via . /// /// /// -/// 2026-04-30 dev-rig finding: the MXAccess COM -/// Toolkit installed at C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll -/// does not expose any alarm event family. Reflection -/// enumeration of the assembly (which exports a single COM interop -/// module containing ILMXProxyServerEvents and -/// ILMXProxyServerEvents2) confirms the only available events -/// are OnDataChange, OnWriteComplete, -/// OperationComplete, and OnBufferedDataChange. There is -/// no OnAlarmTransition, no IAlarmEventSink, and no -/// Alarms collection on the COM server. +/// Architecture (pinned 2026-04-30): the worker hosts +/// aaAlarmManagedClient.AlarmClient alongside the existing +/// ArchestrA.MxAccess 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:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll +/// exposes no alarm events; the alarm provider lives in a separate +/// AVEVA service that aaAlarmManagedClient subscribes to. /// /// -/// AVEVA's separate alarm-subscription managed assemblies -/// (aaAlarmManagedClient.dll under -/// InTouch\ViewAppFramework\Content\MA\, -/// ArchestrAAlarmsAndEvents.SDK.Common.dll under -/// Wonderware\Historian\x64\) are present on this box but are -/// x64-only; they cannot load into the worker process, -/// which is x86 because of the MXAccess COM bitness constraint that -/// the mxaccessgw 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. +/// Discovered API surface (see +/// AlarmClientDiscoveryTests.DumpAlarmClientPublicSurface in +/// MxGateway.Worker.Tests — Skip-gated reflection probe): /// -/// -/// Two paths forward — operator decision needed before the -/// sink can be wired: -/// -/// -/// -/// -/// Stay on the value-driven sub-attribute path -/// (current production behaviour). The lmxopcua server's -/// AlarmConditionService already synthesizes Part 9 -/// transitions from the four MXAccess sub-attributes -/// (InAlarm, Acked, Priority, -/// Description) via the data-change subscription. -/// Operator-comment fidelity is the only regression vs. v1; if -/// acceptable, this row stays the production path and the -/// family stays -/// reserved-but-dormant on the wire. -/// -/// -/// -/// -/// Add an x64 alarm-helper sub-process alongside -/// the worker that loads aaAlarmManagedClient, -/// subscribes to alarms, and forwards transitions to the worker -/// over a small named-pipe IPC. Then this sink's -/// connects to that helper instead of to -/// the COM server, and routes each transition through -/// . Adds operational complexity -/// but recovers full v1 fidelity (operator user, comment, -/// original raise time, category). -/// -/// +/// +/// RegisterConsumer(hWnd, productName, applicationName, version, retainHidden) — registers a Windows-message-pump consumer; the AVEVA alarm service WM_APP-pokes the hWnd when alarms change. +/// Subscribe(provider, fromPri, toPri, queryType, sortFlags, filterMask, filterSpec) — subscribes to a Galaxy alarm provider with priority + filter scoping. +/// GetStatistics(out percentQuery, totalAlarms, activeAlarms, …, out int[] changeCodes, out int[] changePos, out int[] hAlarm) — called on each WM_APP poke; enumerates which alarms changed. +/// GetAlarmExtendedRec(index, out AlarmRecord) — pulls the full alarm record (operator, comment, original raise, category, severity). +/// AlarmAckByGUID(alarmGuid, ackComment, oprName, oprNode, oprDomain, oprFullName) — full-fidelity native Acknowledge: comment + four operator-identity fields are atomic with the ack transition. /// /// -/// Until that decision is made, this sink's is a -/// no-op. The worker continues to function for data subscriptions, and -/// the gateway's family -/// is reserved on the wire but never emitted. lmxopcua-side -/// AlarmConditionService keeps the sub-attribute synthesis -/// active and continues to surface alarms to OPC UA Part 9 clients. +/// Wiring plan (subsequent PRs): +/// +/// +/// Worker session-startup wires AlarmClient.RegisterConsumer against the worker's existing STA hWnd; Subscribe with the Galaxy provider name + a permissive priority/filter range. +/// The STA's WM_APP handler routes alarm-changed messages into ; the message ID is established at runtime via the consumer's reported handler (verify on dev rig). +/// Gateway-side AcknowledgeAlarm RPC translates to a worker command that calls AlarmClient.AlarmAckByGUID with the OPC UA operator's resolved identity — replaces the worker-pending diagnostic from PR A.3. +/// +/// +/// Until those PRs land, is a no-op. The worker +/// continues to function for data subscriptions, and the gateway's +/// family is reserved on +/// the wire but never emitted. lmxopcua-side AlarmConditionService +/// keeps the sub-attribute synthesis active and continues to surface +/// alarms to OPC UA Part 9 clients in the meantime. /// /// public sealed class MxAccessAlarmEventSink : IMxAccessEventSink diff --git a/src/MxGateway.Worker/MxGateway.Worker.csproj b/src/MxGateway.Worker/MxGateway.Worker.csproj index a23d32a..dbde52e 100644 --- a/src/MxGateway.Worker/MxGateway.Worker.csproj +++ b/src/MxGateway.Worker/MxGateway.Worker.csproj @@ -24,6 +24,11 @@ false false + + C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll + false + false +