Files
mxaccessgw/src/MxGateway.Worker/MxAccess/MxAccessAlarmEventSink.cs
T
Joseph Doherty 1764eff1cf Resolve Worker-009..015 code-review findings
Worker-009: WorkerFrameWriter serialized twice and WorkerFrameReader
allocated a payload byte[] per frame. The writer now serializes once into a
single prefix+payload buffer; the reader rents the payload buffer from
ArrayPool and honors the logical frame length.

Worker-010: VariantConverter projected a uint+Time value as a full FILETIME,
producing a near-1601 timestamp. The FILETIME projection is now gated on
`value is long`; uint falls through to the integer projection.

Worker-011: replaced the opaque retryAttempts formula in WorkerPipeClient
with MaxRetryAttempts = int.MaxValue, leaving the connect deadline as the
sole bound.

Worker-012: rewrote stale "future PR / polls on a Timer" comments in
AlarmDispatcher, AlarmCommandHandler, MxAccessAlarmEventSink and
MxAccessEventMapper to match the shipped, post-Worker-001 behavior.

Worker-013 (re-triaged): already resolved — StaMessagePumpTests and
MxAccessStaSessionTests cover the pump and poll loop directly.

Worker-014: moved IAlarmCommandHandler into its own file so
AlarmCommandHandler.cs declares one public type.

Worker-015: clarified the MxAccessBaseEventSink.EnqueueEvent overflow-catch
comment explaining the deliberate double RecordFault no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:42:17 -04:00

121 lines
4.5 KiB
C#

using System;
using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Sink for native MxAccess alarm transitions. Bridges
/// <see cref="WnWrapAlarmConsumer"/> to the worker's event queue,
/// producing <see cref="OnAlarmTransitionEvent"/> messages via
/// <see cref="MxAccessEventMapper.CreateOnAlarmTransition"/>.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="AlarmDispatcher"/> owns the wire-up: it constructs the
/// consumer/sink pair, calls <see cref="Attach"/> to propagate the
/// session id, and subscribes the consumer's
/// <see cref="IMxAccessAlarmConsumer.AlarmTransitionEmitted"/> event
/// so each decoded transition reaches <see cref="EnqueueTransition"/>.
/// The <see cref="Attach"/> method here carries only the session id —
/// the alarm path needs no COM-event subscription of its own because
/// the consumer already polls and raises transition events. The
/// captured payload schema is described in
/// <c>docs/AlarmClientDiscovery.md</c> "Option A — captured".
/// </para>
/// </remarks>
public sealed class MxAccessAlarmEventSink : IMxAccessEventSink
{
private readonly MxAccessEventMapper eventMapper;
private readonly MxAccessEventQueue eventQueue;
private string sessionId = string.Empty;
private bool attached;
public MxAccessAlarmEventSink()
: this(new MxAccessEventQueue(), new MxAccessEventMapper())
{
}
public MxAccessAlarmEventSink(
MxAccessEventQueue eventQueue,
MxAccessEventMapper eventMapper)
{
this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue));
this.eventMapper = eventMapper ?? throw new ArgumentNullException(nameof(eventMapper));
}
/// <inheritdoc />
public void Attach(object mxAccessComObject, string sessionId)
{
if (mxAccessComObject is null) throw new ArgumentNullException(nameof(mxAccessComObject));
this.sessionId = sessionId ?? string.Empty;
// The alarm path needs no COM-event subscription here: the wnwrap
// consumer is polled by the worker's STA and raises transition events
// that AlarmDispatcher routes into EnqueueTransition. Attach only
// records the session id stamped onto every emitted MxEvent.
attached = true;
}
/// <inheritdoc />
public void Detach()
{
if (!attached) return;
attached = false;
sessionId = string.Empty;
}
/// <summary>
/// Enqueues a decoded alarm transition. The COM-side delegate registered
/// in <see cref="Attach"/> calls this method once it pulls the alarm
/// fields out of the MxAccess event payload. Exposed internal so unit
/// tests can drive the proto build path without a real COM event
/// source.
/// </summary>
internal void EnqueueTransition(
string alarmFullReference,
string sourceObjectReference,
string alarmTypeName,
AlarmTransitionKind transitionKind,
int severity,
DateTime? originalRaiseTimestampUtc,
DateTime transitionTimestampUtc,
string operatorUser,
string operatorComment,
string category,
string description)
{
try
{
MxEvent mxEvent = eventMapper.CreateOnAlarmTransition(
sessionId,
alarmFullReference,
sourceObjectReference,
alarmTypeName,
transitionKind,
severity,
originalRaiseTimestampUtc,
transitionTimestampUtc,
operatorUser,
operatorComment,
category,
description,
statuses: null);
eventQueue.Enqueue(mxEvent);
}
catch (Exception exception)
{
eventQueue.RecordFault(new WorkerFault
{
Category = WorkerFaultCategory.MxaccessEventConversionFailed,
ExceptionType = exception.GetType().FullName ?? string.Empty,
DiagnosticMessage = $"{exception.GetType().FullName}: HRESULT 0x{unchecked((uint)exception.HResult):X8}",
ProtocolStatus = new ProtocolStatus
{
Code = ProtocolStatusCode.MxaccessFailure,
Message = "MXAccess alarm event conversion failed.",
},
});
}
}
}