docs(src): add missing XML docs and strip tracking-ID comments

Sweep of 203 source files resolving CommentChecker findings: add
<summary>/<param>/<returns>/<inheritdoc> where missing, and remove
resolved task/issue tracking markers (Tests-NNN, Worker-NNN, Server-NNN,
Task N) from code comments. Comment/doc-only — no logic changes.
Server+Tests build clean under TreatWarningsAsErrors.
This commit is contained in:
Joseph Doherty
2026-07-07 14:09:49 -04:00
parent 8914472706
commit fca978de07
203 changed files with 1834 additions and 1383 deletions
@@ -16,17 +16,13 @@ public sealed class WorkerConsoleLogger : IWorkerLogger
_writer = writer ?? throw new ArgumentNullException(nameof(writer));
}
/// <summary>Writes an informational log entry.</summary>
/// <param name="eventName">Name of the event being logged.</param>
/// <param name="fields">Event fields and values to log.</param>
/// <inheritdoc />
public void Information(string eventName, IReadOnlyDictionary<string, object?> fields)
{
Write("Information", eventName, fields);
}
/// <summary>Writes an error log entry.</summary>
/// <param name="eventName">Name of the event being logged.</param>
/// <param name="fields">Event fields and values to log.</param>
/// <inheritdoc />
public void Error(string eventName, IReadOnlyDictionary<string, object?> fields)
{
Write("Error", eventName, fields);
@@ -28,6 +28,7 @@ public static class WorkerLogRedactor
/// Redacts sensitive field values from a log field dictionary.
/// </summary>
/// <param name="fields">Dictionary of field names and values.</param>
/// <returns>A new dictionary with sensitive field values replaced by <see cref="RedactedValue"/>.</returns>
public static Dictionary<string, object?> RedactFields(IReadOnlyDictionary<string, object?> fields)
{
Dictionary<string, object?> redactedFields = [];
@@ -45,6 +46,7 @@ public static class WorkerLogRedactor
/// </summary>
/// <param name="fieldName">Name of the field to check.</param>
/// <param name="value">Value to redact if sensitive.</param>
/// <returns>The redacted placeholder when <paramref name="fieldName"/> looks sensitive; otherwise the original value.</returns>
public static object? RedactValue(string fieldName, object? value)
{
if (value is null)
@@ -11,6 +11,7 @@ public sealed class MxStatusProxyConverter
{
/// <summary>Converts a single status object to a protobuf message, reflecting all fields and diagnostics.</summary>
/// <param name="status">COM status object to convert.</param>
/// <returns>The converted protobuf status message.</returns>
public MxStatusProxy Convert(object status)
{
if (status is null)
@@ -38,6 +39,7 @@ public sealed class MxStatusProxyConverter
/// <summary>Converts an array of status objects, handling nulls gracefully.</summary>
/// <param name="statuses">Array of COM status objects; null returns empty list.</param>
/// <returns>The converted protobuf status messages, one per input element (null entries become an "Unknown" placeholder).</returns>
public IReadOnlyList<MxStatusProxy> ConvertMany(Array? statuses)
{
if (statuses is null)
@@ -67,6 +69,7 @@ public sealed class MxStatusProxyConverter
/// <summary>Preserves completion-only status bytes as a diagnostic hex string since they cannot be unpacked.</summary>
/// <param name="statusBytes">Status bytes to encode as hex string.</param>
/// <returns>The diagnostic hex-encoded representation of <paramref name="statusBytes"/>.</returns>
public string PreserveCompletionOnlyStatusBytes(byte[] statusBytes)
{
if (statusBytes is null)
@@ -270,7 +270,7 @@ public sealed class VariantConverter
// (a 64-bit 100-ns tick count since 1601). Only a genuine 64-bit source
// (long) can carry a valid full FILETIME; a uint can only hold the low
// 32 bits, which DateTime.FromFileTimeUtc would silently render as a
// near-1601 timestamp. For uint sources fall through to the integer
// timestamp near the 1601 epoch. For uint sources fall through to the integer
// projection rather than producing a bogus timestamp.
if (expectedDataType == MxDataType.Time && value is long)
{
@@ -10,6 +10,7 @@ public interface IWorkerPipeClient
/// <summary>Connects to the gateway and runs the worker until the session ends or is cancelled.</summary>
/// <param name="options">Configuration options.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default);
@@ -27,6 +27,7 @@ public sealed class WorkerFrameReader
/// <summary>Reads and validates a single length-prefixed frame from the stream.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>The validated <see cref="WorkerEnvelope"/> read from the stream.</returns>
public async Task<WorkerEnvelope> ReadAsync(CancellationToken cancellationToken = default)
{
byte[] lengthPrefix = new byte[sizeof(uint)];
@@ -28,6 +28,7 @@ public sealed class WorkerFrameWriter
/// <summary>Writes a worker envelope frame to the stream with length prefix.</summary>
/// <param name="envelope">Worker envelope to write.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task WriteAsync(
WorkerEnvelope envelope,
CancellationToken cancellationToken = default)
@@ -139,11 +139,7 @@ public sealed class WorkerPipeClient : IWorkerPipeClient
_connectAttemptTimeoutMilliseconds = connectAttemptTimeoutMilliseconds;
}
/// <summary>
/// Runs the worker by connecting to the gateway and executing the frame protocol.
/// </summary>
/// <param name="options">Worker configuration options.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <inheritdoc />
public async Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default)
@@ -106,15 +106,9 @@ public sealed class WorkerPipeSession
/// <summary>Runs the worker session, completing the handshake and processing messages until cancellation.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task RunAsync(CancellationToken cancellationToken = default)
{
// Worker-025: the factory delegate itself is null-checked in the
// constructor, but its return value is not — a factory that returned
// null would NRE on the StartAsync lambda below. Throw a diagnostic
// exception instead so the failure is unambiguous (and so the
// finally block's _runtimeSession?.Dispose() can't silently no-op
// on a torn half-initialized session). Mirrors the same pattern
// AlarmCommandHandler.Subscribe uses for its consumerFactory().
_runtimeSession = _runtimeSessionFactory()
?? throw new InvalidOperationException(
"Worker runtime session factory returned null.");
@@ -142,6 +136,7 @@ public sealed class WorkerPipeSession
/// <summary>Completes the gateway startup handshake using default MXAccess initialization.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task CompleteStartupHandshakeAsync(CancellationToken cancellationToken = default)
{
return CompleteStartupHandshakeAsync(InitializeMxAccessAsync, cancellationToken);
@@ -150,6 +145,7 @@ public sealed class WorkerPipeSession
/// <summary>Completes the gateway startup handshake with custom MXAccess initialization that returns void.</summary>
/// <param name="initializeMxAccessAsync">Async function to initialize MXAccess.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task CompleteStartupHandshakeAsync(
Func<CancellationToken, Task> initializeMxAccessAsync,
CancellationToken cancellationToken = default)
@@ -171,6 +167,7 @@ public sealed class WorkerPipeSession
/// <summary>Completes the gateway startup handshake with custom MXAccess initialization that returns WorkerReady.</summary>
/// <param name="initializeMxAccessAsync">Async function to initialize MXAccess and return ready state.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task CompleteStartupHandshakeAsync(
Func<CancellationToken, Task<WorkerReady>> initializeMxAccessAsync,
CancellationToken cancellationToken = default)
@@ -811,7 +808,7 @@ public sealed class WorkerPipeSession
/// command (e.g. <c>ReadBulk</c> waiting <c>timeout_ms</c> for the
/// first OnDataChange callback) freezes <c>LastActivityUtc</c> for the
/// duration of the wait even though the worker is healthy. To avoid
/// self-faulting a healthy in-flight command (Worker-017), the
/// self-faulting a healthy in-flight command, the
/// watchdog is suppressed while <c>CurrentCommandCorrelationId</c> is
/// non-empty — the worker already advertises the in-flight command on
/// each heartbeat, so the gateway has the signal it needs to decide
@@ -819,18 +816,6 @@ public sealed class WorkerPipeSession
/// STA (no command in flight and no activity), which is the only case
/// the watchdog can usefully distinguish from a slow command.
/// </summary>
/// <remarks>
/// Worker-023: the in-flight-command suppression is itself bounded by
/// <c>WorkerPipeSessionOptions.HeartbeatStuckCeiling</c>. A truly stuck
/// synchronous COM call (e.g. against a dead MXAccess provider whose
/// cross-apartment marshaler is permanently blocked) leaves
/// <c>CurrentCommandCorrelationId</c> non-empty forever; without an
/// upper bound the worker-side <c>StaHung</c> watchdog would be
/// permanently defeated and only the gateway's per-command timeout
/// would catch the hang. Once <c>LastActivityUtc</c> has been stale
/// for longer than <c>HeartbeatStuckCeiling</c> the watchdog fires
/// <c>StaHung</c> regardless of whether a command is in flight.
/// </remarks>
private async Task ReportWatchdogFaultIfNeededAsync(
WorkerRuntimeHeartbeatSnapshot snapshot,
CancellationToken cancellationToken)
@@ -852,12 +837,6 @@ public sealed class WorkerPipeSession
// point this branch stops being taken. The heartbeat already
// surfaces the in-flight correlation id so the gateway can apply
// its own per-command timeout if it considers the command too slow.
//
// Worker-023: once staleFor exceeds HeartbeatStuckCeiling we fall
// through to the fault path even with a command in flight — a
// truly stuck synchronous COM call would otherwise keep
// CurrentCommandCorrelationId non-empty indefinitely and the
// worker-side watchdog would never fire.
return;
}
@@ -35,7 +35,7 @@ public sealed class WorkerPipeSessionOptions
/// <summary>
/// Gets or sets the defensive upper bound on how long the watchdog
/// will suppress its <c>StaHung</c> fault while a command is in
/// flight. Worker-017 suppresses the watchdog when the heartbeat
/// flight. The watchdog suppresses itself when the heartbeat
/// snapshot's <c>CurrentCommandCorrelationId</c> is non-empty so a
/// legitimately slow command (e.g. <c>ReadBulk</c> against many
/// uncached tags) does not self-fault — but a truly stuck
@@ -65,7 +65,7 @@ public sealed class AlarmCommandHandler : IAlarmCommandHandler
}
/// <summary>
/// Worker-024: production constructor that also injects an
/// Production constructor that also injects an
/// STA-affinity guard. <paramref name="threadAffinityCheck"/> is
/// invoked at the entry of every method that touches the underlying
/// <see cref="IMxAccessAlarmConsumer"/> (or the wnwrap COM object
@@ -154,6 +154,7 @@ public sealed class AlarmDispatcher : IDisposable
/// <see cref="ActiveAlarmSnapshot"/> protos for the
/// <c>QueryActiveAlarms</c> RPC's ConditionRefresh stream.
/// </summary>
/// <returns>The currently active alarm snapshots.</returns>
public IReadOnlyList<ActiveAlarmSnapshot> SnapshotActiveAlarms()
{
if (disposed) throw new ObjectDisposedException(nameof(AlarmDispatcher));
@@ -24,6 +24,7 @@ public static class AlarmRecordTransitionMapper
/// <see cref="MxAlarmStateKind.Unspecified"/>.
/// </summary>
/// <param name="stateXml">The state XML string from AVEVA (e.g., UNACK_ALM, ACK_RTN).</param>
/// <returns>The parsed <see cref="MxAlarmStateKind"/>, or <see cref="MxAlarmStateKind.Unspecified"/> when unrecognized.</returns>
public static MxAlarmStateKind ParseStateKind(string? stateXml)
{
if (string.IsNullOrWhiteSpace(stateXml)) return MxAlarmStateKind.Unspecified;
@@ -51,6 +52,7 @@ public static class AlarmRecordTransitionMapper
/// </summary>
/// <param name="previous">The previous alarm state kind.</param>
/// <param name="current">The current alarm state kind.</param>
/// <returns>The proto <see cref="AlarmTransitionKind"/> that the state change represents.</returns>
public static AlarmTransitionKind MapTransition(
MxAlarmStateKind previous,
MxAlarmStateKind current)
@@ -87,6 +89,7 @@ public static class AlarmRecordTransitionMapper
/// <param name="providerName">The provider name, or null.</param>
/// <param name="groupName">The group name, or null.</param>
/// <param name="alarmName">The alarm name, or null.</param>
/// <returns>The composed <c>Provider!Group.AlarmName</c> reference string.</returns>
public static string ComposeFullReference(string? providerName, string? groupName, string? alarmName)
{
string provider = providerName ?? string.Empty;
@@ -113,6 +116,7 @@ public static class AlarmRecordTransitionMapper
/// <param name="xmlTime">e.g. <c>"13:26:14.709"</c>.</param>
/// <param name="gmtOffsetMinutes">Offset of the producer's local time vs UTC, in minutes.</param>
/// <param name="dstAdjustMinutes">DST adjustment already applied to local time, in minutes.</param>
/// <returns>The reassembled UTC timestamp, or <see cref="DateTime.MinValue"/> when parsing fails.</returns>
public static DateTime ParseTransitionTimestampUtc(
string? xmlDate,
string? xmlTime,
@@ -181,7 +185,7 @@ public static class AlarmRecordTransitionMapper
// GMTOFFSET = minutes east of UTC (or behind, depending on convention).
// The wnwrap convention observed: GMTOFFSET=240, DSTADJUST=0 for
// EDT (UTC-4) — so the field is "minutes from local to UTC". To get
// EDT (4 hours behind UTC) — so the field is "minutes from local to UTC". To get
// UTC, ADD the offset.
DateTime utc = localProducerTime.AddMinutes(gmtOffsetMinutes - dstAdjustMinutes);
return DateTime.SpecifyKind(utc, DateTimeKind.Utc);
@@ -84,7 +84,11 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
this.standby.AlarmTransitionEmitted += OnChildTransition;
}
/// <inheritdoc />
/// <summary>
/// Fires once per detected alarm-state transition, forwarded from whichever
/// child (primary or standby) is currently active. See "Active-child event
/// forwarding" in the type remarks for the forwarding rules.
/// </summary>
public event EventHandler<MxAlarmTransitionEvent>? AlarmTransitionEmitted;
/// <summary>
@@ -100,14 +104,6 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
public AlarmProviderMode Mode => mode;
/// <inheritdoc />
/// <remarks>
/// Arms BOTH children up front so the standby snapshot is warm at the
/// moment of failover. The standby is always subscribed even if the
/// primary's <c>Subscribe</c> throws; a standby subscribe failure is
/// surfaced (rethrown) but does not count toward primary failover. The
/// primary subscribe runs through the failure-counting wrapper so a
/// COM failure on subscribe contributes to the failover threshold.
/// </remarks>
public void Subscribe(string subscription)
{
if (disposed) throw new ObjectDisposedException(nameof(FailoverAlarmConsumer));
@@ -131,13 +127,6 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
}
/// <inheritdoc />
/// <remarks>
/// While the primary is active, drives <c>primary.PollOnce</c> through
/// the failure-counting wrapper. While degraded (standby active),
/// drives <c>standby.PollOnce</c> and then runs one failback probe per
/// call via <see cref="ProbeOnce"/> — the worker drives this on a
/// timer, so one degraded poll equals one probe tick.
/// </remarks>
public void PollOnce()
{
if (disposed) throw new ObjectDisposedException(nameof(FailoverAlarmConsumer));
@@ -318,7 +307,7 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
// snapshot below throws. A handler exception here must never escape the
// switch — escaping would (a) leave `active` flipped with no
// notification and (b) unwind into RunAlarmPollLoopAsync's trailing
// catch, which permanently stops alarm polling (Worker-026).
// catch, which permanently stops alarm polling.
RaiseModeChanged(AlarmProviderMode.Subtag, reason, hresult);
// Warm the standby snapshot for the gateway hand-off. The gateway
@@ -337,7 +326,7 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
cleanProbes = 0;
// Guarded so a ProviderModeChanged handler exception cannot escape into
// the STA poll loop and kill alarm delivery (Worker-026).
// the STA poll loop and kill alarm delivery.
RaiseModeChanged(AlarmProviderMode.Alarmmgr, reason, hresult);
}
@@ -375,7 +364,7 @@ public sealed class FailoverAlarmConsumer : IMxAccessAlarmConsumer
// AlarmCommandHandler's eventQueue.Enqueue hitting capacity). The
// switch itself has already taken effect; swallow so the failure
// cannot unwind into RunAlarmPollLoopAsync and permanently stop
// alarm polling (Worker-026). The event-queue overflow it most
// alarm polling. The event-queue overflow it most
// likely signals is already surfaced as a fault on the IPC path.
}
}
@@ -34,6 +34,7 @@ public interface IAlarmCommandHandler : IDisposable
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain name.</param>
/// <param name="operatorFullName">The operator full name.</param>
/// <returns>AVEVA's native alarm status code (0 = success).</returns>
int Acknowledge(
Guid alarmGuid,
string comment,
@@ -54,6 +55,7 @@ public interface IAlarmCommandHandler : IDisposable
/// <param name="operatorNode">The operator node name.</param>
/// <param name="operatorDomain">The operator domain name.</param>
/// <param name="operatorFullName">The operator full name.</param>
/// <returns>AVEVA's native alarm status code (0 = success).</returns>
int AcknowledgeByName(
string alarmName,
string providerName,
@@ -69,6 +71,7 @@ public interface IAlarmCommandHandler : IDisposable
/// prefix matched against <c>AlarmFullReference</c>.
/// </summary>
/// <param name="alarmFilterPrefix">Optional prefix to filter alarms by.</param>
/// <returns>The currently active alarms matching the filter.</returns>
IReadOnlyList<ActiveAlarmSnapshot> QueryActive(string? alarmFilterPrefix);
/// <summary>
@@ -101,6 +101,7 @@ public interface IMxAccessAlarmConsumer : IDisposable
/// ConditionRefresh path — operator clients call this after reconnect
/// to seed local Part 9 state.
/// </summary>
/// <returns>The most recently parsed snapshot of currently active alarms.</returns>
IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms();
/// <summary>
@@ -34,6 +34,7 @@ public interface IWorkerRuntimeSession : IDisposable
/// <summary>
/// Captures a heartbeat snapshot of the runtime state.
/// </summary>
/// <returns>The current heartbeat snapshot.</returns>
WorkerRuntimeHeartbeatSnapshot CaptureHeartbeat();
/// <summary>
@@ -46,6 +47,7 @@ public interface IWorkerRuntimeSession : IDisposable
/// <summary>
/// Drains a pending fault from the queue, if any.
/// </summary>
/// <returns>The pending fault, or <see langword="null"/> if none is queued.</returns>
WorkerFault? DrainFault();
/// <summary>
@@ -104,15 +104,10 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
this.registered = true;
}
/// <inheritdoc />
/// <summary>Raised when an advised subtag reports a new value.</summary>
public event EventHandler<SubtagValueChange>? ValueChanged;
/// <inheritdoc />
/// <remarks>
/// Idempotent per address: an address already advised is skipped. The
/// MXAccess COM object is created and the client registered on the first
/// call. Must be invoked on the worker's STA.
/// </remarks>
public void Advise(IReadOnlyCollection<string> itemAddresses)
{
if (itemAddresses is null)
@@ -145,12 +140,6 @@ public sealed class LmxSubtagAlarmSource : ISubtagAlarmSource
}
/// <inheritdoc />
/// <remarks>
/// Adds the item if it has not been added yet — the write may target a
/// subtag (e.g. the ack-comment) that was never advised — then writes
/// with MXAccess user id 0 (unsecured). Must be invoked on the worker's
/// STA.
/// </remarks>
public void Write(string itemAddress, object? value)
{
if (itemAddress is null)
@@ -84,11 +84,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
this.pumpStep = pumpStep ?? (static () => { });
}
/// <summary>
/// Executes an MXAccess command and returns the reply.
/// </summary>
/// <param name="command">STA command to execute.</param>
/// <returns>Command reply with result or error details.</returns>
/// <inheritdoc />
public MxCommandReply Execute(StaCommand command)
{
if (command is null)
@@ -37,6 +37,7 @@ public sealed class MxAccessEventMapper
/// <param name="quality">Item quality code from MXAccess.</param>
/// <param name="timestamp">Item timestamp from MXAccess.</param>
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
/// <returns>The mapped OnDataChange event.</returns>
public MxEvent CreateOnDataChange(
string sessionId,
int serverHandle,
@@ -65,6 +66,7 @@ public sealed class MxAccessEventMapper
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
/// <returns>The mapped OnWriteComplete event.</returns>
public MxEvent CreateOnWriteComplete(
string sessionId,
int serverHandle,
@@ -87,6 +89,7 @@ public sealed class MxAccessEventMapper
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
/// <returns>The mapped OperationComplete event.</returns>
public MxEvent CreateOperationComplete(
string sessionId,
int serverHandle,
@@ -133,6 +136,7 @@ public sealed class MxAccessEventMapper
/// The alarm provider that sourced this transition. Defaults to
/// <see cref="AlarmProviderMode.Alarmmgr"/> for the native path.
/// </param>
/// <returns>The mapped OnAlarmTransition event.</returns>
public MxEvent CreateOnAlarmTransition(
string sessionId,
string alarmFullReference,
@@ -194,6 +198,7 @@ public sealed class MxAccessEventMapper
/// <param name="reason">Human-readable reason for the switch.</param>
/// <param name="hresult">The COM HRESULT that triggered a failover, or 0 for a clean failback.</param>
/// <param name="atUtc">The UTC instant the switch occurred.</param>
/// <returns>The mapped OnAlarmProviderModeChanged event.</returns>
public MxEvent CreateOnAlarmProviderModeChanged(
string sessionId,
AlarmProviderMode mode,
@@ -228,6 +233,7 @@ public sealed class MxAccessEventMapper
/// <param name="quality">Array of quality values from MXAccess.</param>
/// <param name="timestamp">Array of timestamp values from MXAccess.</param>
/// <param name="statuses">Array of MxStatusProxy values from MXAccess.</param>
/// <returns>The mapped OnBufferedDataChange event.</returns>
public MxEvent CreateOnBufferedDataChange(
string sessionId,
int serverHandle,
@@ -148,6 +148,7 @@ public sealed class MxAccessEventQueue
/// Attempts to dequeue the next event without removing it if empty.
/// </summary>
/// <param name="workerEvent">The dequeued event if successful; null if queue is empty.</param>
/// <returns><see langword="true"/> if an event was dequeued; <see langword="false"/> if the queue was empty.</returns>
public bool TryDequeue(out WorkerEvent? workerEvent)
{
lock (syncRoot)
@@ -167,6 +168,7 @@ public sealed class MxAccessEventQueue
/// Drains up to maxEvents from the queue; if maxEvents is 0, drains all events.
/// </summary>
/// <param name="maxEvents">Maximum number of events to drain; 0 means drain all.</param>
/// <returns>The drained events, in enqueue order.</returns>
public IReadOnlyList<WorkerEvent> Drain(uint maxEvents)
{
lock (syncRoot)
@@ -209,6 +211,7 @@ public sealed class MxAccessEventQueue
/// <summary>
/// Returns and clears the fault so it is not reported twice.
/// </summary>
/// <returns>The recorded <see cref="WorkerFault"/>, or <see langword="null"/> if none was recorded or it was already drained.</returns>
public WorkerFault? DrainFault()
{
lock (syncRoot)
@@ -66,6 +66,7 @@ public sealed class MxAccessHandleRegistry
/// <summary>Checks if the registry contains the specified server handle.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <returns><see langword="true"/> if the server handle is registered; otherwise, <see langword="false"/>.</returns>
public bool ContainsServerHandle(int serverHandle)
{
return serverHandles.ContainsKey(serverHandle);
@@ -106,6 +107,7 @@ public sealed class MxAccessHandleRegistry
/// <summary>Checks if the registry contains the specified item handle.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <returns><see langword="true"/> if the item handle is registered; otherwise, <see langword="false"/>.</returns>
public bool ContainsItemHandle(
int serverHandle,
int itemHandle)
@@ -149,6 +151,7 @@ public sealed class MxAccessHandleRegistry
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="adviceKind">Type of advice to check.</param>
/// <returns><see langword="true"/> if the advice handle is registered; otherwise, <see langword="false"/>.</returns>
public bool ContainsAdviceHandle(
int serverHandle,
int itemHandle,
@@ -47,6 +47,7 @@ public sealed class MxAccessSession : IDisposable
/// <summary>Creates a WorkerReady message with session metadata.</summary>
/// <param name="workerProcessId">Process ID of the worker.</param>
/// <returns>The populated <see cref="WorkerReady"/> message.</returns>
public WorkerReady CreateWorkerReady(int workerProcessId)
{
return new WorkerReady
@@ -70,13 +71,14 @@ public sealed class MxAccessSession : IDisposable
/// the production sink wired by <see cref="Create"/> — because the
/// <c>new object()</c> stand-in this factory uses for the COM object
/// would silently bypass <see cref="System.Runtime.InteropServices.Marshal.IsComObject(object)"/>
/// during disposal and mask lifetime regressions (Worker.Tests-026).
/// during disposal and mask lifetime regressions.
/// </summary>
/// <param name="mxAccessServer">The server abstraction to drive.</param>
/// <param name="eventSink">The event sink to attach to the session.</param>
/// <param name="handleRegistry">Optional handle registry; a fresh one is created when null.</param>
/// <param name="valueCache">Optional value cache; a fresh one is created when null.</param>
/// <param name="creationThreadId">Optional creation thread id; defaults to the current managed thread id.</param>
/// <returns>A test-only <see cref="MxAccessSession"/> backed by the supplied test doubles.</returns>
/// <exception cref="ArgumentException">
/// Thrown when <paramref name="eventSink"/> is the production
/// <see cref="MxAccessBaseEventSink"/>. Tests must pass a test
@@ -110,6 +112,7 @@ public sealed class MxAccessSession : IDisposable
/// <param name="factory">Factory to create the MXAccess COM object.</param>
/// <param name="eventSink">Event sink to attach to the COM object.</param>
/// <param name="sessionId">Identifier of the session.</param>
/// <returns>The initialized <see cref="MxAccessSession"/>.</returns>
public static MxAccessSession Create(
IMxAccessComObjectFactory factory,
IMxAccessEventSink eventSink,
@@ -176,6 +179,7 @@ public sealed class MxAccessSession : IDisposable
/// <summary>Registers a client with MXAccess and returns the server handle.</summary>
/// <param name="clientName">Name of the client to register.</param>
/// <returns>The server handle assigned to the registered client.</returns>
public int Register(string clientName)
{
ThrowIfDisposed();
@@ -199,6 +203,7 @@ public sealed class MxAccessSession : IDisposable
/// <summary>Adds an item to an MXAccess server and returns the item handle.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemDefinition">Definition or address of the item to add.</param>
/// <returns>The item handle assigned to the added item.</returns>
public int AddItem(
int serverHandle,
string itemDefinition)
@@ -220,6 +225,7 @@ public sealed class MxAccessSession : IDisposable
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemDefinition">Definition or address of the item to add.</param>
/// <param name="itemContext">Context string for the item.</param>
/// <returns>The item handle assigned to the added item.</returns>
public int AddItem2(
int serverHandle,
string itemDefinition,
@@ -358,6 +364,7 @@ public sealed class MxAccessSession : IDisposable
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemDefinition">Definition or address of the item to add.</param>
/// <param name="itemContext">Context string for the item.</param>
/// <returns>The item handle assigned to the added item.</returns>
public int AddBufferedItem(
int serverHandle,
string itemDefinition,
@@ -463,6 +470,7 @@ public sealed class MxAccessSession : IDisposable
/// <summary>Adds multiple items in bulk, returning success/failure results.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="tagAddresses">Enumerable of item definitions to add.</param>
/// <returns>The per-item success/failure results, in the order the tags were provided.</returns>
public IReadOnlyList<SubscribeResult> AddItemBulk(
int serverHandle,
IEnumerable<string> tagAddresses)
@@ -499,6 +507,7 @@ public sealed class MxAccessSession : IDisposable
/// <summary>Advises on multiple items in bulk, returning success/failure results.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandles">Enumerable of item handles to advise on.</param>
/// <returns>The per-item success/failure results, in the order the handles were provided.</returns>
public IReadOnlyList<SubscribeResult> AdviseItemBulk(
int serverHandle,
IEnumerable<int> itemHandles)
@@ -529,6 +538,7 @@ public sealed class MxAccessSession : IDisposable
/// <summary>Removes multiple items in bulk, returning success/failure results.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandles">Enumerable of item handles to remove.</param>
/// <returns>The per-item success/failure results, in the order the handles were provided.</returns>
public IReadOnlyList<SubscribeResult> RemoveItemBulk(
int serverHandle,
IEnumerable<int> itemHandles)
@@ -559,6 +569,7 @@ public sealed class MxAccessSession : IDisposable
/// <summary>Removes advice subscriptions from multiple items in bulk, returning success/failure results.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandles">Enumerable of item handles to unadvise.</param>
/// <returns>The per-item success/failure results, in the order the handles were provided.</returns>
public IReadOnlyList<SubscribeResult> UnAdviseItemBulk(
int serverHandle,
IEnumerable<int> itemHandles)
@@ -589,6 +600,7 @@ public sealed class MxAccessSession : IDisposable
/// <summary>Adds multiple items and subscribes to them in bulk, returning success/failure results.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="tagAddresses">Enumerable of item definitions to add and subscribe to.</param>
/// <returns>The per-item success/failure results, in the order the tags were provided.</returns>
public IReadOnlyList<SubscribeResult> SubscribeBulk(
int serverHandle,
IEnumerable<string> tagAddresses)
@@ -633,6 +645,7 @@ public sealed class MxAccessSession : IDisposable
/// <summary>Unsubscribes from multiple items in bulk, returning success/failure results.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandles">Enumerable of item handles to unsubscribe from.</param>
/// <returns>The per-item success/failure results, in the order the handles were provided.</returns>
public IReadOnlyList<SubscribeResult> UnsubscribeBulk(
int serverHandle,
IEnumerable<int> itemHandles)
@@ -684,6 +697,7 @@ public sealed class MxAccessSession : IDisposable
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="entries">The write entries to process.</param>
/// <param name="convertValue">Converts protobuf MxValue to COM-compatible variant.</param>
/// <returns>The per-entry write results, in the order the entries were provided.</returns>
public IReadOnlyList<BulkWriteResult> WriteBulk(
int serverHandle,
IReadOnlyList<WriteBulkEntry> entries,
@@ -716,6 +730,7 @@ public sealed class MxAccessSession : IDisposable
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="entries">The write2 entries to process.</param>
/// <param name="convertValue">Converts protobuf MxValue to COM-compatible variant.</param>
/// <returns>The per-entry write results, in the order the entries were provided.</returns>
public IReadOnlyList<BulkWriteResult> Write2Bulk(
int serverHandle,
IReadOnlyList<Write2BulkEntry> entries,
@@ -753,6 +768,7 @@ public sealed class MxAccessSession : IDisposable
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="entries">The WriteSecured entries to process.</param>
/// <param name="convertValue">Converts protobuf MxValue to COM-compatible variant.</param>
/// <returns>The per-entry write results, in the order the entries were provided.</returns>
public IReadOnlyList<BulkWriteResult> WriteSecuredBulk(
int serverHandle,
IReadOnlyList<WriteSecuredBulkEntry> entries,
@@ -790,6 +806,7 @@ public sealed class MxAccessSession : IDisposable
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="entries">The WriteSecured2 entries to process.</param>
/// <param name="convertValue">Converts protobuf MxValue to COM-compatible variant.</param>
/// <returns>The per-entry write results, in the order the entries were provided.</returns>
public IReadOnlyList<BulkWriteResult> WriteSecured2Bulk(
int serverHandle,
IReadOnlyList<WriteSecured2BulkEntry> entries,
@@ -838,6 +855,7 @@ public sealed class MxAccessSession : IDisposable
/// <param name="tagAddresses">The tag addresses to read.</param>
/// <param name="timeout">The timeout per tag.</param>
/// <param name="pumpStep">Action invoked on each poll iteration.</param>
/// <returns>The per-tag read results, in the order the tags were provided.</returns>
public IReadOnlyList<BulkReadResult> ReadBulk(
int serverHandle,
IReadOnlyList<string> tagAddresses,
@@ -1079,6 +1097,7 @@ public sealed class MxAccessSession : IDisposable
}
/// <summary>Gracefully shuts down the session, cleaning up all handles.</summary>
/// <returns>The shutdown result, including any per-handle cleanup failures.</returns>
public MxAccessShutdownResult ShutdownGracefully()
{
if (disposed)
@@ -1096,7 +1115,7 @@ public sealed class MxAccessSession : IDisposable
return new MxAccessShutdownResult(failures);
}
/// <summary>Releases the MXAccess COM object and resources.</summary>
/// <inheritdoc />
public void Dispose()
{
if (disposed)
@@ -17,15 +17,6 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
private readonly IMxAccessEventSink eventSink;
private readonly MxAccessEventQueue eventQueue;
private readonly StaRuntime staRuntime;
// Worker-024: the factory takes an Action so MxAccessStaSession can hand
// the alarm handler its STA-affinity guard (a closure over
// alarmConsumerThreadId captured at the factory call site). The handler
// then invokes the guard at the entry of every method that touches the
// wnwrap consumer, matching the STA-affinity invariant already enforced
// for the poll path via EnsureOnAlarmConsumerThread.
// Worker-9: the third arg is the session's IMxAccessComObjectFactory, so
// the handler can build the subtag-fallback source's own proxy-server COM
// object on this STA when a subscribe selects the subtag / failover path.
private readonly Func<MxAccessEventQueue, Action, IMxAccessComObjectFactory, IAlarmCommandHandler>? alarmCommandHandlerFactory;
private StaCommandDispatcher? commandDispatcher;
private MxAccessSession? session;
@@ -171,13 +162,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
return StartAsync(string.Empty, workerProcessId, cancellationToken);
}
/// <summary>
/// Starts the MXAccess COM session with a session ID asynchronously.
/// </summary>
/// <param name="sessionId">Session identifier.</param>
/// <param name="workerProcessId">Worker process identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Worker ready message.</returns>
/// <inheritdoc />
public async Task<WorkerReady> StartAsync(
string sessionId,
int workerProcessId,
@@ -204,20 +189,6 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
// thread id; RunAlarmPollLoopAsync then asserts each
// PollOnce executes on the same thread.
alarmConsumerThreadId = Environment.CurrentManagedThreadId;
// Worker-024: hand the handler an affinity guard so each
// of its command-path entries (Subscribe / Acknowledge /
// AcknowledgeByName / QueryActive / Unsubscribe / PollOnce)
// asserts the same STA-affinity invariant the poll path
// already enforced. Without this the command path relied
// on convention alone; a future refactor that let a
// command run off-STA would silently deadlock on
// cross-apartment marshaling against the wnwrap consumer.
// Worker-9: the factory also receives the session's
// IMxAccessComObjectFactory so the subtag-fallback source
// (LmxSubtagAlarmSource) can create its OWN proxy-server COM
// object on this STA, isolated from the item pipeline's
// MxAccessSession. The factory call runs on the STA, so the
// resulting source is bound to the correct apartment.
alarmCommandHandler = alarmCommandHandlerFactory(
eventQueue,
EnsureOnAlarmConsumerThread,
@@ -295,7 +266,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
// STA runtime shutting down — stop the loop gracefully.
// The dedicated shutdown type lets us distinguish this
// graceful-stop signal from the STA-affinity assertion
// raised by EnsureOnAlarmConsumerThread (Worker-008),
// raised by EnsureOnAlarmConsumerThread,
// which is also an InvalidOperationException but signals
// a programming-error regression — that case falls through
// to the generic Exception arm below and is recorded as a
@@ -378,11 +349,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
return fault;
}
/// <summary>
/// Dispatches a command to the STA thread for execution asynchronously.
/// </summary>
/// <param name="command">The command to dispatch.</param>
/// <returns>Command reply.</returns>
/// <inheritdoc />
public Task<MxCommandReply> DispatchAsync(StaCommand command)
{
if (commandDispatcher is null)
@@ -393,10 +360,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
return commandDispatcher.DispatchAsync(command);
}
/// <summary>
/// Captures a heartbeat snapshot of the session's runtime state.
/// </summary>
/// <returns>Heartbeat snapshot.</returns>
/// <inheritdoc />
public WorkerRuntimeHeartbeatSnapshot CaptureHeartbeat()
{
uint pendingCommandCount = 0;
@@ -416,38 +380,25 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
currentCommandCorrelationId);
}
/// <summary>
/// Requests graceful shutdown of the command dispatcher.
/// </summary>
/// <inheritdoc />
public void RequestShutdown()
{
commandDispatcher?.RequestShutdown();
}
/// <summary>
/// Drains up to the specified number of events from the queue.
/// </summary>
/// <param name="maxEvents">Maximum events to drain.</param>
/// <returns>Drained events.</returns>
/// <inheritdoc />
public IReadOnlyList<WorkerEvent> DrainEvents(uint maxEvents)
{
return eventQueue.Drain(maxEvents);
}
/// <summary>
/// Drains a fault from the queue if present.
/// </summary>
/// <returns>Drained fault or null.</returns>
/// <inheritdoc />
public WorkerFault? DrainFault()
{
return eventQueue.DrainFault();
}
/// <summary>
/// Cancels a queued command by correlation ID.
/// </summary>
/// <param name="correlationId">Correlation ID of the command to cancel.</param>
/// <returns>True if cancelled; otherwise false.</returns>
/// <inheritdoc />
public bool CancelCommand(string correlationId)
{
return commandDispatcher?.CancelQueuedCommand(correlationId) ?? false;
@@ -507,12 +458,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
cancellationToken);
}
/// <summary>
/// Performs graceful shutdown of the MXAccess session within a timeout.
/// </summary>
/// <param name="timeout">Maximum time allowed for shutdown.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Shutdown result with any cleanup failures.</returns>
/// <inheritdoc />
public async Task<MxAccessShutdownResult> ShutdownGracefullyAsync(
TimeSpan timeout,
CancellationToken cancellationToken = default)
@@ -620,7 +566,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
return result;
}
/// <summary>Releases resources and shuts down the session.</summary>
/// <inheritdoc />
public void Dispose()
{
if (disposed)
@@ -60,6 +60,7 @@ public sealed class MxAccessValueCache
/// <param name="serverHandle">MXAccess server handle.</param>
/// <param name="itemHandle">MXAccess item handle.</param>
/// <param name="value">The cached value if found.</param>
/// <returns><see langword="true"/> if a cached value exists for the handle pair; otherwise <see langword="false"/>.</returns>
public bool TryGet(
int serverHandle,
int itemHandle,
@@ -102,6 +103,7 @@ public sealed class MxAccessValueCache
/// <param name="pumpStep">Action that pumps any pending Windows messages.</param>
/// <param name="value">The cached value if the update was received before the deadline.</param>
/// <param name="pollIntervalMs">How long to sleep between pump cycles. Default 5 ms.</param>
/// <returns><see langword="true"/> if an update newer than <paramref name="sinceVersion"/> arrived before the deadline; otherwise <see langword="false"/>.</returns>
public bool TryWaitForUpdate(
int serverHandle,
int itemHandle,
@@ -137,6 +139,7 @@ public sealed class MxAccessValueCache
/// <summary>Returns the current version for a handle pair, or 0 if no entry exists.</summary>
/// <param name="serverHandle">MXAccess server handle.</param>
/// <param name="itemHandle">MXAccess item handle.</param>
/// <returns>The current cache entry version, or 0 if no entry exists.</returns>
public ulong CurrentVersion(
int serverHandle,
int itemHandle)
@@ -77,18 +77,14 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
this.valueChangedHandler = OnValueChanged;
}
/// <summary>
/// Advises every alarm subtag (active / acked / priority, plus the
/// ack-comment subtag) and begins listening for value changes. The
/// ack-comment subtag is advised so it is an active MXAccess item by the
/// time <see cref="AcknowledgeByName"/> writes it — MXAccess rejects a
/// write to an added-but-not-advised item with E_INVALIDARG (confirmed
/// against live MXAccess). Its value changes carry no transition (the
/// state machine ignores addresses it does not map to active/acked).
/// The <paramref name="subscription"/> expression is ignored: the subtag
/// set is fixed by the watch list.
/// </summary>
/// <param name="subscription">The subscription expression (unused in subtag mode).</param>
/// <inheritdoc />
/// <remarks>
/// The <paramref name="subscription"/> expression is ignored — the subtag
/// set is fixed by the watch list. Also advises the ack-comment subtag so
/// it is an active MXAccess item by the time <see cref="AcknowledgeByName"/>
/// writes it; MXAccess rejects a write to an added-but-not-advised item
/// with E_INVALIDARG.
/// </remarks>
public void Subscribe(string subscription)
{
if (disposed)
@@ -114,17 +110,12 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
}
}
/// <summary>
/// Acknowledges an alarm by its synthetic GUID. Resolves the GUID back
/// to its alarm full reference and delegates to the by-name write path.
/// </summary>
/// <param name="alarmGuid">The synthetic alarm GUID.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name (unused in subtag mode).</param>
/// <param name="ackOperatorNode">The operator node (unused in subtag mode).</param>
/// <param name="ackOperatorDomain">The operator domain (unused in subtag mode).</param>
/// <param name="ackOperatorFullName">The operator full name (unused in subtag mode).</param>
/// <returns>0 on success; non-zero when the GUID or ack-comment subtag is unknown.</returns>
/// <inheritdoc />
/// <remarks>
/// Resolves the synthetic GUID back to its alarm full reference and
/// delegates to the by-name write path; operator-identity arguments are
/// not surfaced through the subtag write.
/// </remarks>
public int AcknowledgeByGuid(
Guid alarmGuid,
string ackComment,
@@ -147,21 +138,12 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
return WriteAckComment(target, ackComment);
}
/// <summary>
/// Acknowledges an alarm by its (name, provider, group) tuple. In subtag
/// mode the comment is written to the target's writable ack-comment
/// subtag; the operator-identity arguments are not surfaced through the
/// subtag write.
/// </summary>
/// <param name="alarmName">The alarm name (object-rooted tag name as the dispatcher derives it).</param>
/// <param name="providerName">The provider name used to recompose the full reference for lookup.</param>
/// <param name="groupName">The group name used to recompose the full reference for lookup.</param>
/// <param name="ackComment">The acknowledgment comment.</param>
/// <param name="ackOperatorName">The operator name (unused in subtag mode).</param>
/// <param name="ackOperatorNode">The operator node (unused in subtag mode).</param>
/// <param name="ackOperatorDomain">The operator domain (unused in subtag mode).</param>
/// <param name="ackOperatorFullName">The operator full name (unused in subtag mode).</param>
/// <returns>0 on success; non-zero when no target matches or it lacks an ack-comment subtag.</returns>
/// <inheritdoc />
/// <remarks>
/// In subtag mode the comment is written to the target's writable
/// ack-comment subtag; the operator-identity arguments are not
/// surfaced through the subtag write.
/// </remarks>
public int AcknowledgeByName(
string alarmName,
string providerName,
@@ -186,12 +168,11 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
return WriteAckComment(target, ackComment);
}
/// <summary>
/// Returns the state machine's currently-active alarm snapshot, with
/// each record stamped <see cref="MxAlarmSnapshotRecord.Degraded"/> and
/// assigned its synthetic GUID.
/// </summary>
/// <returns>The active alarm snapshot records.</returns>
/// <inheritdoc />
/// <remarks>
/// Each returned record is stamped <see cref="MxAlarmSnapshotRecord.Degraded"/>
/// and assigned its synthetic GUID.
/// </remarks>
public IReadOnlyList<MxAlarmSnapshotRecord> SnapshotActiveAlarms()
{
IReadOnlyList<MxAlarmSnapshotRecord> records = stateMachine.SnapshotActive();
@@ -203,9 +184,8 @@ public sealed class SubtagAlarmConsumer : IMxAccessAlarmConsumer
return records;
}
/// <summary>
/// No-op: the subtag path is event-driven and owns no poll cadence.
/// </summary>
/// <inheritdoc />
/// <remarks>No-op: the subtag path is event-driven and owns no poll cadence.</remarks>
public void PollOnce()
{
// Subtag mode is event-driven; value changes arrive via the source's
@@ -274,14 +274,19 @@ public sealed class SubtagAlarmStateMachine
private readonly struct SubtagBinding
{
/// <summary>Pairs a bound subtag address with the alarm state it feeds and the role it plays.</summary>
/// <param name="state">The alarm state that this subtag's value changes update.</param>
/// <param name="role">The role the bound subtag plays (active, acked, or priority).</param>
public SubtagBinding(AlarmState state, SubtagRole role)
{
State = state;
Role = role;
}
/// <summary>Gets the alarm state that this subtag's value changes update.</summary>
public AlarmState State { get; }
/// <summary>Gets the role the bound subtag plays (active, acked, or priority).</summary>
public SubtagRole Role { get; }
}
@@ -291,13 +296,17 @@ public sealed class SubtagAlarmStateMachine
private readonly string _group;
private readonly string _tagName;
/// <summary>Initializes the tracked state for one alarm target, deriving its provider/group/tag name.</summary>
/// <param name="target">The configured alarm subtag target this state tracks.</param>
public AlarmState(AlarmSubtagTarget target)
{
(_providerName, _group, _tagName) = DeriveNameParts(target);
}
/// <summary>Gets or sets whether the alarm is currently active (raised).</summary>
public bool Active { get; set; }
/// <summary>Gets or sets whether the alarm is currently acknowledged.</summary>
public bool Acked { get; set; }
/// <summary>
@@ -307,11 +316,14 @@ public sealed class SubtagAlarmStateMachine
/// </summary>
public bool AckedDuringEpisode { get; set; }
/// <summary>Gets or sets the alarm's priority, as last reported by the priority subtag.</summary>
public int Priority { get; set; }
/// <summary>Gets or sets the UTC timestamp at which the alarm was most recently raised.</summary>
public DateTime FirstRaiseUtc { get; set; }
/// <summary>Derives the current alarm state from the tracked flags, before a transition is applied.</summary>
/// <returns>The alarm state kind (unspecified, unacknowledged, or acknowledged) implied by the tracked flags.</returns>
public MxAlarmStateKind DerivedState()
{
if (!Active)
@@ -322,6 +334,10 @@ public sealed class SubtagAlarmStateMachine
return Acked ? MxAlarmStateKind.AckAlm : MxAlarmStateKind.UnackAlm;
}
/// <summary>Builds a snapshot record for this alarm at the given state kind and timestamp.</summary>
/// <param name="kind">The alarm state kind to record.</param>
/// <param name="timestampUtc">The UTC timestamp of the transition being recorded.</param>
/// <returns>The snapshot record describing this alarm's provider, name, priority, state, and timestamp.</returns>
public MxAlarmSnapshotRecord BuildRecord(MxAlarmStateKind kind, DateTime timestampUtc)
{
return new MxAlarmSnapshotRecord
@@ -86,7 +86,11 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
: DefaultMaxAlarmsPerFetch;
}
/// <inheritdoc />
/// <summary>
/// Fires once per detected alarm-state transition (raise, acknowledge,
/// clear, or new-alarm-already-acked-on-arrival), dispatched from
/// <see cref="PollOnce"/> on the calling (STA) thread.
/// </summary>
public event EventHandler<MxAlarmTransitionEvent>? AlarmTransitionEmitted;
/// <inheritdoc />
@@ -112,8 +116,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
// only path that lets AlarmAckByName succeed afterwards. The
// v2 Initialize/Register/Subscribe methods on the class
// succeed (return 0) but acks against that consumer state
// return -55. The v1 prefix path is what WIN-911-style code
// uses against the same wnwrap library.
// return -55.
int init = com.IwwAlarmConsumer_InitializeConsumer(DefaultApplicationName);
if (init != 0)
{
@@ -297,15 +300,15 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
}
}
/// <summary>
/// Synchronously poll the wnwrap consumer once and dispatch any
/// transitions. STA-bound hosts drive polling by calling this from
/// <inheritdoc />
/// <remarks>
/// STA-bound hosts drive polling by calling this from
/// the thread that owns the COM object. The consumer deliberately
/// owns no internal timer: a thread-pool timer would call the
/// apartment-threaded COM object off its owning STA and can block
/// indefinitely on cross-apartment marshaling when the STA is not
/// pumping messages.
/// </summary>
/// </remarks>
public void PollOnce()
{
wwAlarmConsumerClass? com;
@@ -350,7 +353,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// <see cref="PollOnce"/> after a successful
/// <c>GetXmlCurrentAlarms2</c> call; exposed as <c>internal static</c>
/// so the diff rules can be unit-tested without driving the
/// wnwrapConsumer COM object (Worker.Tests-022).
/// wnwrapConsumer COM object.
/// </summary>
/// <remarks>
/// <para>Rules:</para>
@@ -396,6 +399,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// resync).
/// </summary>
/// <param name="xml">The XML snapshot payload.</param>
/// <returns>The parsed alarm snapshot records, keyed by alarm GUID.</returns>
public static Dictionary<Guid, MxAlarmSnapshotRecord> ParseSnapshotXml(string xml)
{
Dictionary<Guid, MxAlarmSnapshotRecord> records =
@@ -460,6 +464,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// </summary>
/// <param name="hex">The 32-character hex GUID string.</param>
/// <param name="guid">The parsed GUID, or Empty if parsing fails.</param>
/// <returns><see langword="true"/> if <paramref name="hex"/> was successfully parsed; otherwise, <see langword="false"/>.</returns>
public static bool TryParseHexGuid(string? hex, out Guid guid)
{
guid = Guid.Empty;
@@ -487,6 +492,7 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
/// so the worker doesn't fail to start.
/// </summary>
/// <param name="subscription">The subscription expression.</param>
/// <returns>The XML query payload to pass to <c>SetXmlAlarmQuery</c>.</returns>
internal static string ComposeXmlAlarmQuery(string subscription)
{
string node = Environment.MachineName;
@@ -9,7 +9,7 @@ public sealed class StaComApartmentInitializer : IStaComApartmentInitializer
private const int SOk = 0;
private const int SFalse = 1;
/// <summary>Initializes the COM apartment in single-threaded mode.</summary>
/// <inheritdoc />
public void Initialize()
{
int hresult = CoInitializeEx(IntPtr.Zero, CoInitializeApartmentThreaded);
@@ -19,7 +19,7 @@ public sealed class StaComApartmentInitializer : IStaComApartmentInitializer
}
}
/// <summary>Uninitializes the COM apartment.</summary>
/// <inheritdoc />
public void Uninitialize()
{
CoUninitialize();
@@ -43,6 +43,7 @@ public sealed class StaMessagePump
}
/// <summary>Pumps and dispatches all pending Windows messages, returning the count processed.</summary>
/// <returns>The number of messages pumped and dispatched.</returns>
public int PumpPendingMessages()
{
int pumpedMessages = 0;
@@ -86,6 +86,7 @@ public sealed class StaRuntime : IDisposable
/// wait. Callers must already be on the STA; the method is otherwise
/// safe (PeekMessage simply finds no messages).
/// </summary>
/// <returns>The number of messages pumped.</returns>
public int PumpPendingMessages() => messagePump.PumpPendingMessages();
/// <summary>
@@ -212,9 +213,7 @@ public sealed class StaRuntime : IDisposable
return stopped;
}
/// <summary>
/// Releases resources used by the STA runtime.
/// </summary>
/// <inheritdoc />
public void Dispose()
{
if (disposed)
@@ -41,7 +41,7 @@ internal sealed class StaWorkItem<T> : IStaWorkItem
private TaskCompletionSource<T> Completion { get; }
/// <summary>Cancels the work item before execution begins.</summary>
/// <inheritdoc />
public void CancelBeforeExecution()
{
if (Interlocked.CompareExchange(ref started, 1, 0) == 0)
@@ -51,7 +51,7 @@ internal sealed class StaWorkItem<T> : IStaWorkItem
}
}
/// <summary>Executes the work item command.</summary>
/// <inheritdoc />
public void Execute()
{
if (Interlocked.CompareExchange(ref started, 1, 0) != 0)
@@ -11,6 +11,7 @@ public static class WorkerApplication
{
/// <summary>Initializes and runs the worker with default environment and logging.</summary>
/// <param name="args">Command-line arguments.</param>
/// <returns>The process exit code.</returns>
public static int Run(string[] args)
{
return Run(
@@ -23,6 +24,7 @@ public static class WorkerApplication
/// <param name="args">Command-line arguments.</param>
/// <param name="environment">Worker environment for resolving configuration.</param>
/// <param name="logger">Worker logger for diagnostics.</param>
/// <returns>The process exit code.</returns>
public static int Run(
string[] args,
IWorkerEnvironment environment,
@@ -40,6 +42,7 @@ public static class WorkerApplication
/// <param name="environment">Worker environment for resolving configuration.</param>
/// <param name="logger">Worker logger for diagnostics.</param>
/// <param name="pipeClient">Named pipe client for gateway communication.</param>
/// <returns>The process exit code.</returns>
public static int Run(
string[] args,
IWorkerEnvironment environment,