using System; using System.Collections.Generic; using MxGateway.Contracts.Proto; namespace MxGateway.Worker.MxAccess; /// /// Per-session owner of the worker's alarm-side state. Lazy-creates an /// (with a wnwrap-backed /// by default) on the first /// call, then routes /// / / /// through the same instance for the /// session's lifetime. /// /// /// /// Construction is dependency-injectable: the consumer factory /// (default () => new WnWrapAlarmConsumer()) lets tests /// substitute a fake without touching AVEVA COM. The event queue /// is supplied by the owning so /// the alarm-side proto events land on the same queue the worker /// already drains for IPC dispatch. /// /// /// Threading: invoked from /// which runs on the STA. The wnwrap consumer's polling timer /// fires on a thread-pool thread; the only cross-thread surface /// is the 's event handler, which /// hand-offs into the thread-safe . /// /// public sealed class AlarmCommandHandler : IAlarmCommandHandler { private readonly MxAccessEventQueue eventQueue; private readonly Func consumerFactory; private readonly object syncRoot = new object(); private AlarmDispatcher? dispatcher; private bool disposed; public AlarmCommandHandler(MxAccessEventQueue eventQueue) : this(eventQueue, () => new WnWrapAlarmConsumer()) { } /// Test seam — inject a custom consumer factory. public AlarmCommandHandler( MxAccessEventQueue eventQueue, Func consumerFactory) { this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue)); this.consumerFactory = consumerFactory ?? throw new ArgumentNullException(nameof(consumerFactory)); } public bool IsSubscribed { get { lock (syncRoot) return dispatcher is not null; } } /// public void Subscribe(string subscription, string sessionId) { if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler)); if (subscription is null) throw new ArgumentNullException(nameof(subscription)); lock (syncRoot) { if (dispatcher is not null) { throw new InvalidOperationException( "AlarmCommandHandler already has an active subscription; " + "call Unsubscribe before issuing another SubscribeAlarms command."); } IMxAccessAlarmConsumer consumer = consumerFactory() ?? throw new InvalidOperationException("Alarm consumer factory returned null."); MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink( eventQueue, new MxAccessEventMapper()); dispatcher = new AlarmDispatcher(consumer, sink, sessionId ?? string.Empty); try { dispatcher.Subscribe(subscription); } catch { try { dispatcher.Dispose(); } catch { /* swallow */ } dispatcher = null; throw; } } } /// public void Unsubscribe() { AlarmDispatcher? toDispose; lock (syncRoot) { toDispose = dispatcher; dispatcher = null; } toDispose?.Dispose(); } /// public int Acknowledge( Guid alarmGuid, string comment, string operatorUser, string operatorNode, string operatorDomain, string operatorFullName) { AlarmDispatcher? d = GetDispatcherOrThrow(); return d.Acknowledge( alarmGuid, comment ?? string.Empty, operatorUser ?? string.Empty, operatorNode ?? string.Empty, operatorDomain ?? string.Empty, operatorFullName ?? string.Empty); } /// public int AcknowledgeByName( string alarmName, string providerName, string groupName, string comment, string operatorUser, string operatorNode, string operatorDomain, string operatorFullName) { AlarmDispatcher? d = GetDispatcherOrThrow(); return d.AcknowledgeByName( alarmName ?? string.Empty, providerName ?? string.Empty, groupName ?? string.Empty, comment ?? string.Empty, operatorUser ?? string.Empty, operatorNode ?? string.Empty, operatorDomain ?? string.Empty, operatorFullName ?? string.Empty); } /// public IReadOnlyList QueryActive(string? alarmFilterPrefix) { AlarmDispatcher? d = GetDispatcherOrThrow(); IReadOnlyList all = d.SnapshotActiveAlarms(); if (string.IsNullOrEmpty(alarmFilterPrefix)) return all; List filtered = new List(all.Count); foreach (ActiveAlarmSnapshot snap in all) { if (snap.AlarmFullReference.StartsWith(alarmFilterPrefix!, StringComparison.Ordinal)) { filtered.Add(snap); } } return filtered; } /// public void PollOnce() { AlarmDispatcher? d; lock (syncRoot) d = dispatcher; // No-op when not yet subscribed or already disposed. d?.PollOnce(); } private AlarmDispatcher GetDispatcherOrThrow() { if (disposed) throw new ObjectDisposedException(nameof(AlarmCommandHandler)); AlarmDispatcher? d; lock (syncRoot) d = dispatcher; if (d is null) { throw new InvalidOperationException( "AlarmCommandHandler has no active subscription; " + "call SubscribeAlarms before issuing alarm-related commands."); } return d; } /// public void Dispose() { if (disposed) return; disposed = true; Unsubscribe(); } } /// /// Per-session interface routing the worker's alarm IPC commands — /// SubscribeAlarmsCommand, AcknowledgeAlarmCommand, /// QueryActiveAlarmsCommand, UnsubscribeAlarmsCommand — /// to the underlying . Production binding /// is ; tests substitute a fake. /// public interface IAlarmCommandHandler : IDisposable { /// Begin a subscription against the supplied AVEVA alarm-provider expression. void Subscribe(string subscription, string sessionId); /// Tear down the active subscription. No-op if not subscribed. void Unsubscribe(); /// Acknowledge a single alarm by GUID. Returns AVEVA's native status (0 = success). int Acknowledge( Guid alarmGuid, string comment, string operatorUser, string operatorNode, string operatorDomain, string operatorFullName); /// /// Acknowledge a single alarm by (name, provider, group) — used when /// the caller has the human-readable reference but not the GUID. /// int AcknowledgeByName( string alarmName, string providerName, string groupName, string comment, string operatorUser, string operatorNode, string operatorDomain, string operatorFullName); /// /// Snapshot the currently-active alarm set, optionally scoped to a /// prefix matched against AlarmFullReference. /// IReadOnlyList QueryActive(string? alarmFilterPrefix); /// /// Drives a single poll of the underlying alarm consumer on the /// caller's thread. This is a no-op when there is no active /// subscription. In production the caller is the worker's STA /// (marshalled via StaRuntime.InvokeAsync), which satisfies /// the ThreadingModel=Apartment requirement of /// wwAlarmConsumerClass. /// void PollOnce(); }