alarms-over-gateway: full pipeline (wnwrap consumer + dispatcher + IPC + auto-subscribe + ack-by-name + live smoke) #118
@@ -87,35 +87,97 @@ a result — `RaiseAlarmRecordReceived` is `internal` for tests and
|
|||||||
never gets invoked at runtime. Until A.2 lands a WM_APP pump,
|
never gets invoked at runtime. Until A.2 lands a WM_APP pump,
|
||||||
`MX_EVENT_FAMILY_ON_ALARM_TRANSITION` cannot carry events.
|
`MX_EVENT_FAMILY_ON_ALARM_TRANSITION` cannot carry events.
|
||||||
|
|
||||||
## Implications for A.2
|
## Live runtime probe — 2026-05-01
|
||||||
|
|
||||||
The original plan banner was right: A.2 needs a hidden message-only
|
`MxGateway.Worker.Tests.AlarmClientWmProbeTests.ProbeAlarmClientWmMessages`
|
||||||
window inside the worker's STA whose hWnd is passed to
|
is a Skip-gated runtime probe that creates a real message-only
|
||||||
`RegisterConsumer`, with a `WindowProc` that intercepts the AVEVA
|
window, calls `AlarmClient.RegisterConsumer(hWnd, …)` +
|
||||||
WM_APP message and routes change-enumeration into
|
`Subscribe(@"\Galaxy!", …)`, and pumps for 20s while logging every
|
||||||
`MxAccessAlarmEventSink.EnqueueTransition`. Open questions before
|
window message that arrives. Run results below — this turned the
|
||||||
implementation:
|
"WM_APP pump" design assumption upside down.
|
||||||
|
|
||||||
1. **WM_APP message ID.** Not in the public surface — needs either
|
**`RegisterConsumer` and `Subscribe` both returned 0 (success).** The
|
||||||
AVEVA's C++ Toolkit reference (canonical doc per `gateway.md`) or
|
calls are valid against the deployed assembly; no parameter pinning
|
||||||
a runtime probe (subclass a window, log every WM arriving while a
|
needed.
|
||||||
live alarm is fired, identify the AVEVA one). Worth doing once on
|
|
||||||
the dev rig and checking the result in.
|
**A registered-message-class WM (ID `0xC275` in this OS session)
|
||||||
2. **`wParam` / `lParam` semantics.** Probably none — the pattern is
|
fired every ~1s after `Subscribe` completed.** Constant
|
||||||
"got poked; pull state via `GetStatistics`." Confirm during the
|
`wParam = 0x00001100`, constant `lParam = 0x079E46D8` (looks like a
|
||||||
probe.
|
stable pointer into AVEVA-internal state) for all 20 hits. The
|
||||||
3. **Threading.** AVEVA almost certainly delivers the WM on the
|
constant payload across hits with no Galaxy alarm being fired
|
||||||
thread that owns the window. The worker's STA is the natural
|
suggests this is a **heartbeat/keepalive**, not a per-change
|
||||||
home; the existing `StaRuntime` already runs a pump there. If
|
notification.
|
||||||
AVEVA assumes a UI thread (MTA-incompatible call paths inside
|
|
||||||
`GetStatistics`), the alarm path may need its own STA.
|
**Critically: this WM is delivered to AVEVA's own internal window
|
||||||
4. **Subscription scope.** `AlarmClient.Subscribe(szSubscription,
|
(`hwnd=0x18032E`) — NOT to the consumer's `hWnd` we passed in.** The
|
||||||
…)` takes an AVEVA-syntax string for the alarm provider (e.g.
|
consumer window's `WndProc` received only the standard creation
|
||||||
`"\Galaxy!"` for a whole Galaxy or `"\Galaxy!Group.Tag"` for a
|
sequence (`WM_GETMINMAXINFO`, `WM_NCCREATE`, `WM_NCCALCSIZE`,
|
||||||
subset). The configured Galaxy name is already known to the
|
`WM_CREATE`) and the destruction sequence (`WM_NCDESTROY`,
|
||||||
worker via the existing data session — reuse it.
|
`WM_DESTROY`, `WM_NCCALCSIZE`) — nothing in between. AVEVA's
|
||||||
|
notification path runs entirely against AVEVA's internal window;
|
||||||
|
it never forwards to the user-supplied hWnd.
|
||||||
|
|
||||||
|
The message ID itself is dynamic (a `RegisterWindowMessage`
|
||||||
|
allocation in the >= 0xC000 range), so it cannot be hard-coded —
|
||||||
|
each consumer process must call `RegisterWindowMessage` with the
|
||||||
|
correct *string* and use whatever ID the OS returns.
|
||||||
|
|
||||||
|
## What this means for A.2
|
||||||
|
|
||||||
|
The "WM_APP pump on the user hWnd" design — what the original plan
|
||||||
|
banner described and what the previous version of this doc
|
||||||
|
recommended — does not match how AVEVA actually delivers
|
||||||
|
notifications. The hWnd parameter to `RegisterConsumer` does not
|
||||||
|
appear to receive any of AVEVA's alarm traffic; it's likely used
|
||||||
|
only as a registration identity (and perhaps as a parent for modal
|
||||||
|
dialogs).
|
||||||
|
|
||||||
|
Two viable A.2 designs given the probe data:
|
||||||
|
|
||||||
|
1. **Polling.** Just call `GetStatistics` on a timer (e.g. every
|
||||||
|
500ms in the worker's STA) and react to the change set it
|
||||||
|
reports. No window plumbing needed. Trade-off: latency floor =
|
||||||
|
poll period; modest CPU floor because the call is cheap. Matches
|
||||||
|
the heartbeat-style WM 0xC275 semantics — AVEVA itself runs a
|
||||||
|
poll loop internally.
|
||||||
|
2. **Hook AVEVA's internal window.** Discover AVEVA's own window
|
||||||
|
(`hwnd=0x18032E` in the probe), `SetWindowsHookEx` or
|
||||||
|
`SetWindowSubclass` on it, and intercept WM 0xC275 on AVEVA's
|
||||||
|
thread. Higher fidelity, near-zero latency, but invasive,
|
||||||
|
fragile across AVEVA upgrades, and requires running on the same
|
||||||
|
process / thread as the AVEVA window. Probably a non-starter
|
||||||
|
without further AVEVA documentation.
|
||||||
|
|
||||||
|
**Recommendation:** the polling path (option 1) is cheaper to
|
||||||
|
implement, more robust against AVEVA-internal change, and
|
||||||
|
acceptable for a typical alarm cadence. The worker's existing STA
|
||||||
|
already provides a thread-affinitized timer surface. The unanswered
|
||||||
|
question is whether `GetStatistics` can be safely called outside
|
||||||
|
AVEVA's own message-pump thread — confirmable by extending the
|
||||||
|
probe to fire `GetStatistics` on its own thread and check the
|
||||||
|
result.
|
||||||
|
|
||||||
|
## Open follow-up probes
|
||||||
|
|
||||||
|
Each can be added to `AlarmClientWmProbeTests` as a separate
|
||||||
|
Skip-gated test:
|
||||||
|
|
||||||
|
1. **Fire a real Galaxy alarm during the pump window.** Confirms
|
||||||
|
whether the WM 0xC275 cadence changes (becomes per-change rather
|
||||||
|
than periodic) and whether `GetStatistics` returns a non-empty
|
||||||
|
`ChangeCodes / ChangePos / hAlarm` triple.
|
||||||
|
2. **Call `GetStatistics` on a different thread from the
|
||||||
|
`RegisterConsumer` thread** to test threading affinity.
|
||||||
|
3. **Hook AVEVA's internal window** to log what WMs it actually
|
||||||
|
processes (would resolve option 2 above).
|
||||||
|
4. **Decompile `aaAlarmManagedClient.dll`'s IL** for the
|
||||||
|
`RegisterConsumer` method to find what `RegisterWindowMessage`
|
||||||
|
string is used and whether there's a callback-registration
|
||||||
|
surface on `WNAL_Register` that the managed client wraps. The
|
||||||
|
alarmlst.dll strings (`WNAL_CallBack`, "Invalid callbacks" error)
|
||||||
|
suggest the underlying C API takes callbacks, but the managed
|
||||||
|
wrapper exposes none of them.
|
||||||
|
|
||||||
PR A.5's `Subscribe` / `AcknowledgeByGuid` / `SnapshotActiveAlarms`
|
PR A.5's `Subscribe` / `AcknowledgeByGuid` / `SnapshotActiveAlarms`
|
||||||
are correct — they're pull-style and don't depend on the missing
|
are correct — they're pull-style and don't depend on the
|
||||||
event surface. The event-subscription wiring is what has to be
|
notification mechanism.
|
||||||
replaced.
|
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using AlarmMgrDataProviderCOM;
|
||||||
|
using aaAlarmManagedClient;
|
||||||
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
|
namespace MxGateway.Worker.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime probe — registers as an AlarmClient consumer with a real
|
||||||
|
/// hidden message-only window, subscribes to a Galaxy alarm provider,
|
||||||
|
/// and logs every Win32 message that arrives during a fixed pump
|
||||||
|
/// window. The intent is to identify the WM_APP / RegisterWindowMessage
|
||||||
|
/// ID that AVEVA's alarm provider posts when alarms change, plus the
|
||||||
|
/// <c>wParam</c>/<c>lParam</c> semantics on each.
|
||||||
|
///
|
||||||
|
/// Skip-gated by default; flip Skip=null and run against the live dev
|
||||||
|
/// rig to capture output. Requires:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>A reachable Galaxy with at least one alarmable object.</description></item>
|
||||||
|
/// <item><description>The configured Galaxy expression below to match a real provider (default <c>"\\Galaxy"</c> — adjust if needed).</description></item>
|
||||||
|
/// <item><description>An alarm trigger during the pump window (raise / ack / clear something in the Galaxy via System Platform IDE) — without one, only ambient activity is captured.</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class AlarmClientWmProbeTests : IDisposable
|
||||||
|
{
|
||||||
|
// Probe configuration. Override in the constructor below if needed.
|
||||||
|
private const string SubscriptionExpression = @"\Galaxy!";
|
||||||
|
private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(20);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateWindowExW")]
|
||||||
|
private static extern IntPtr CreateWindowEx(
|
||||||
|
int dwExStyle, string lpClassName, string lpWindowName,
|
||||||
|
int dwStyle, int X, int Y, int nWidth, int nHeight,
|
||||||
|
IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool DestroyWindow(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
private static extern ushort RegisterClassW(ref WNDCLASSW lpWndClass);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool UnregisterClassW(string lpClassName, IntPtr hInstance);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr DefWindowProcW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr DispatchMessage(ref MSG lpMsg);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool TranslateMessage(ref MSG lpMsg);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll")]
|
||||||
|
private static extern IntPtr GetModuleHandle(string lpModuleName);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern uint RegisterWindowMessage(string lpString);
|
||||||
|
|
||||||
|
private const int HWND_MESSAGE = -3;
|
||||||
|
private const uint PM_REMOVE = 0x0001;
|
||||||
|
|
||||||
|
private delegate IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
private struct WNDCLASSW
|
||||||
|
{
|
||||||
|
public uint style;
|
||||||
|
public IntPtr lpfnWndProc;
|
||||||
|
public int cbClsExtra;
|
||||||
|
public int cbWndExtra;
|
||||||
|
public IntPtr hInstance;
|
||||||
|
public IntPtr hIcon;
|
||||||
|
public IntPtr hCursor;
|
||||||
|
public IntPtr hbrBackground;
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] public string? lpszMenuName;
|
||||||
|
[MarshalAs(UnmanagedType.LPWStr)] public string lpszClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct MSG
|
||||||
|
{
|
||||||
|
public IntPtr hwnd;
|
||||||
|
public uint message;
|
||||||
|
public IntPtr wParam;
|
||||||
|
public IntPtr lParam;
|
||||||
|
public uint time;
|
||||||
|
public int x;
|
||||||
|
public int y;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ITestOutputHelper output;
|
||||||
|
private readonly ConcurrentQueue<string> log = new ConcurrentQueue<string>();
|
||||||
|
private readonly Stopwatch elapsed = Stopwatch.StartNew();
|
||||||
|
private GCHandle wndProcHandle;
|
||||||
|
private IntPtr probeWindow = IntPtr.Zero;
|
||||||
|
private string? registeredClass;
|
||||||
|
|
||||||
|
public AlarmClientWmProbeTests(ITestOutputHelper output)
|
||||||
|
{
|
||||||
|
this.output = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (with live Galaxy) to capture AVEVA WM_APP message IDs")]
|
||||||
|
public void ProbeAlarmClientWmMessages()
|
||||||
|
{
|
||||||
|
// 1. Pre-resolve a few candidate RegisterWindowMessage strings so any
|
||||||
|
// matches in the captured log can be labeled. None of these is
|
||||||
|
// confirmed; we record what each resolves to so the actual AVEVA
|
||||||
|
// message ID (whatever it turns out to be) can be cross-referenced.
|
||||||
|
string[] candidateNames =
|
||||||
|
{
|
||||||
|
"WW_AlarmConsumer", "WW_AlarmManager", "WW_Alarm",
|
||||||
|
"WNAL_AlarmChange", "WNAL_AlarmChanges", "WNAL_AlarmNotify",
|
||||||
|
"WNAL_Notify", "WNAL_ChangeNotification",
|
||||||
|
"AlarmManager.Notify", "AlarmManagerNotify",
|
||||||
|
"ArchestrA.AlarmChange", "AVEVA.AlarmNotify",
|
||||||
|
"aaAlarmManagedClient.Notify",
|
||||||
|
"GotAlarmChanges", "OnAlarmChanges",
|
||||||
|
};
|
||||||
|
foreach (string name in candidateNames)
|
||||||
|
{
|
||||||
|
uint id = RegisterWindowMessage(name);
|
||||||
|
output.WriteLine($"RegisterWindowMessage(\"{name}\") -> 0x{id:X4} ({id})");
|
||||||
|
}
|
||||||
|
output.WriteLine("");
|
||||||
|
|
||||||
|
// 2. Spin up a single STA-affinitized thread, create a hidden message-
|
||||||
|
// only window owned by it, run RegisterConsumer + Subscribe against
|
||||||
|
// that window's hWnd, then pump messages on that thread for the
|
||||||
|
// configured duration. Threading discipline matches the worker's
|
||||||
|
// StaRuntime model.
|
||||||
|
Exception? threadException = null;
|
||||||
|
var pumpDone = new ManualResetEventSlim(false);
|
||||||
|
var thread = new Thread(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RunProbe();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
threadException = ex;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
pumpDone.Set();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
thread.IsBackground = false;
|
||||||
|
thread.SetApartmentState(ApartmentState.STA);
|
||||||
|
thread.Start();
|
||||||
|
pumpDone.Wait();
|
||||||
|
thread.Join();
|
||||||
|
|
||||||
|
// 3. Drain the log to xunit output regardless of outcome — partial
|
||||||
|
// captures are still informative.
|
||||||
|
output.WriteLine("");
|
||||||
|
output.WriteLine($"Captured {log.Count} log line(s):");
|
||||||
|
while (log.TryDequeue(out string? line))
|
||||||
|
{
|
||||||
|
output.WriteLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threadException != null)
|
||||||
|
{
|
||||||
|
throw threadException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RunProbe()
|
||||||
|
{
|
||||||
|
// 3a. Register a window class and create a message-only window.
|
||||||
|
WndProc wndProc = ProbeWndProc;
|
||||||
|
wndProcHandle = GCHandle.Alloc(wndProc); // keep delegate alive
|
||||||
|
|
||||||
|
registeredClass = "MxGatewayAlarmProbe_" + Guid.NewGuid().ToString("N");
|
||||||
|
var cls = new WNDCLASSW
|
||||||
|
{
|
||||||
|
style = 0,
|
||||||
|
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(wndProc),
|
||||||
|
hInstance = GetModuleHandle(null!),
|
||||||
|
lpszClassName = registeredClass,
|
||||||
|
};
|
||||||
|
ushort atom = RegisterClassW(ref cls);
|
||||||
|
if (atom == 0)
|
||||||
|
{
|
||||||
|
int err = Marshal.GetLastWin32Error();
|
||||||
|
Log($"RegisterClass failed err=0x{err:X8}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log($"RegisterClass ok atom=0x{atom:X4} class={registeredClass}");
|
||||||
|
|
||||||
|
probeWindow = CreateWindowEx(
|
||||||
|
dwExStyle: 0, lpClassName: registeredClass, lpWindowName: "AlarmProbe",
|
||||||
|
dwStyle: 0, X: 0, Y: 0, nWidth: 0, nHeight: 0,
|
||||||
|
hWndParent: (IntPtr)HWND_MESSAGE, hMenu: IntPtr.Zero,
|
||||||
|
hInstance: cls.hInstance, lpParam: IntPtr.Zero);
|
||||||
|
if (probeWindow == IntPtr.Zero)
|
||||||
|
{
|
||||||
|
int err = Marshal.GetLastWin32Error();
|
||||||
|
Log($"CreateWindowEx(HWND_MESSAGE) failed err=0x{err:X8}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log($"Created message-only window hWnd=0x{probeWindow.ToInt64():X}");
|
||||||
|
|
||||||
|
// 3b. Create the AlarmClient and try the lifecycle. RegisterConsumer
|
||||||
|
// accepts an int hWnd — narrow the IntPtr (sufficient on x86).
|
||||||
|
AlarmClient? client = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
client = new AlarmClient();
|
||||||
|
int register = client.RegisterConsumer(
|
||||||
|
hWnd: probeWindow.ToInt32(),
|
||||||
|
szProductName: "AlarmProbe",
|
||||||
|
szApplicationName: "AlarmProbe.Tests",
|
||||||
|
szVersion: "1.0",
|
||||||
|
bRetainHiddenAlarms: false);
|
||||||
|
Log($"RegisterConsumer -> {register}");
|
||||||
|
|
||||||
|
int subscribe = client.Subscribe(
|
||||||
|
szSubscription: SubscriptionExpression,
|
||||||
|
wFromPri: 1, wToPri: 999,
|
||||||
|
QueryType: eQueryType.qtSummary,
|
||||||
|
SortFlags: eSortFlags.sfReturnNewestFirst,
|
||||||
|
FilterMask: eAlarmFilterState.asNone,
|
||||||
|
FilterSpecification: eAlarmFilterState.asNone);
|
||||||
|
Log($"Subscribe('{SubscriptionExpression}') -> {subscribe}");
|
||||||
|
|
||||||
|
// 3c. Pump for the configured duration. Log every message we see
|
||||||
|
// (filtered light to avoid noise from WM_PAINT / WM_TIMER /
|
||||||
|
// WM_GETICON spam from typical pumps).
|
||||||
|
DateTime deadline = DateTime.UtcNow + PumpDuration;
|
||||||
|
while (DateTime.UtcNow < deadline)
|
||||||
|
{
|
||||||
|
while (PeekMessage(out MSG msg, IntPtr.Zero, 0, 0, PM_REMOVE))
|
||||||
|
{
|
||||||
|
LogIfInteresting(msg);
|
||||||
|
TranslateMessage(ref msg);
|
||||||
|
DispatchMessage(ref msg);
|
||||||
|
}
|
||||||
|
Thread.Sleep(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log($"Pump duration {PumpDuration.TotalSeconds:F0}s elapsed; deregistering.");
|
||||||
|
|
||||||
|
try { int dereg = client.DeregisterConsumer(); Log($"DeregisterConsumer -> {dereg}"); }
|
||||||
|
catch (Exception ex) { Log($"DeregisterConsumer threw: {ex.GetType().Name}: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { client?.Dispose(); } catch { /* swallow */ }
|
||||||
|
if (probeWindow != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
DestroyWindow(probeWindow);
|
||||||
|
probeWindow = IntPtr.Zero;
|
||||||
|
}
|
||||||
|
if (registeredClass != null)
|
||||||
|
{
|
||||||
|
UnregisterClassW(registeredClass, GetModuleHandle(null!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogIfInteresting(MSG m)
|
||||||
|
{
|
||||||
|
// Filter out the highest-volume noise (timer ticks, paint, mouse moves
|
||||||
|
// from a desktop session). Keep WM_USER..WM_APP+ entirely; those are
|
||||||
|
// the candidates for the AVEVA-registered message.
|
||||||
|
const uint WM_PAINT = 0x000F;
|
||||||
|
const uint WM_TIMER = 0x0113;
|
||||||
|
const uint WM_MOUSEMOVE = 0x0200;
|
||||||
|
const uint WM_NCMOUSEMOVE = 0x00A0;
|
||||||
|
if (m.message == WM_PAINT || m.message == WM_TIMER ||
|
||||||
|
m.message == WM_MOUSEMOVE || m.message == WM_NCMOUSEMOVE)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string interpreted = InterpretMessageId(m.message);
|
||||||
|
Log(string.Format(
|
||||||
|
"WM 0x{0:X4} ({1}) wParam=0x{2:X8} lParam=0x{3:X8} hwnd=0x{4:X}",
|
||||||
|
m.message, interpreted,
|
||||||
|
m.wParam.ToInt64() & 0xFFFFFFFF, m.lParam.ToInt64() & 0xFFFFFFFF,
|
||||||
|
m.hwnd.ToInt64()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string InterpretMessageId(uint id)
|
||||||
|
{
|
||||||
|
if (id < 0x0400) return "WM_<system>";
|
||||||
|
if (id < 0x8000) return $"WM_USER+0x{id - 0x0400:X4}";
|
||||||
|
if (id < 0xC000) return $"WM_APP+0x{id - 0x8000:X4}";
|
||||||
|
return $"RegisterWindowMessage_0x{id:X4}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr ProbeWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
|
||||||
|
{
|
||||||
|
// Log every WM that lands on the probe window itself.
|
||||||
|
string interpreted = InterpretMessageId(msg);
|
||||||
|
Log(string.Format(
|
||||||
|
"WndProc WM 0x{0:X4} ({1}) wParam=0x{2:X8} lParam=0x{3:X8}",
|
||||||
|
msg, interpreted,
|
||||||
|
wParam.ToInt64() & 0xFFFFFFFF, lParam.ToInt64() & 0xFFFFFFFF));
|
||||||
|
return DefWindowProcW(hWnd, msg, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Log(string line)
|
||||||
|
{
|
||||||
|
log.Enqueue($"[t={elapsed.Elapsed.TotalSeconds:F3}s] {line}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (wndProcHandle.IsAllocated) wndProcHandle.Free();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,4 +25,17 @@
|
|||||||
<ProjectReference Include="..\MxGateway.Worker\MxGateway.Worker.csproj" />
|
<ProjectReference Include="..\MxGateway.Worker\MxGateway.Worker.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="aaAlarmManagedClient">
|
||||||
|
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\aaAlarmManagedClient.dll</HintPath>
|
||||||
|
<Private>true</Private>
|
||||||
|
<SpecificVersion>false</SpecificVersion>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="IAlarmMgrDataProvider">
|
||||||
|
<HintPath>C:\Program Files (x86)\ArchestrA\Framework\Bin\ViewAppFramework\Content\MA\IAlarmMgrDataProvider.dll</HintPath>
|
||||||
|
<Private>true</Private>
|
||||||
|
<SpecificVersion>false</SpecificVersion>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user