Merge pull request 'worker: document MXAccess Toolkit alarm-API gap (A.2 follow-up)' (#114) from track-a2-followup-com-api-finding into main

This commit was merged in pull request #114.
This commit is contained in:
2026-04-30 21:30:58 -04:00
@@ -4,33 +4,80 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// PR A.2 sink that registers against the MXAccess Toolkit's alarm event
/// source and forwards each alarm transition into the worker's event queue
/// as an <see cref="OnAlarmTransitionEvent"/>. Sister to
/// <see cref="MxAccessBaseEventSink"/>, but for the alarm event family
/// instead of data-change.
/// 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.
/// </summary>
/// <remarks>
/// <para>
/// The MXAccess Toolkit's alarm subscription API differs across major
/// AVEVA versions. The exact COM interface (today expected to be one of
/// <c>IAlarmEventSink</c>, <c>IAlarmEventSubscription</c>, or a method
/// on the existing <c>LMXProxyServerClass</c> like
/// <c>OnAlarmEvent</c>) is pinned during dev-rig validation against the
/// worker host's installed Toolkit version. Until that pin lands, the
/// <see cref="Attach"/> path logs a clear "alarm subscription not yet
/// wired" warning and registers no COM hook — the worker continues to
/// function for data subscriptions, and the gateway's
/// <see cref="MxEventFamily.OnAlarmTransition"/> path simply receives no
/// events.
/// <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.
/// </para>
/// <para>
/// <see cref="MxAccessEventMapper.CreateOnAlarmTransition"/> is fully
/// implemented and unit-testable — it builds the proto event from
/// decoded fields, so once the COM subscription resolves to a method
/// that produces those fields, the only edit needed here is to wire
/// the COM event-handler delegate to call
/// <see cref="EnqueueTransition"/>.
/// 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.
/// </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>
/// <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.
/// </para>
/// </remarks>
public sealed class MxAccessAlarmEventSink : IMxAccessEventSink