alarms-over-gateway: full pipeline (wnwrap consumer + dispatcher + IPC + auto-subscribe + ack-by-name + live smoke) #118

Merged
dohertj2 merged 16 commits from docs/alarm-client-wm-app-finding into main 2026-05-01 12:31:29 -04:00
2 changed files with 76 additions and 0 deletions
Showing only changes of commit f490ae2593 - Show all commits
+30
View File
@@ -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
@@ -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<System.Runtime.InteropServices.GuidAttribute>().FirstOrDefault();
Log($"Class GUID: {classGuid?.Value ?? "(none)"}");
foreach (var iface in ct.GetInterfaces())
{
var ig = iface.GetCustomAttributes(typeof(System.Runtime.InteropServices.GuidAttribute), true)
.Cast<System.Runtime.InteropServices.GuidAttribute>().FirstOrDefault();
var ity = iface.GetCustomAttributes(typeof(System.Runtime.InteropServices.InterfaceTypeAttribute), true)
.Cast<System.Runtime.InteropServices.InterfaceTypeAttribute>().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