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 80 additions and 12 deletions
Showing only changes of commit 4e8928cf71 - Show all commits
+40
View File
@@ -196,6 +196,46 @@ absence.
gateway-worker installation runs under a service account
that may have access where `dotnet test` doesn't.
## InitializeConsumer required — fourth probe run, 2026-05-01
Adding `InitializeConsumer("AlarmProbe.Tests")` before
`RegisterConsumer` made `\Galaxy!` appear in `GetProviders`
(count=1, status 0 → 100 within 500ms). So #2 and #3 above are
NOT the cause — the consumer can see the alarm provider once it
calls Initialize. That's a missing API-call ordering, not a
permission or service issue.
```
InitializeConsumer -> 0
RegisterConsumer -> 0
GetProviders [after Register] -> rc=0 count=0 list=[]
Subscribe('\Galaxy!') -> 0
GetProviders [after Subscribe] -> rc=0 count=1 list=[ 0 \Galaxy!]
GetProviders [poll #1] -> rc=0 count=1 list=[100 \Galaxy!]
```
Despite the provider being visible at "100% query complete" for
the entire 60s window, `GetStatistics` continued to report
`total=0 active=0 codes=[7]` — no alarm transitions reached the
consumer even with a System Platform script flipping the test
boolean every 10s during the run.
That isolates the remaining unknown to whether the test bool's
alarm extension is actually generating MxAccess alarm-provider
events when its value flips. The probe has confirmed every link
in the consumer chain works (Initialize → Register → Subscribe →
provider visible at 100%) — what's missing is alarm traffic from
the producer side. ObjectViewer or another live consumer running
alongside the script is the next discriminator: does it visibly
see the alarm fire?
API-ordering finding: `InitializeConsumer` MUST precede
`RegisterConsumer` (or at least, must be called before
`GetProviders` returns anything). PR A.5's `AlarmClientConsumer`
omits `InitializeConsumer` entirely — that's a bug fix to apply
even before A.2 lands, since without it the provider chain never
becomes visible.
### Implications for A.2 implementation
The A.2 PR's value is unmeasurable until at least one alarm
@@ -231,6 +231,22 @@ public sealed class AlarmClientWmProbeTests : IDisposable
try
{
client = new AlarmClient();
// Try InitializeConsumer first — separate from RegisterConsumer
// per the discovered API surface; previous probe runs skipped
// it. Some AVEVA managed-client patterns require Initialize
// before Register; others reverse the order. Try Initialize
// first; on failure proceed to Register.
try
{
int init = client.InitializeConsumer("AlarmProbe.Tests");
Log($"InitializeConsumer -> {init}");
}
catch (Exception ex)
{
Log($"InitializeConsumer threw: {ex.GetType().Name}: {ex.Message}");
}
int register = client.RegisterConsumer(
hWnd: probeWindow.ToInt32(),
szProductName: "AlarmProbe",
@@ -239,18 +255,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable
bRetainHiddenAlarms: false);
Log($"RegisterConsumer -> {register}");
// Discover what providers AVEVA sees before subscribing, so we
// can spot a wrong subscription expression up front.
try
{
var providers = new System.Collections.Generic.List<string>();
int gp = client.GetProviders(providers);
Log($"GetProviders -> rc={gp} count={providers.Count} list=[{string.Join(", ", providers)}]");
}
catch (Exception ex)
{
Log($"GetProviders threw: {ex.GetType().Name}: {ex.Message}");
}
LogProviders(client, "after Register");
int subscribe = client.Subscribe(
szSubscription: SubscriptionExpression,
@@ -261,6 +266,8 @@ public sealed class AlarmClientWmProbeTests : IDisposable
FilterSpecification: eAlarmFilterState.asNone);
Log($"Subscribe('{SubscriptionExpression}') -> {subscribe}");
LogProviders(client, "after 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). Poll GetStatistics on
@@ -290,6 +297,7 @@ public sealed class AlarmClientWmProbeTests : IDisposable
if (DateTime.UtcNow >= nextPoll)
{
PollGetStatistics(client, ++pollCount);
LogProviders(client, $"poll #{pollCount}");
nextPoll = DateTime.UtcNow + PollInterval;
}
Thread.Sleep(10);
@@ -316,6 +324,26 @@ public sealed class AlarmClientWmProbeTests : IDisposable
}
private string lastStatsSummary = string.Empty;
private string lastProvidersSummary = string.Empty;
private void LogProviders(AlarmClient client, string when)
{
try
{
var providers = new System.Collections.Generic.List<string>();
int rc = client.GetProviders(providers);
string summary = $"count={providers.Count} list=[{string.Join(", ", providers)}]";
if (summary != lastProvidersSummary)
{
Log($"GetProviders [{when}] -> rc={rc} {summary} (changed)");
lastProvidersSummary = summary;
}
}
catch (Exception ex)
{
Log($"GetProviders [{when}] threw: {ex.GetType().Name}: {ex.Message}");
}
}
/// <summary>
/// Drive an MxAccess write to <see cref="TriggerTagReference"/> with the