using System; using System.IO; using System.Reflection; using WNWRAPCONSUMERLib; namespace ZB.MOM.WW.MxGateway.Worker.MxAccess; /// /// Acknowledge path over AVEVA's modern managed alarm client, /// aaAlarmManagedClient.AlarmClient (the C++/CLI assembly the AVEVA /// Alarm Control itself uses: AlarmClient → CwwAlarmConsumer → /// alarmlst.dll → alarmmgr.exe). Preferred over a hand-rolled native /// WNAL consumer because AlarmAckByGUID performs the /// register → match-by-GUID → submit sequence internally — the record /// layout and selection semantics that are provider-defined (and therefore /// NOT safely recoverable by static RE of alarmlst.dll) stay inside AVEVA's /// own code. See for why the native /// acquisition path is deliberately unfinished. /// /// The AlarmClient members are famorasm (protected-internal) /// and mxaccessgw is not an InternalsVisibleTo friend, so everything /// here is invoked by reflection. In full-trust .NET Framework 4.8 that is /// permitted. Method shapes are confirmed from the assembly metadata: /// .ctor() (internal) /// int InitializeConsumer(string appName) /// int RegisterConsumer(int hWnd, string product, string app, string version, bool) /// int Subscribe(string expr, short fromPri, short toPri, /// localQueryType, localSortFlags, /// localAlarmFilterState mask, localAlarmFilterState spec) /// int AlarmAckByGUID(System.Guid guid, string comment, string oper, /// string node, string domain, string fullName) /// int DeregisterConsumer() / int UninitializeConsumer() / Dispose() /// /// THREADING: the underlying consumer is apartment-affine (same as the /// wnwrap COM path). Construct and call this only on the worker STA /// (marshal via StaRuntime.InvokeAsync). /// /// x86 ONLY — aaAlarmManagedClient.dll is a 32-bit mixed-mode PE. /// /// OPEN (confirm at runtime, flagged inline): the trailing bool of /// RegisterConsumer (passed false here); whether /// Subscribe is actually required before an ack by GUID; and the /// native status returned on a real secured ack. /// public sealed class AaManagedAlarmAckConsumer : IManagedAlarmAckClient { /// Default install location of the managed alarm client (eac.dll folder). public const string DefaultAssemblyPath = @"C:\Program Files (x86)\ArchestrA\Framework\Bin\Invensys_Systems__Inc__eac.dll\aaAlarmManagedClient.dll"; /// /// Env var that opts the worker into the managed ack path. Unset/anything /// other than "1" keeps the existing wnwrap ack behavior. /// public const string EnableEnvVar = "MXGATEWAY_USE_MANAGED_ALARM_ACK"; private const string AckApplicationName = "ZB.MOM.WW.MxGateway.ack"; /// /// Production factory for the seam: /// returns a managed ack client only when == "1", /// otherwise (ack stays on the wnwrap path). Never /// throws — a construction failure (missing DLL, etc.) degrades to the /// wnwrap path rather than breaking the alarm subscription. /// public static IManagedAlarmAckClient? EnvGatedFactory() { if (!string.Equals(Environment.GetEnvironmentVariable(EnableEnvVar), "1", StringComparison.Ordinal)) return null; try { return new AaManagedAlarmAckConsumer(AckApplicationName); } catch { // Opt-in enhancement only: if the managed client can't load, fall // back to the wnwrap ack path rather than failing alarm reads. return null; } } private const string TypeName = "aaAlarmManagedClient.AlarmClient"; private const BindingFlags Inst = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private readonly object syncRoot = new(); private readonly object client; private readonly MethodInfo initializeConsumer; private readonly MethodInfo registerConsumer; private readonly MethodInfo subscribe; private readonly MethodInfo alarmAckByGuid; private readonly MethodInfo? deregisterConsumer; private readonly MethodInfo? uninitializeConsumer; private bool subscribed; private bool disposed; /// Consumer application name (e.g. "MxGateway.ack"). /// Override for . public AaManagedAlarmAckConsumer(string applicationName, string? assemblyPath = null) { if (string.IsNullOrWhiteSpace(applicationName)) throw new ArgumentException("Application name is required.", nameof(applicationName)); string path = assemblyPath ?? DefaultAssemblyPath; if (!File.Exists(path)) throw new FileNotFoundException($"aaAlarmManagedClient.dll not found at '{path}'.", path); Assembly asm = Assembly.LoadFrom(path); Type type = asm.GetType(TypeName, throwOnError: true) ?? throw new InvalidOperationException($"Type '{TypeName}' not found in {path}."); initializeConsumer = Resolve(type, "InitializeConsumer"); registerConsumer = Resolve(type, "RegisterConsumer"); subscribe = Resolve(type, "Subscribe"); alarmAckByGuid = Resolve(type, "AlarmAckByGUID"); deregisterConsumer = type.GetMethod("DeregisterConsumer", Inst, null, Type.EmptyTypes, null); uninitializeConsumer = type.GetMethod("UninitializeConsumer", Inst, null, Type.EmptyTypes, null); client = Activator.CreateInstance(type, nonPublic: true) ?? throw new InvalidOperationException($"Could not construct '{TypeName}'."); ApplicationName = applicationName; } /// The consumer application name this instance registered under. public string ApplicationName { get; } private static MethodInfo Resolve(Type type, string name) => type.GetMethod(name, Inst) ?? throw new MissingMethodException(TypeName, name); /// /// Run the Initialize → Register → Subscribe lifecycle once, mirroring the /// proven wnwrap argument values. is the /// alarm query root, e.g. \\NODE\Galaxy!Area. Throws if any native /// status is non-zero. /// public void Subscribe(string subscription) { if (subscription is null) throw new ArgumentNullException(nameof(subscription)); lock (syncRoot) { if (disposed) throw new ObjectDisposedException(nameof(AaManagedAlarmAckConsumer)); if (subscribed) throw new InvalidOperationException("Subscribe was already called."); int init = (int)initializeConsumer.Invoke(client, new object[] { ApplicationName })!; if (init != 0) throw new InvalidOperationException($"InitializeConsumer returned {init}."); // RegisterConsumer(hWnd, product, app, version, bool). hWnd=0 => pull model. // The trailing bool is unconfirmed; false = client/non-modal is the safe default. int reg = (int)registerConsumer.Invoke(client, new object[] { 0, "ZB.MOM.WW.MxGateway", ApplicationName, "1.0", false, })!; if (reg != 0) throw new InvalidOperationException($"RegisterConsumer returned {reg}."); int sub = (int)subscribe.Invoke(client, BuildSubscribeArgs(subscription))!; if (sub != 0) throw new InvalidOperationException($"Subscribe('{subscription}') returned {sub}."); subscribed = true; } } /// /// Acknowledge a single alarm by GUID with full operator identity. Returns /// AVEVA's native status (0 = success). A zero return means accepted, not /// necessarily applied — verify the alarm's acked state / Historian row /// (see docs/AlarmProbeFindings.md). /// public int AcknowledgeByGuid( Guid alarmGuid, string comment, string operatorName, string operatorNode, string operatorDomain, string operatorFullName) { lock (syncRoot) { if (disposed) throw new ObjectDisposedException(nameof(AaManagedAlarmAckConsumer)); if (!subscribed) throw new InvalidOperationException("Subscribe must be called before acknowledging."); return (int)alarmAckByGuid.Invoke(client, new object[] { alarmGuid, comment ?? string.Empty, operatorName ?? string.Empty, operatorNode ?? string.Empty, operatorDomain ?? string.Empty, operatorFullName ?? string.Empty, })!; } } /// /// Build the 7 Subscribe args, boxing the proven wnwrap enum integers into /// aaAlarmManagedClient's own enum parameter types (localQueryType / /// localSortFlags / localAlarmFilterState) via . /// private object[] BuildSubscribeArgs(string subscription) { ParameterInfo[] p = subscribe.GetParameters(); if (p.Length != 7) throw new InvalidOperationException($"Unexpected AlarmClient.Subscribe arity {p.Length}; expected 7."); return new object[] { subscription, (short)1, // wFromPri (short)999, // wToPri Enum.ToObject(p[3].ParameterType, (int)eQueryType.qtSummary), Enum.ToObject(p[4].ParameterType, (int)eSortFlags.sfReturnNewestFirst), Enum.ToObject(p[5].ParameterType, (int)eAlarmFilterState.asAlarmActiveNow), Enum.ToObject(p[6].ParameterType, (int)eAlarmFilterState.asAlarmActiveNow), }; } /// public void Dispose() { lock (syncRoot) { if (disposed) return; disposed = true; try { if (subscribed) deregisterConsumer?.Invoke(client, Array.Empty()); } catch { /* best effort */ } try { uninitializeConsumer?.Invoke(client, Array.Empty()); } catch { /* best effort */ } (client as IDisposable)?.Dispose(); } } }