diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index 0a05b7f..d9fe774 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -392,6 +392,36 @@ disruptive. Once the interop is custom, `AlarmClient.Subscribe` + `GetHighPriAlarm` + `GetAlarmExtendedRec` form a viable polling-style alarm consumer. +**REVISED 2026-05-01 — option 1 not directly applicable.** +Reflection on `aaAlarmManagedClient.AlarmClient` shows it +implements only `IDisposable` (no `[ComImport]` interface, no +class GUID). It has a single field `CwwAlarmConsumer* +m_almUnmanaged` — meaning `AlarmClient` is a **C++/CLI managed +wrapper around a native C++ class**, NOT a COM-interop class. +The DateTime conversion happens inside the AVEVA wrapper's IL, +not at a .NET-to-COM marshaling boundary. There is no separate +COM interface IID we can QI to. + +Revised approach options: + +A. **Switch to `wnwrapConsumer.dll`** — a separate standalone + COM library AVEVA ships at + `C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll` + exposing `WNWRAPCONSUMERLib.wwAlarmConsumerClass` with + `SetXmlAlarmQuery` / `GetXmlCurrentAlarms`. XML-string output + bypasses FILETIME marshaling entirely. +B. **Patch `aaAlarmManagedClient.dll` IL** — wrap the unsafe + `DateTime.FromFileTime` calls with a safe variant. Direct + fix but modifies a vendor binary. +C. **Reflect into `m_almUnmanaged` and call native vtable** — + get the IntPtr, walk the MSVC C++ vtable, call + `__thiscall` methods via `Marshal.GetDelegateForFunctionPointer`. + Doable but requires reverse-engineering the C++ class layout. + +Option A is the best fit: real COM-based, self-contained in +our code, conventional production-grade approach (the WIN-911 +consumer pattern referenced in AVEVA support forums uses it). + The polling-vs-WM_APP-callback question from earlier is now moot: `GetStatistics`'s `positions[]/handles[]` arrays remained empty even when alarms were demonstrably present. The active diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs index 6e156c4..ad716de 100644 --- a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -250,6 +250,52 @@ public sealed class AlarmClientWmProbeTests : IDisposable { client = new AlarmClient(); + // One-time interop introspection: dump AlarmClient's class GUID + // (CoClass IID) and every interface it implements with their + // GUID + InterfaceType. The IID we need to redeclare with safe + // blittable types is the one whose vtable carries + // GetHighPriAlarm. + try + { + Type ct = client.GetType(); + Log($"=== AlarmClient interop introspection ==="); + Log($"Class FullName: {ct.FullName}"); + var classGuid = ct.GetCustomAttributes(typeof(System.Runtime.InteropServices.GuidAttribute), true) + .Cast().FirstOrDefault(); + Log($"Class GUID: {classGuid?.Value ?? "(none)"}"); + foreach (var iface in ct.GetInterfaces()) + { + var ig = iface.GetCustomAttributes(typeof(System.Runtime.InteropServices.GuidAttribute), true) + .Cast().FirstOrDefault(); + var ity = iface.GetCustomAttributes(typeof(System.Runtime.InteropServices.InterfaceTypeAttribute), true) + .Cast().FirstOrDefault(); + int methodCount = iface.GetMethods().Length; + Log($" iface {iface.FullName} | GUID={ig?.Value ?? "(none)"} | type={ity?.Value.ToString() ?? "(none)"} | methods={methodCount}"); + } + // Dump fields (private/internal) — the COM object reference + // is likely on a private field. + Log($"--- AlarmClient instance fields ---"); + foreach (var f in ct.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + Log($" field {f.FieldType.FullName} {f.Name} (public={f.IsPublic})"); + } + // Dump base class chain. + Log($"--- base class chain ---"); + Type? baseT = ct.BaseType; + int depth = 0; + while (baseT != null && depth < 5) + { + Log($" base[{depth}]: {baseT.FullName}"); + baseT = baseT.BaseType; + depth++; + } + Log($"=== end introspection ==="); + } + catch (Exception ex) + { + Log($"Interop introspection threw: {ex.GetType().Name}: {ex.Message}"); + } + // Try InitializeConsumer first — separate from RegisterConsumer // per the discovered API surface; previous probe runs skipped // it. Some AVEVA managed-client patterns require Initialize