diff --git a/docs/AlarmClientDiscovery.md b/docs/AlarmClientDiscovery.md index 537856f..24045cd 100644 --- a/docs/AlarmClientDiscovery.md +++ b/docs/AlarmClientDiscovery.md @@ -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 diff --git a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs index 303b220..39f1b04 100644 --- a/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs +++ b/src/MxGateway.Worker.Tests/AlarmClientWmProbeTests.cs @@ -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(); - 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(); + 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}"); + } + } /// /// Drive an MxAccess write to with the