1764eff1cf
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>
121 lines
4.5 KiB
C#
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.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|