feat(worker): add opt-in managed GUID alarm-ack path
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m19s
ci / java (push) Successful in 2m8s
ci / portable (push) Failing after 2m23s
ci / nightly-windev (pull_request) Has been skipped
ci / windows-x86 (pull_request) Successful in 1m18s
ci / java (pull_request) Successful in 2m3s
ci / portable (pull_request) Failing after 2m17s
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m19s
ci / java (push) Successful in 2m8s
ci / portable (push) Failing after 2m23s
ci / nightly-windev (pull_request) Has been skipped
ci / windows-x86 (pull_request) Successful in 1m18s
ci / java (pull_request) Successful in 2m3s
ci / portable (pull_request) Failing after 2m17s
Adds a managed acknowledge path over AVEVA's modern alarm client so GUID acks carry full operator identity (domain + full name) and work on builds where the wnwrap COM AlarmAckByGUID is E_NOTIMPL. Default off; opt in with MXGATEWAY_USE_MANAGED_ALARM_ACK=1 (wnwrap behavior unchanged otherwise). - AaManagedAlarmAckConsumer: reflection wrapper over aaAlarmManagedClient.AlarmClient (InitializeConsumer/RegisterConsumer/ Subscribe/AlarmAckByGUID); EnvGatedFactory degrades to null (wnwrap) if the DLL can't load, so alarm reads never break. - IManagedAlarmAckClient seam. AlarmDispatcher takes an optional client and routes GUID acks to it (subscribing + disposing it alongside the reader). - AlarmCommandHandler gains an optional managedAckFactory; WorkerPipeSession wires AaManagedAlarmAckConsumer.EnvGatedFactory at the production sites. - WnalNativeAckConsumer: confirmed native alarmlst.dll WNAL submit layer (WNALalarmReqSetText / WNALUserRequest / WNALUserRequestEx + lpAlmAck layout). List acquisition is intentionally not hand-rolled: the alarm-record layout and selection semantics are provider-defined and not recoverable from alarmlst.dll, so inferring them risks acking the wrong alarm. - AlarmManagedGuidAckLiveSmokeTests: skip-gated live smoke that pulls a real (non-degraded) alarm GUID and asserts the ack APPLIES (Acknowledge transition / acked snapshot), not merely rc=0. By-name acks stay on wnwrap (AlarmClient has no AlarmAckByName). Grounded in static RE of alarmlst.dll, aaAlarmManagedClient.dll and alarmmgr.exe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -36,20 +36,28 @@ public sealed class AlarmDispatcher : IDisposable
|
||||
private readonly MxAccessAlarmEventSink sink;
|
||||
private readonly string sessionId;
|
||||
private readonly EventHandler<MxAlarmTransitionEvent> handler;
|
||||
private readonly IManagedAlarmAckClient? managedAck;
|
||||
private bool disposed;
|
||||
|
||||
/// <summary>Initializes a new alarm dispatcher for the given consumer, sink, and session ID.</summary>
|
||||
/// <param name="consumer">The alarm consumer.</param>
|
||||
/// <param name="sink">The alarm event sink.</param>
|
||||
/// <param name="sessionId">The session identifier.</param>
|
||||
/// <param name="managedAck">
|
||||
/// Optional managed ack client. When non-null, GUID-keyed acknowledges
|
||||
/// route to it (AVEVA's modern <c>AlarmClient.AlarmAckByGUID</c>) instead
|
||||
/// of the wnwrap consumer; null keeps the existing wnwrap ack path.
|
||||
/// </param>
|
||||
public AlarmDispatcher(
|
||||
IMxAccessAlarmConsumer consumer,
|
||||
MxAccessAlarmEventSink sink,
|
||||
string sessionId)
|
||||
string sessionId,
|
||||
IManagedAlarmAckClient? managedAck = null)
|
||||
{
|
||||
this.consumer = consumer ?? throw new ArgumentNullException(nameof(consumer));
|
||||
this.sink = sink ?? throw new ArgumentNullException(nameof(sink));
|
||||
this.sessionId = sessionId ?? string.Empty;
|
||||
this.managedAck = managedAck;
|
||||
// Sink.Attach is the seam that propagates the session id onto the
|
||||
// proto SessionId field of every emitted MxEvent. Pass the consumer
|
||||
// as the "associated COM object" — sink ignores the object reference
|
||||
@@ -70,6 +78,10 @@ public sealed class AlarmDispatcher : IDisposable
|
||||
{
|
||||
if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher));
|
||||
consumer.Subscribe(subscription);
|
||||
// The managed ack client maintains its own registered/subscribed
|
||||
// consumer; run its lifecycle against the same expression so a later
|
||||
// AlarmAckByGUID has a live consumer to act through.
|
||||
managedAck?.Subscribe(subscription);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -93,6 +105,20 @@ public sealed class AlarmDispatcher : IDisposable
|
||||
string ackOperatorFullName)
|
||||
{
|
||||
if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher));
|
||||
if (managedAck is not null)
|
||||
{
|
||||
// Preferred path: AVEVA's modern AlarmClient.AlarmAckByGUID carries
|
||||
// operator domain + full name and is not E_NOTIMPL like the wnwrap
|
||||
// COM AlarmAckByGUID.
|
||||
return managedAck.AcknowledgeByGuid(
|
||||
alarmGuid,
|
||||
ackComment,
|
||||
ackOperatorName,
|
||||
ackOperatorNode,
|
||||
ackOperatorDomain,
|
||||
ackOperatorFullName);
|
||||
}
|
||||
|
||||
return consumer.AcknowledgeByGuid(
|
||||
alarmGuid,
|
||||
ackComment,
|
||||
@@ -268,5 +294,6 @@ public sealed class AlarmDispatcher : IDisposable
|
||||
try { consumer.AlarmTransitionEmitted -= handler; } catch { /* swallow */ }
|
||||
try { sink.Detach(); } catch { /* swallow */ }
|
||||
try { consumer.Dispose(); } catch { /* swallow */ }
|
||||
try { managedAck?.Dispose(); } catch { /* swallow */ }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user