docs: alarm acknowledgement writes AckMsg as the authenticated operator

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
This commit is contained in:
Joseph Doherty
2026-10-19 19:44:18 -04:00
parent 2858e42e80
commit 9409488008
12 changed files with 243 additions and 105 deletions
+50 -52
View File
@@ -621,8 +621,9 @@ returned to normal but is unacknowledged — i.e., visible in the
"current alarms" list because operator hasn't acked it yet) and
`UNACK_ALM` (the alarm is currently active and unacknowledged).
The other states from `eAlmState` (`ACK_RTN`, `ACK_ALM`) would
appear when an ack is performed — `wwAlarmConsumerClass.AlarmAckByGUID`
is the method to call.
appear when an ack is performed. The alarm-client ack methods
(`AlarmAckByGUID` and friends) do not produce them headless; an `AckMsg`
write does (`AlarmProbeFindings.md`).
### `GetStatistics` AV — unrelated quirk
@@ -735,25 +736,32 @@ for `SetXmlAlarmQuery`.
the canonical `\\<machine>\Galaxy!<area>` form into the XML's
NODE/PROVIDER/GROUP fields. Mangled or not, the call enables reads.
### 2. Two consumers required: read-side vs. ack-side
### 2. Two consumers required: read-side vs. ack-side (historical)
> Superseded 2026-10-19: acks now write `AckMsg` as the authenticated operator;
> see `AlarmProbeFindings.md`. The worker no longer provisions an ack-only
> wnwrap consumer. The text below records why the split existed.
`SetXmlAlarmQuery` enables reads but **breaks `AlarmAckByName` on
the same consumer instance**. With SetXml applied, AlarmAckByName
returns -55 even with valid name+provider+group+operator. Without
SetXml, AlarmAckByName succeeds with rc=0.
The production consumer therefore provisions **two** wnwrap COM
The consumer at the time provisioned **two** wnwrap COM
instances:
- Primary consumer (`client`): runs full lifecycle including
`SetXmlAlarmQuery` for `GetXmlCurrentAlarms2` polls.
- Ack-only consumer (`ackClient`): runs Initialize → Register →
Subscribe via the v1-prefixed methods, **no SetXmlAlarmQuery**.
All `AcknowledgeByName` calls dispatch through this instance.
All by-name ack calls dispatched through this instance.
Both consumers subscribe to the same expression. Disposal cleans up
both via a shared `ReleaseConsumerCom` helper.
### 3. `AlarmAckByName` v2 8-arg vs. v1 6-arg
### 3. `AlarmAckByName` v2 8-arg vs. v1 6-arg (historical)
> Superseded 2026-10-19: the gateway no longer calls `AlarmAckByName`; see
> `AlarmProbeFindings.md`.
`wwAlarmConsumerClass` exposes two `AlarmAckByName` overloads:
- `IwwAlarmConsumer2` v2: 8 args (`name, provider, group, comment,
@@ -762,8 +770,8 @@ both via a shared `ReleaseConsumerCom` helper.
The v2 8-arg method returns -55 on this AVEVA build regardless of
operator-identity inputs — looks like a stub. The v1 6-arg method
works. Production `WnWrapAlarmConsumer.AcknowledgeByName` calls the
6-arg overload and discards the proto's `domain` + `full_name` fields.
works. `WnWrapAlarmConsumer.AcknowledgeByName` called the
6-arg overload and discarded the proto's `domain` + `full_name` fields.
**Correction, 2026-08-18** (`docs/AlarmProbeFindings.md`). Both halves
of the paragraph above are narrower than they read. The -55 tracks the
@@ -775,42 +783,34 @@ snapshot stay `UNACK_ALM`, `OPERATOR_NAME` stay empty, and the alarm
extension's own `.Acked` attribute stay `False` for 16 s after each
call. On this rig the wnwrap ack is accepted and then inert, so a zero
return code must not be read as an applied acknowledgement.
The proto contract keeps the 8 fields for forward compatibility if
AVEVA fixes the v2 method later.
### 4. `AlarmAckByGUID` is not implemented
### 4. `AlarmAckByGUID` is not implemented (historical)
> Superseded 2026-10-19: acks now write `AckMsg` as the authenticated operator;
> see `AlarmProbeFindings.md`.
The v2 `AlarmAckByGUID(VBGUID, …)` throws `NotImplementedException`
(COM `E_NOTIMPL`) on `wwAlarmConsumerClass` against this AVEVA
build. The reference→GUID lookup that we initially planned to wire
through `AlarmAckByGUID` is therefore not viable on wnwrap; all acks
must go through `AlarmAckByName`.
through `AlarmAckByGUID` is therefore not viable on wnwrap.
The proto `AcknowledgeAlarmCommand` (GUID-based) and the worker's
`MxAccessCommandExecutor.ExecuteAcknowledgeAlarm` switch arm remain
in the codebase for the forward-compat shape, but the gateway-side
`WorkerAlarmRpcDispatcher.AcknowledgeAsync` now always routes through
`AcknowledgeAlarmByName` when the public RPC supplies a recognizable
`Provider!Group.Tag` reference.
#### Managed GUID ack experiment — did not work (live-tested 2026-10-18, removed 2026-10-19)
**Opt-in managed GUID ack — does not work on this rig (live-tested
2026-10-18).** Setting `MXGATEWAY_USE_MANAGED_ALARM_ACK=1` in the
gateway's environment (workers inherit it) makes the worker build an
`AaManagedAlarmAckConsumer`, which drives
`aaAlarmManagedClient.AlarmClient` by reflection (its members are
non-public) and is meant to take GUID-keyed acks in place of the
wnwrap method. The live test shows it cannot do that as built, so leave
the flag off.
An opt-in experiment drove `aaAlarmManagedClient.AlarmClient` by reflection
(its members are non-public) to take GUID-keyed acks in place of the wnwrap
method. The live test showed it could not work, and the experiment and its
enabling flag were removed once the `AckMsg` write replaced it. The findings
are kept because they explain why the alarm-client route was abandoned.
The test raised `TestMachine_001.TestAlarm001` with a secured write,
read its GUID from the wnwrap XML snapshot (the public API exposes no
alarm GUIDs), and acknowledged it through the `AcknowledgeAlarm` RPC
with the flag on:
with the experiment enabled:
| Step | Result |
|---|---|
| RPC ack by GUID, flag off (control) | `NotImplementedException`; `.Acked` stays `False` |
| RPC ack by GUID, flag on | Same `NotImplementedException`; `.Acked` `False`, `.AckMsg` empty |
| RPC ack by GUID, experiment off (control) | `NotImplementedException`; `.Acked` stays `False` |
| RPC ack by GUID, experiment on | Same `NotImplementedException`; `.Acked` `False`, `.AckMsg` empty |
| `AlarmClient.RegisterConsumer` in a process with a registered wnwrap consumer | `-65` — and wnwrap gets `-65` if the managed client registered first; a real `hWnd` does not help |
| Managed client alone in its own process, records loaded | `AlarmAckByGUID` → `-1`, `SelectByGUID` → `-1` for the wnwrap GUID in both byte orders, `AlarmAckSelected` → `-57` |
@@ -820,7 +820,7 @@ Three separate problems follow:
a wnwrap reader, so the managed `RegisterConsumer` fails with `-65`.
The dispatcher is built to drop back to the wnwrap ack path on a
lifecycle failure (so alarm reads keep working), and it does so
silently — which is why the flag-on RPC is indistinguishable from the
silently — which is why the experiment-on RPC was indistinguishable from the
control.
2. **The managed client does not recognise the wnwrap GUID.** `-1` from
`SelectByGUID` is "not found", not a security refusal, even with the
@@ -833,14 +833,8 @@ Three separate problems follow:
strings, so the domain and full name the managed method carries
would not arrive through the public RPC even if 1 and 2 were solved.
A working managed ack would need the client hosted out of process, a way
to resolve the alarm to the identifier the managed client uses, and the
identity fields plumbed through the gateway. Routing rules the worker
already applies stay correct and would carry over: subtag-mode acks
(synthetic GUIDs) stay on the subtag consumer, and by-name acks stay on
wnwrap because `AlarmClient` has no by-name ack. The skip-gated
`AlarmManagedGuidAckLiveSmokeTests` exercises the in-process
arrangement and hits problem 1.
The route was dropped in favour of the authenticated `AckMsg` write
(`AlarmProbeFindings.md`, "Fourth attempt").
### 5. STA / threading — resolved
@@ -996,20 +990,24 @@ AVEVA-internal GUID.
### Acknowledge in subtag mode
`AlarmDispatcher` routes ack calls by active provider mode:
Both provider modes acknowledge through the worker's `AlarmAckWriter`, which
calls `AuthenticateUser` as the requesting operator and writes the comment to
the ack item over MXAccess on its own `LMXProxyServer`:
- **Alarm-manager mode:** `AlarmAckByName` on `wwAlarmConsumerClass` (unchanged).
- **Subtag mode:** `SubtagAlarmConsumer.AcknowledgeByName` resolves the
watch-list entry's `ack_comment_subtag` and issues a `Write(comment)` on
the STA via `LmxSubtagAlarmSource`. Writing the `AckMsg` subtag performs
the acknowledge in AVEVA (`AckMsg` is the confirmed `AlarmExtension` ack-comment
write target).
- **Alarm-manager mode:** the item is `<TagName>.AckMsg` for Galaxy-provider
alarms. An alarm from another provider is not ackable.
- **Subtag mode:** `SubtagAlarmConsumer` resolves the watch-list entry's
`ack_comment_subtag` and the writer writes that item verbatim. Writing the
`AckMsg` subtag performs the acknowledge in AVEVA (`AckMsg` is the confirmed
`AlarmExtension` ack-comment write target).
If the alarm has no writable ack-comment subtag (`AckComment` config key is
empty, or the entry's `ack_comment_subtag` field is empty), the ack call
returns a failure code that the gateway surfaces as `FailedPrecondition`.
`AcknowledgeByGuid` maps the synthetic GUID back to its reference via an
internal dictionary, then calls the same write path.
Because the write is authenticated, the Historian records the operator's name,
domain and full name, and the writer's MXAccess client name
(`MxGateway:Alarms:AckClientName`) as OperatorNode. If the alarm has no ack
target (`AckComment` config key is empty, the entry's `ack_comment_subtag` is
empty, or the reference or synthetic GUID is unknown), the worker reports it
as not found or not ackable and the gateway surfaces `InvalidRequest` with
`hresult` -1.
`SubtagAlarmConsumer.Subscribe` advises the ack-comment subtag alongside the
observed ones (active/acked/priority). This is required: MXAccess rejects a
@@ -1027,7 +1025,7 @@ The subtag path was validated against live MXAccess on the dev rig
`…​.Priority` → `500` (Int32), `…​.AckMsg` → string — confirming the field
names **and** the runtime reference shape `<Object>.<AlarmAttr>.<field>`
with **no** intermediate alarm-condition segment.
- `AcknowledgeByName` (AckMsg write) returned `0` once the ack-comment subtag
- The `AckMsg` write returned `0` once the ack-comment subtag
was advised — confirming the ack-by-comment-write mechanism end to end.
### Fidelity limitations