Add XML documentation across gateway, worker, and .NET client

This commit is contained in:
Joseph Doherty
2026-04-30 11:49:58 -04:00
parent 4731ab535c
commit eed1e88a37
269 changed files with 4555 additions and 13 deletions
@@ -2,8 +2,12 @@ using System;
namespace MxGateway.Worker.Bootstrap;
/// <summary>
/// Worker environment that reads from system environment variables.
/// </summary>
public sealed class EnvironmentVariableWorkerEnvironment : IWorkerEnvironment
{
/// <inheritdoc />
public string? GetEnvironmentVariable(string name)
{
return Environment.GetEnvironmentVariable(name);
@@ -1,6 +1,14 @@
namespace MxGateway.Worker.Bootstrap;
/// <summary>
/// Abstracts access to environment variables for the worker.
/// </summary>
public interface IWorkerEnvironment
{
/// <summary>
/// Gets an environment variable by name.
/// </summary>
/// <param name="name">Name of the environment variable.</param>
/// <returns>The value of the environment variable, or null if not found.</returns>
string? GetEnvironmentVariable(string name);
}
@@ -4,7 +4,17 @@ namespace MxGateway.Worker.Bootstrap;
public interface IWorkerLogger
{
/// <summary>
/// Logs an informational event with fields.
/// </summary>
/// <param name="eventName">Event name.</param>
/// <param name="fields">Event fields.</param>
void Information(string eventName, IReadOnlyDictionary<string, object?> fields);
/// <summary>
/// Logs an error event with fields.
/// </summary>
/// <param name="eventName">Event name.</param>
/// <param name="fields">Event fields.</param>
void Error(string eventName, IReadOnlyDictionary<string, object?> fields);
}
@@ -15,19 +15,42 @@ public sealed class WorkerBootstrapResult
Errors = errors;
}
/// <summary>
/// Gets the worker process exit code.
/// </summary>
public WorkerExitCode ExitCode { get; }
/// <summary>
/// Gets the bootstrap options if bootstrap succeeded.
/// </summary>
public WorkerOptions? Options { get; }
/// <summary>
/// Gets the list of bootstrap errors if any.
/// </summary>
public IReadOnlyList<string> Errors { get; }
/// <summary>
/// Gets a value indicating whether bootstrap succeeded.
/// </summary>
public bool Succeeded => ExitCode == WorkerExitCode.Success;
/// <summary>
/// Creates a successful bootstrap result with the given options.
/// </summary>
/// <param name="options">Bootstrap options.</param>
/// <returns>Successful bootstrap result.</returns>
public static WorkerBootstrapResult Success(WorkerOptions options)
{
return new WorkerBootstrapResult(WorkerExitCode.Success, options, []);
}
/// <summary>
/// Creates a failed bootstrap result with the given exit code and errors.
/// </summary>
/// <param name="exitCode">Worker exit code.</param>
/// <param name="errors">Bootstrap errors.</param>
/// <returns>Failed bootstrap result.</returns>
public static WorkerBootstrapResult Failure(WorkerExitCode exitCode, IEnumerable<string> errors)
{
return new WorkerBootstrapResult(exitCode, null, errors.ToArray());
@@ -9,16 +9,24 @@ public sealed class WorkerConsoleLogger : IWorkerLogger
{
private readonly TextWriter _writer;
/// <summary>Initializes a new worker console logger.</summary>
/// <param name="writer">Text writer destination for log output.</param>
public WorkerConsoleLogger(TextWriter writer)
{
_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>
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>
public void Error(string eventName, IReadOnlyDictionary<string, object?> fields)
{
Write("Error", eventName, fields);
@@ -3,8 +3,14 @@ using System.Collections.Generic;
namespace MxGateway.Worker.Bootstrap;
/// <summary>
/// Redacts sensitive fields from worker log messages.
/// </summary>
public static class WorkerLogRedactor
{
/// <summary>
/// Replacement text for redacted values.
/// </summary>
public const string RedactedValue = "[redacted]";
private static readonly string[] SensitiveFieldNameParts =
@@ -18,6 +24,10 @@ public static class WorkerLogRedactor
"api_key",
];
/// <summary>
/// Redacts sensitive field values from a log field dictionary.
/// </summary>
/// <param name="fields">Dictionary of field names and values.</param>
public static Dictionary<string, object?> RedactFields(IReadOnlyDictionary<string, object?> fields)
{
Dictionary<string, object?> redactedFields = [];
@@ -30,6 +40,11 @@ public static class WorkerLogRedactor
return redactedFields;
}
/// <summary>
/// Redacts a single value if its field name contains sensitive keywords.
/// </summary>
/// <param name="fieldName">Name of the field to check.</param>
/// <param name="value">Value to redact if sensitive.</param>
public static object? RedactValue(string fieldName, object? value)
{
if (value is null)
@@ -1,9 +1,16 @@
namespace MxGateway.Worker.Bootstrap;
/// <summary>Worker bootstrap options passed via environment variables and named pipes.</summary>
public sealed class WorkerOptions
{
/// <summary>Environment variable name for the worker nonce.</summary>
public const string NonceEnvironmentVariableName = "MXGATEWAY_WORKER_NONCE";
/// <summary>Initializes worker options from a bootstrap handshake.</summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <param name="pipeName">Named pipe name for gateway communication.</param>
/// <param name="protocolVersion">Protocol version agreed with the gateway.</param>
/// <param name="nonce">Authentication nonce for the handshake.</param>
public WorkerOptions(
string sessionId,
string pipeName,
@@ -16,11 +23,15 @@ public sealed class WorkerOptions
Nonce = nonce;
}
/// <summary>Unique identifier for the gateway session this worker serves.</summary>
public string SessionId { get; }
/// <summary>Named pipe name for communicating with the gateway.</summary>
public string PipeName { get; }
/// <summary>Worker protocol version negotiated with the gateway.</summary>
public uint ProtocolVersion { get; }
/// <summary>Nonce used to authenticate the handshake with the gateway.</summary>
public string Nonce { get; }
}
@@ -4,6 +4,9 @@ using MxGateway.Contracts;
namespace MxGateway.Worker.Bootstrap;
/// <summary>
/// Parses worker command-line arguments and environment variables.
/// </summary>
public sealed class WorkerOptionsParser
{
private const string SessionIdOptionName = "--session-id";
@@ -12,11 +15,20 @@ public sealed class WorkerOptionsParser
private readonly IWorkerEnvironment _environment;
/// <summary>
/// Initializes the parser with a worker environment.
/// </summary>
/// <param name="environment">Worker environment for reading configuration.</param>
public WorkerOptionsParser(IWorkerEnvironment environment)
{
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
}
/// <summary>
/// Parses command-line arguments and returns bootstrap configuration or errors.
/// </summary>
/// <param name="args">Command-line arguments to parse.</param>
/// <returns>Bootstrap result containing configuration or error messages.</returns>
public WorkerBootstrapResult Parse(string[] args)
{
if (args is null)
@@ -2,8 +2,13 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.Conversion;
/// <summary>Result of converting an HResult to a protocol status and diagnostic message.</summary>
public sealed class HResultConversion
{
/// <summary>Initializes the conversion result.</summary>
/// <param name="hresult">The original HResult value.</param>
/// <param name="protocolStatus">The converted protocol status.</param>
/// <param name="diagnosticMessage">Diagnostic message describing the HResult.</param>
public HResultConversion(
int hresult,
ProtocolStatus protocolStatus,
@@ -14,9 +19,12 @@ public sealed class HResultConversion
DiagnosticMessage = diagnosticMessage;
}
/// <summary>The original HResult value.</summary>
public int HResult { get; }
/// <summary>The converted protocol status.</summary>
public ProtocolStatus ProtocolStatus { get; }
/// <summary>Diagnostic message describing the HResult.</summary>
public string DiagnosticMessage { get; }
}
@@ -6,6 +6,9 @@ namespace MxGateway.Worker.Conversion;
public sealed class HResultConverter
{
/// <summary>Converts an exception to an HResult conversion with protocol status and diagnostic message.</summary>
/// <param name="exception">Exception to convert.</param>
/// <returns>Conversion result with HResult, protocol status, and diagnostics.</returns>
public HResultConversion Convert(Exception exception)
{
if (exception is null)
@@ -23,6 +26,10 @@ public sealed class HResultConverter
CreateSafeDiagnosticMessage(exception));
}
/// <summary>Creates a protocol status from an HResult code and optional exception.</summary>
/// <param name="hresult">HResult error code.</param>
/// <param name="exception">Exception providing additional context.</param>
/// <returns>Protocol status with mapped code and message.</returns>
public ProtocolStatus CreateProtocolStatus(
int hresult,
Exception? exception = null)
@@ -4,6 +4,8 @@ namespace MxGateway.Worker.Conversion;
public sealed class MxStatusConversionException : Exception
{
/// <summary>Initializes a new instance of the <see cref="MxStatusConversionException"/> class.</summary>
/// <param name="message">The exception message.</param>
public MxStatusConversionException(string message)
: base(message)
{
@@ -47,6 +47,9 @@ internal static class MxStatusDetailText
[8017] = "Object must be offscan to modify attributes that have an MxSecurityConfigure security classification",
};
/// <summary>Looks up the text description for an MXAccess status detail code.</summary>
/// <param name="detail">Status detail code.</param>
/// <returns>Description text if found; otherwise empty string.</returns>
public static string Lookup(int detail)
{
return KnownDetails.TryGetValue(detail, out string text) ? text : string.Empty;
@@ -6,8 +6,11 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.Conversion;
/// <summary>Converts MXAccess MXSTATUS_PROXY COM objects to protobuf MxStatusProxy messages.</summary>
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>
public MxStatusProxy Convert(object status)
{
if (status is null)
@@ -33,6 +36,8 @@ 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>
public IReadOnlyList<MxStatusProxy> ConvertMany(Array? statuses)
{
if (statuses is null)
@@ -60,6 +65,8 @@ public sealed class MxStatusProxyConverter
return converted;
}
/// <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>
public string PreserveCompletionOnlyStatusBytes(byte[] statusBytes)
{
if (statusBytes is null)
@@ -8,11 +8,22 @@ namespace MxGateway.Worker.Conversion;
public sealed class VariantConverter
{
/// <summary>
/// Converts an object value to an MxValue without a specified data type.
/// </summary>
/// <param name="value">Value to convert.</param>
/// <returns>Converted MxValue.</returns>
public MxValue Convert(object? value)
{
return Convert(value, MxDataType.Unspecified);
}
/// <summary>
/// Converts an object value to an MxValue with an expected data type.
/// </summary>
/// <param name="value">Value to convert.</param>
/// <param name="expectedDataType">Expected MXAccess data type.</param>
/// <returns>Converted MxValue.</returns>
public MxValue Convert(
object? value,
MxDataType expectedDataType)
@@ -35,6 +46,12 @@ public sealed class VariantConverter
return ConvertScalar(value, expectedDataType);
}
/// <summary>
/// Converts a .NET array to an MxArray.
/// </summary>
/// <param name="array">Array to convert.</param>
/// <param name="expectedElementDataType">Expected data type for array elements.</param>
/// <returns>Converted MxArray.</returns>
public MxArray ConvertArray(
Array array,
MxDataType expectedElementDataType = MxDataType.Unspecified)
@@ -4,8 +4,12 @@ using MxGateway.Worker.Bootstrap;
namespace MxGateway.Worker.Ipc;
/// <summary>Manages the worker's named pipe connection to the gateway.</summary>
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>
Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default);
@@ -5,7 +5,9 @@ namespace MxGateway.Worker.Ipc;
public static class WorkerContractInfo
{
/// <summary>The worker protocol version supported by this contract.</summary>
public static uint SupportedProtocolVersion => GatewayContractInfo.WorkerProtocolVersion;
/// <summary>The fully qualified name of the WorkerEnvelope message descriptor.</summary>
public static string WorkerEnvelopeDescriptorName => WorkerEnvelope.Descriptor.FullName;
}
@@ -3,8 +3,12 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.Ipc;
/// <summary>Validates worker envelope frames against protocol options.</summary>
internal static class WorkerEnvelopeValidator
{
/// <summary>Validates a worker envelope for protocol compliance.</summary>
/// <param name="envelope">The envelope to validate.</param>
/// <param name="options">The frame protocol configuration.</param>
public static void Validate(
WorkerEnvelope envelope,
WorkerFrameProtocolOptions options)
@@ -2,8 +2,16 @@ using System;
namespace MxGateway.Worker.Ipc;
/// <summary>
/// Exception raised when the named-pipe frame protocol encounters an error.
/// </summary>
public sealed class WorkerFrameProtocolException : Exception
{
/// <summary>
/// Initializes with an error code and message.
/// </summary>
/// <param name="errorCode">Protocol error classification.</param>
/// <param name="message">Exception message.</param>
public WorkerFrameProtocolException(
WorkerFrameProtocolErrorCode errorCode,
string message)
@@ -12,6 +20,12 @@ public sealed class WorkerFrameProtocolException : Exception
ErrorCode = errorCode;
}
/// <summary>
/// Initializes with an error code, message, and inner exception.
/// </summary>
/// <param name="errorCode">Protocol error classification.</param>
/// <param name="message">Exception message.</param>
/// <param name="innerException">Underlying cause.</param>
public WorkerFrameProtocolException(
WorkerFrameProtocolErrorCode errorCode,
string message,
@@ -21,5 +35,8 @@ public sealed class WorkerFrameProtocolException : Exception
ErrorCode = errorCode;
}
/// <summary>
/// The protocol error code classifying the failure.
/// </summary>
public WorkerFrameProtocolErrorCode ErrorCode { get; }
}
@@ -4,10 +4,14 @@ using MxGateway.Worker.Bootstrap;
namespace MxGateway.Worker.Ipc;
/// <summary>Configuration options for the worker frame protocol.</summary>
public sealed class WorkerFrameProtocolOptions
{
/// <summary>Default maximum message size in bytes (16 MB).</summary>
public const int DefaultMaxMessageBytes = 16 * 1024 * 1024;
/// <summary>Initializes a new instance of the WorkerFrameProtocolOptions class from WorkerOptions.</summary>
/// <param name="options">Worker initialization options.</param>
public WorkerFrameProtocolOptions(WorkerOptions options)
: this(
options?.SessionId ?? throw new ArgumentNullException(nameof(options)),
@@ -17,6 +21,10 @@ public sealed class WorkerFrameProtocolOptions
{
}
/// <summary>Initializes a new instance of the WorkerFrameProtocolOptions class with default max message bytes.</summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <param name="protocolVersion">Protocol version.</param>
/// <param name="nonce">Nonce for startup validation.</param>
public WorkerFrameProtocolOptions(
string sessionId,
uint protocolVersion,
@@ -29,6 +37,11 @@ public sealed class WorkerFrameProtocolOptions
{
}
/// <summary>Initializes a new instance of the WorkerFrameProtocolOptions class with all parameters.</summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <param name="protocolVersion">Protocol version.</param>
/// <param name="nonce">Nonce for startup validation.</param>
/// <param name="maxMessageBytes">Maximum message size in bytes.</param>
public WorkerFrameProtocolOptions(
string sessionId,
uint protocolVersion,
@@ -76,11 +89,15 @@ public sealed class WorkerFrameProtocolOptions
MaxMessageBytes = maxMessageBytes;
}
/// <summary>Gets the session ID for the worker protocol.</summary>
public string SessionId { get; }
/// <summary>Gets the protocol version.</summary>
public uint ProtocolVersion { get; }
/// <summary>Gets the nonce for startup validation.</summary>
public string Nonce { get; }
/// <summary>Gets the maximum message size in bytes.</summary>
public int MaxMessageBytes { get; }
}
@@ -7,11 +7,15 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.Ipc;
/// <summary>Reads length-prefixed WorkerEnvelope protobuf frames from a stream.</summary>
public sealed class WorkerFrameReader
{
private readonly WorkerFrameProtocolOptions _options;
private readonly Stream _stream;
/// <summary>Initializes the reader with a stream and protocol options.</summary>
/// <param name="stream">Stream to read frames from.</param>
/// <param name="options">Protocol options for frame validation.</param>
public WorkerFrameReader(
Stream stream,
WorkerFrameProtocolOptions options)
@@ -20,6 +24,8 @@ public sealed class WorkerFrameReader
_options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <summary>Reads and validates a single length-prefixed frame from the stream.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
public async Task<WorkerEnvelope> ReadAsync(CancellationToken cancellationToken = default)
{
byte[] lengthPrefix = new byte[sizeof(uint)];
@@ -7,12 +7,16 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.Ipc;
/// <summary>Writes worker frames to a stream with length-prefixed protobuf serialization.</summary>
public sealed class WorkerFrameWriter
{
private readonly WorkerFrameProtocolOptions _options;
private readonly SemaphoreSlim _writeLock = new(1, 1);
private readonly Stream _stream;
/// <summary>Initializes a new instance of the WorkerFrameWriter class.</summary>
/// <param name="stream">Stream to write frames to.</param>
/// <param name="options">Protocol options for frame encoding.</param>
public WorkerFrameWriter(
Stream stream,
WorkerFrameProtocolOptions options)
@@ -21,6 +25,9 @@ public sealed class WorkerFrameWriter
_options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <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>
public async Task WriteAsync(
WorkerEnvelope envelope,
CancellationToken cancellationToken = default)
@@ -10,10 +10,18 @@ using Polly.Retry;
namespace MxGateway.Worker.Ipc;
/// <summary>
/// Connects to the gateway via a named pipe and runs the worker frame protocol session.
/// </summary>
public sealed class WorkerPipeClient : IWorkerPipeClient
{
/// <summary>Default overall connection timeout in milliseconds.</summary>
public const int DefaultConnectTimeoutMilliseconds = 30000;
/// <summary>Default per-attempt connection timeout in milliseconds.</summary>
public const int DefaultConnectAttemptTimeoutMilliseconds = 2000;
/// <summary>Environment variable for overriding the per-attempt connection timeout.</summary>
public const string ConnectAttemptTimeoutEnvironmentVariableName =
"MXGATEWAY_WORKER_PIPE_CONNECT_ATTEMPT_TIMEOUT_MS";
@@ -22,21 +30,37 @@ public sealed class WorkerPipeClient : IWorkerPipeClient
private readonly Func<Stream, WorkerFrameProtocolOptions, IWorkerLogger?, WorkerPipeSession> _sessionFactory;
private readonly IWorkerLogger? _logger;
/// <summary>
/// Initializes a worker pipe client with default timeouts.
/// </summary>
public WorkerPipeClient()
: this(null, DefaultConnectTimeoutMilliseconds)
{
}
/// <summary>
/// Initializes a worker pipe client with a logger and default timeouts.
/// </summary>
/// <param name="logger">Optional logger for diagnostic output.</param>
public WorkerPipeClient(IWorkerLogger? logger)
: this(logger, DefaultConnectTimeoutMilliseconds)
{
}
/// <summary>
/// Initializes a worker pipe client with a custom overall connect timeout.
/// </summary>
/// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
public WorkerPipeClient(int connectTimeoutMilliseconds)
: this(null, connectTimeoutMilliseconds)
{
}
/// <summary>
/// Initializes a worker pipe client with custom timeouts and a session factory.
/// </summary>
/// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
/// <param name="sessionFactory">Factory creating the worker pipe session.</param>
public WorkerPipeClient(
int connectTimeoutMilliseconds,
Func<Stream, WorkerFrameProtocolOptions, WorkerPipeSession> sessionFactory)
@@ -48,6 +72,11 @@ public sealed class WorkerPipeClient : IWorkerPipeClient
{
}
/// <summary>
/// Initializes a worker pipe client with a logger and custom overall timeout.
/// </summary>
/// <param name="logger">Optional logger for diagnostic output.</param>
/// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
public WorkerPipeClient(
IWorkerLogger? logger,
int connectTimeoutMilliseconds)
@@ -59,6 +88,12 @@ public sealed class WorkerPipeClient : IWorkerPipeClient
{
}
/// <summary>
/// Initializes a worker pipe client with logger, timeouts, and a session factory.
/// </summary>
/// <param name="logger">Optional logger for diagnostic output.</param>
/// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
/// <param name="sessionFactory">Factory creating the worker pipe session.</param>
public WorkerPipeClient(
IWorkerLogger? logger,
int connectTimeoutMilliseconds,
@@ -71,6 +106,13 @@ public sealed class WorkerPipeClient : IWorkerPipeClient
{
}
/// <summary>
/// Initializes a worker pipe client with full configuration.
/// </summary>
/// <param name="logger">Optional logger for diagnostic output.</param>
/// <param name="connectTimeoutMilliseconds">Overall connection timeout in milliseconds.</param>
/// <param name="connectAttemptTimeoutMilliseconds">Per-attempt connection timeout in milliseconds.</param>
/// <param name="sessionFactory">Factory creating the worker pipe session.</param>
public WorkerPipeClient(
IWorkerLogger? logger,
int connectTimeoutMilliseconds,
@@ -97,6 +139,11 @@ 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>
public async Task RunAsync(
WorkerOptions options,
CancellationToken cancellationToken = default)
@@ -34,6 +34,10 @@ public sealed class WorkerPipeSession
private bool _watchdogFaultSent;
private bool _shutdownTimedOut;
/// <summary>Initializes a new worker pipe session over the provided stream.</summary>
/// <param name="stream">Network stream for reading and writing frames.</param>
/// <param name="options">Frame protocol configuration.</param>
/// <param name="logger">Optional logger for diagnostic output.</param>
public WorkerPipeSession(
Stream stream,
WorkerFrameProtocolOptions options,
@@ -49,6 +53,11 @@ public sealed class WorkerPipeSession
{
}
/// <summary>Initializes a new worker pipe session with custom frame reader and writer.</summary>
/// <param name="reader">Frame reader for incoming messages.</param>
/// <param name="writer">Frame writer for outgoing messages.</param>
/// <param name="options">Frame protocol configuration.</param>
/// <param name="processIdProvider">Function returning the current worker process ID.</param>
public WorkerPipeSession(
WorkerFrameReader reader,
WorkerFrameWriter writer,
@@ -65,6 +74,14 @@ public sealed class WorkerPipeSession
{
}
/// <summary>Initializes a new worker pipe session with full configuration and dependencies.</summary>
/// <param name="reader">Frame reader for incoming messages.</param>
/// <param name="writer">Frame writer for outgoing messages.</param>
/// <param name="options">Frame protocol configuration.</param>
/// <param name="processIdProvider">Function returning the current worker process ID.</param>
/// <param name="sessionOptions">Session-specific options.</param>
/// <param name="runtimeSessionFactory">Factory creating the MXAccess runtime session.</param>
/// <param name="logger">Optional logger for diagnostic output.</param>
public WorkerPipeSession(
WorkerFrameReader reader,
WorkerFrameWriter writer,
@@ -84,6 +101,8 @@ public sealed class WorkerPipeSession
_sessionOptions.Validate();
}
/// <summary>Runs the worker session, completing the handshake and processing messages until cancellation.</summary>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
public async Task RunAsync(CancellationToken cancellationToken = default)
{
_runtimeSession = _runtimeSessionFactory();
@@ -106,11 +125,16 @@ 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>
public Task CompleteStartupHandshakeAsync(CancellationToken cancellationToken = default)
{
return CompleteStartupHandshakeAsync(InitializeMxAccessAsync, cancellationToken);
}
/// <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>
public async Task CompleteStartupHandshakeAsync(
Func<CancellationToken, Task> initializeMxAccessAsync,
CancellationToken cancellationToken = default)
@@ -129,6 +153,9 @@ public sealed class WorkerPipeSession
cancellationToken).ConfigureAwait(false);
}
/// <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>
public async Task CompleteStartupHandshakeAsync(
Func<CancellationToken, Task<WorkerReady>> initializeMxAccessAsync,
CancellationToken cancellationToken = default)
@@ -2,21 +2,28 @@ using System;
namespace MxGateway.Worker.Ipc;
/// <summary>Configuration options for worker pipe sessions including heartbeat parameters.</summary>
public sealed class WorkerPipeSessionOptions
{
/// <summary>Default heartbeat interval (5 seconds).</summary>
public static readonly TimeSpan DefaultHeartbeatInterval = TimeSpan.FromSeconds(5);
/// <summary>Default heartbeat grace period (15 seconds).</summary>
public static readonly TimeSpan DefaultHeartbeatGrace = TimeSpan.FromSeconds(15);
/// <summary>Initializes a new instance of the WorkerPipeSessionOptions class with default values.</summary>
public WorkerPipeSessionOptions()
{
HeartbeatInterval = DefaultHeartbeatInterval;
HeartbeatGrace = DefaultHeartbeatGrace;
}
/// <summary>Gets or sets the heartbeat interval.</summary>
public TimeSpan HeartbeatInterval { get; set; }
/// <summary>Gets or sets the heartbeat grace period.</summary>
public TimeSpan HeartbeatGrace { get; set; }
/// <summary>Validates the session options.</summary>
public void Validate()
{
if (HeartbeatInterval <= TimeSpan.Zero)
@@ -2,5 +2,7 @@ namespace MxGateway.Worker.MxAccess;
public interface IMxAccessComObjectFactory
{
/// <summary>Creates an MXAccess COM object instance.</summary>
/// <returns>The created COM object.</returns>
object Create();
}
@@ -2,9 +2,13 @@ namespace MxGateway.Worker.MxAccess;
public interface IMxAccessEventSink
{
/// <summary>Attaches the event sink to an MXAccess COM object.</summary>
/// <param name="mxAccessComObject">The MXAccess COM object.</param>
/// <param name="sessionId">The session ID.</param>
void Attach(
object mxAccessComObject,
string sessionId);
/// <summary>Detaches the event sink from the COM object.</summary>
void Detach();
}
@@ -2,31 +2,57 @@ namespace MxGateway.Worker.MxAccess;
public interface IMxAccessServer
{
/// <summary>Registers a client and returns a server handle.</summary>
/// <param name="clientName">Name of the client requesting registration.</param>
/// <returns>Server handle for subsequent operations.</returns>
int Register(string clientName);
/// <summary>Unregisters a server handle.</summary>
/// <param name="serverHandle">Server handle to unregister.</param>
void Unregister(int serverHandle);
/// <summary>Adds an item to a server and returns an item handle.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemDefinition">Item definition string.</param>
/// <returns>Item handle for the added item.</returns>
int AddItem(
int serverHandle,
string itemDefinition);
/// <summary>Adds an item with context to a server and returns an item handle.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemDefinition">Item definition string.</param>
/// <param name="itemContext">Item context string.</param>
/// <returns>Item handle for the added item.</returns>
int AddItem2(
int serverHandle,
string itemDefinition,
string itemContext);
/// <summary>Removes an item from a server.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to remove.</param>
void RemoveItem(
int serverHandle,
int itemHandle);
/// <summary>Subscribes to change notifications for an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to subscribe to.</param>
void Advise(
int serverHandle,
int itemHandle);
/// <summary>Unsubscribes from change notifications for an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to unsubscribe from.</param>
void UnAdvise(
int serverHandle,
int itemHandle);
/// <summary>Subscribes to supervisory change notifications for an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to subscribe to.</param>
void AdviseSupervisory(
int serverHandle,
int itemHandle);
@@ -7,25 +7,65 @@ using MxGateway.Worker.Sta;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Manages the runtime session between the worker and the MXAccess COM instance on an STA thread.
/// </summary>
public interface IWorkerRuntimeSession : IDisposable
{
/// <summary>
/// Starts the session, creates the MXAccess COM object, and returns ready metadata.
/// </summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <param name="workerProcessId">ID of the worker process.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>Asynchronous task returning worker readiness metadata.</returns>
Task<WorkerReady> StartAsync(
string sessionId,
int workerProcessId,
CancellationToken cancellationToken = default);
/// <summary>
/// Dispatches an STA command to the MXAccess runtime and returns the reply.
/// </summary>
/// <param name="command">STA command to execute on the STA thread.</param>
/// <returns>Asynchronous task returning the command reply.</returns>
Task<MxCommandReply> DispatchAsync(StaCommand command);
/// <summary>
/// Captures a heartbeat snapshot of the runtime state.
/// </summary>
WorkerRuntimeHeartbeatSnapshot CaptureHeartbeat();
/// <summary>
/// Drains up to the specified number of pending events from the queue.
/// </summary>
/// <param name="maxEvents">Maximum number of events to drain.</param>
/// <returns>List of drained events.</returns>
IReadOnlyList<WorkerEvent> DrainEvents(uint maxEvents);
/// <summary>
/// Drains a pending fault from the queue, if any.
/// </summary>
WorkerFault? DrainFault();
/// <summary>
/// Cancels a pending command by correlation ID.
/// </summary>
/// <param name="correlationId">Correlation ID of the command to cancel.</param>
/// <returns>True if the command was found and cancelled; otherwise, false.</returns>
bool CancelCommand(string correlationId);
/// <summary>
/// Requests a graceful shutdown of the session.
/// </summary>
void RequestShutdown();
/// <summary>
/// Shuts down the session gracefully within the specified timeout.
/// </summary>
/// <param name="timeout">Maximum time to allow for graceful shutdown.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
/// <returns>Asynchronous task returning the shutdown result.</returns>
Task<MxAccessShutdownResult> ShutdownGracefullyAsync(
TimeSpan timeout,
CancellationToken cancellationToken = default);
@@ -4,6 +4,7 @@ using Proto = MxGateway.Contracts.Proto;
namespace MxGateway.Worker.MxAccess;
/// <summary>Sink for MXAccess COM events that converts them to protobuf format.</summary>
public sealed class MxAccessBaseEventSink : IMxAccessEventSink
{
private readonly MxAccessEventMapper eventMapper;
@@ -11,16 +12,22 @@ public sealed class MxAccessBaseEventSink : IMxAccessEventSink
private LMXProxyServerClass? server;
private string sessionId = string.Empty;
/// <summary>Initializes a new instance of the MxAccessBaseEventSink class with a default queue.</summary>
public MxAccessBaseEventSink()
: this(new MxAccessEventQueue())
{
}
/// <summary>Initializes a new instance of the MxAccessBaseEventSink class with a provided queue.</summary>
/// <param name="eventQueue">Queue for buffering converted MXAccess events.</param>
public MxAccessBaseEventSink(MxAccessEventQueue eventQueue)
: this(eventQueue, new MxAccessEventMapper())
{
}
/// <summary>Initializes a new instance of the MxAccessBaseEventSink class with provided queue and mapper.</summary>
/// <param name="eventQueue">Queue for buffering converted MXAccess events.</param>
/// <param name="eventMapper">Converter for MXAccess events to protobuf format.</param>
public MxAccessBaseEventSink(
MxAccessEventQueue eventQueue,
MxAccessEventMapper eventMapper)
@@ -29,6 +36,7 @@ public sealed class MxAccessBaseEventSink : IMxAccessEventSink
this.eventMapper = eventMapper ?? throw new ArgumentNullException(nameof(eventMapper));
}
/// <inheritdoc />
public void Attach(
object mxAccessComObject,
string sessionId)
@@ -41,6 +49,7 @@ public sealed class MxAccessBaseEventSink : IMxAccessEventSink
server.OnBufferedDataChange += OnBufferedDataChange;
}
/// <inheritdoc />
public void Detach()
{
if (server is null)
@@ -2,8 +2,10 @@ using ArchestrA.MxAccess;
namespace MxGateway.Worker.MxAccess;
/// <summary>Factory for creating MXAccess COM objects on the STA thread.</summary>
public sealed class MxAccessComObjectFactory : IMxAccessComObjectFactory
{
/// <inheritdoc />
public object Create()
{
return new LMXProxyServerClass();
@@ -5,15 +5,23 @@ using ArchestrA.MxAccess;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Adapter exposing MXAccess COM object methods through the IMxAccessServer interface.
/// </summary>
public sealed class MxAccessComServer : IMxAccessServer
{
private readonly object mxAccessComObject;
/// <summary>
/// Initializes the adapter with the MXAccess COM object.
/// </summary>
/// <param name="mxAccessComObject">MXAccess COM object instance.</param>
public MxAccessComServer(object mxAccessComObject)
{
this.mxAccessComObject = mxAccessComObject ?? throw new ArgumentNullException(nameof(mxAccessComObject));
}
/// <inheritdoc />
public int Register(string clientName)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
@@ -24,6 +32,7 @@ public sealed class MxAccessComServer : IMxAccessServer
return (int)Invoke(nameof(Register), clientName);
}
/// <inheritdoc />
public void Unregister(int serverHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
@@ -35,6 +44,7 @@ public sealed class MxAccessComServer : IMxAccessServer
Invoke(nameof(Unregister), serverHandle);
}
/// <inheritdoc />
public int AddItem(
int serverHandle,
string itemDefinition)
@@ -47,6 +57,7 @@ public sealed class MxAccessComServer : IMxAccessServer
return (int)Invoke(nameof(AddItem), serverHandle, itemDefinition);
}
/// <inheritdoc />
public int AddItem2(
int serverHandle,
string itemDefinition,
@@ -60,6 +71,7 @@ public sealed class MxAccessComServer : IMxAccessServer
return (int)Invoke(nameof(AddItem2), serverHandle, itemDefinition, itemContext);
}
/// <inheritdoc />
public void RemoveItem(
int serverHandle,
int itemHandle)
@@ -73,6 +85,7 @@ public sealed class MxAccessComServer : IMxAccessServer
Invoke(nameof(RemoveItem), serverHandle, itemHandle);
}
/// <inheritdoc />
public void Advise(
int serverHandle,
int itemHandle)
@@ -86,6 +99,7 @@ public sealed class MxAccessComServer : IMxAccessServer
Invoke(nameof(Advise), serverHandle, itemHandle);
}
/// <inheritdoc />
public void UnAdvise(
int serverHandle,
int itemHandle)
@@ -99,6 +113,7 @@ public sealed class MxAccessComServer : IMxAccessServer
Invoke(nameof(UnAdvise), serverHandle, itemHandle);
}
/// <inheritdoc />
public void AdviseSupervisory(
int serverHandle,
int itemHandle)
@@ -6,16 +6,28 @@ using MxGateway.Worker.Sta;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Executes MXAccess commands on an STA session.
/// </summary>
public sealed class MxAccessCommandExecutor : IStaCommandExecutor
{
private readonly MxAccessSession session;
private readonly VariantConverter variantConverter;
/// <summary>
/// Initializes a command executor with an MXAccess session.
/// </summary>
/// <param name="session">MXAccess session on the STA thread.</param>
public MxAccessCommandExecutor(MxAccessSession session)
: this(session, new VariantConverter())
{
}
/// <summary>
/// Initializes a command executor with an MXAccess session and a variant converter.
/// </summary>
/// <param name="session">MXAccess session on the STA thread.</param>
/// <param name="variantConverter">Converter for MXAccess variant values to MxValue protobuf messages.</param>
public MxAccessCommandExecutor(
MxAccessSession session,
VariantConverter variantConverter)
@@ -24,6 +36,11 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
this.variantConverter = variantConverter ?? throw new ArgumentNullException(nameof(variantConverter));
}
/// <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>
public MxCommandReply Execute(StaCommand command)
{
if (command is null)
@@ -3,8 +3,11 @@ using System.Runtime.InteropServices;
namespace MxGateway.Worker.MxAccess;
/// <summary>Thrown when the worker fails to instantiate the MXAccess COM object.</summary>
public sealed class MxAccessCreationException : Exception
{
/// <summary>Initializes a new instance with diagnostic info from the inner exception.</summary>
/// <param name="innerException">The exception that caused the creation failure.</param>
public MxAccessCreationException(Exception innerException)
: base(
$"Failed to create MXAccess COM object {MxAccessInteropInfo.ComClassName} ({MxAccessInteropInfo.ProgId}).",
@@ -16,14 +19,21 @@ public sealed class MxAccessCreationException : Exception
HResult = innerException.HResult;
}
/// <summary>The ProgID that was attempted during COM instantiation.</summary>
public string AttemptedProgId { get; }
/// <summary>The CLSID that was attempted during COM instantiation.</summary>
public string AttemptedClsid { get; }
/// <summary>The COM class name that was attempted during instantiation.</summary>
public string AttemptedComClassName { get; }
/// <summary>The captured HResult from the instantiation failure, or null if zero.</summary>
public int? CapturedHResult => HResult == 0 ? null : HResult;
/// <summary>Wraps an exception in MxAccessCreationException if it is not already.</summary>
/// <param name="exception">The exception to wrap.</param>
/// <returns>An MxAccessCreationException wrapping the input exception.</returns>
public static MxAccessCreationException From(Exception exception)
{
return exception is MxAccessCreationException creationException
@@ -31,6 +41,9 @@ public sealed class MxAccessCreationException : Exception
: new MxAccessCreationException(exception);
}
/// <summary>Extracts the HResult from an exception, handling MXAccess and COM exceptions specially.</summary>
/// <param name="exception">The exception to extract the HResult from.</param>
/// <returns>The HResult value, or null if zero.</returns>
public static int? ExtractHResult(Exception exception)
{
if (exception is MxAccessCreationException creationException)
@@ -4,16 +4,21 @@ using MxGateway.Worker.Conversion;
namespace MxGateway.Worker.MxAccess;
/// <summary>Maps MXAccess COM events to protobuf MxEvent messages.</summary>
public sealed class MxAccessEventMapper
{
private readonly VariantConverter variantConverter;
private readonly MxStatusProxyConverter statusProxyConverter;
/// <summary>Initializes a new instance of the MxAccessEventMapper class with default converters.</summary>
public MxAccessEventMapper()
: this(new VariantConverter(), new MxStatusProxyConverter())
{
}
/// <summary>Initializes a new instance of the MxAccessEventMapper class with provided converters.</summary>
/// <param name="variantConverter">Converter for MXAccess variant values to MxValue protobuf messages.</param>
/// <param name="statusProxyConverter">Converter for MXAccess status arrays to MxStatusProxy protobuf messages.</param>
public MxAccessEventMapper(
VariantConverter variantConverter,
MxStatusProxyConverter statusProxyConverter)
@@ -22,6 +27,14 @@ public sealed class MxAccessEventMapper
this.statusProxyConverter = statusProxyConverter ?? throw new ArgumentNullException(nameof(statusProxyConverter));
}
/// <summary>Creates an OnDataChange event from MXAccess COM event arguments.</summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="value">Item value received from MXAccess.</param>
/// <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>
public MxEvent CreateOnDataChange(
string sessionId,
int serverHandle,
@@ -45,6 +58,11 @@ public sealed class MxAccessEventMapper
return mxEvent;
}
/// <summary>Creates an OnWriteComplete event from MXAccess COM event arguments.</summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <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>
public MxEvent CreateOnWriteComplete(
string sessionId,
int serverHandle,
@@ -62,6 +80,11 @@ public sealed class MxAccessEventMapper
return mxEvent;
}
/// <summary>Creates an OperationComplete event from MXAccess COM event arguments.</summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <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>
public MxEvent CreateOperationComplete(
string sessionId,
int serverHandle,
@@ -79,6 +102,15 @@ public sealed class MxAccessEventMapper
return mxEvent;
}
/// <summary>Creates an OnBufferedDataChange event from MXAccess COM event arguments.</summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="rawDataType">Raw MXAccess data type code for the buffered value.</param>
/// <param name="value">Item value received from MXAccess.</param>
/// <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>
public MxEvent CreateOnBufferedDataChange(
string sessionId,
int serverHandle,
@@ -108,6 +140,9 @@ public sealed class MxAccessEventMapper
return mxEvent;
}
/// <summary>Maps a raw MXAccess data type code to the MxDataType enum.</summary>
/// <param name="rawDataType">Raw MXAccess data type value to map.</param>
/// <returns>The corresponding MxDataType enum value.</returns>
public static MxDataType MapMxDataType(int rawDataType)
{
return rawDataType switch
@@ -5,8 +5,14 @@ using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Thread-safe queue for MxAccess events with capacity overflow and fault tracking.
/// </summary>
public sealed class MxAccessEventQueue
{
/// <summary>
/// Default queue capacity (10,000 events).
/// </summary>
public const int DefaultCapacity = 10000;
private readonly int capacity;
@@ -16,11 +22,18 @@ public sealed class MxAccessEventQueue
private WorkerFault? fault;
private bool faultDrained;
/// <summary>
/// Initializes the queue with the default capacity.
/// </summary>
public MxAccessEventQueue()
: this(DefaultCapacity)
{
}
/// <summary>
/// Initializes the queue with the specified capacity.
/// </summary>
/// <param name="capacity">Maximum number of events the queue can hold.</param>
public MxAccessEventQueue(int capacity)
{
if (capacity <= 0)
@@ -34,8 +47,14 @@ public sealed class MxAccessEventQueue
events = new Queue<WorkerEvent>(capacity);
}
/// <summary>
/// The queue's maximum capacity.
/// </summary>
public int Capacity => capacity;
/// <summary>
/// The current number of events in the queue.
/// </summary>
public int Count
{
get
@@ -47,6 +66,9 @@ public sealed class MxAccessEventQueue
}
}
/// <summary>
/// The highest event sequence number assigned.
/// </summary>
public ulong LastEventSequence
{
get
@@ -58,6 +80,9 @@ public sealed class MxAccessEventQueue
}
}
/// <summary>
/// Indicates whether the queue is in a faulted state.
/// </summary>
public bool IsFaulted
{
get
@@ -69,6 +94,9 @@ public sealed class MxAccessEventQueue
}
}
/// <summary>
/// The current fault if the queue is faulted, or null.
/// </summary>
public WorkerFault? Fault
{
get
@@ -80,6 +108,10 @@ public sealed class MxAccessEventQueue
}
}
/// <summary>
/// Enqueues an MxAccess event, assigning a sequence number and timestamp.
/// </summary>
/// <param name="mxEvent">MXAccess event to enqueue.</param>
public void Enqueue(MxEvent mxEvent)
{
if (mxEvent is null)
@@ -112,6 +144,10 @@ public sealed class MxAccessEventQueue
}
}
/// <summary>
/// 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>
public bool TryDequeue(out WorkerEvent? workerEvent)
{
lock (syncRoot)
@@ -127,6 +163,10 @@ public sealed class MxAccessEventQueue
}
}
/// <summary>
/// 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>
public IReadOnlyList<WorkerEvent> Drain(uint maxEvents)
{
lock (syncRoot)
@@ -149,6 +189,10 @@ public sealed class MxAccessEventQueue
}
}
/// <summary>
/// Records a fault if one has not already been recorded.
/// </summary>
/// <param name="workerFault">Worker fault to record.</param>
public void RecordFault(WorkerFault workerFault)
{
if (workerFault is null)
@@ -162,6 +206,9 @@ public sealed class MxAccessEventQueue
}
}
/// <summary>
/// Returns and clears the fault so it is not reported twice.
/// </summary>
public WorkerFault? DrainFault()
{
lock (syncRoot)
@@ -4,11 +4,18 @@ namespace MxGateway.Worker.MxAccess;
public sealed class MxAccessEventQueueOverflowException : Exception
{
/// <summary>
/// Initializes a new instance of <see cref="MxAccessEventQueueOverflowException"/>.
/// </summary>
/// <param name="capacity">Queue capacity.</param>
public MxAccessEventQueueOverflowException(int capacity)
: base($"MXAccess outbound event queue reached its configured capacity of {capacity}.")
{
Capacity = capacity;
}
/// <summary>
/// Gets the queue capacity.
/// </summary>
public int Capacity { get; }
}
@@ -10,17 +10,20 @@ public sealed class MxAccessHandleRegistry
private readonly Dictionary<long, RegisteredItemHandle> itemHandles = new();
private readonly Dictionary<AdviceHandleKey, RegisteredAdviceHandle> adviceHandles = new();
/// <summary>Gets a read-only list of registered server handles ordered by handle value.</summary>
public IReadOnlyList<RegisteredServerHandle> ServerHandles => serverHandles
.Values
.OrderBy(handle => handle.ServerHandle)
.ToArray();
/// <summary>Gets a read-only list of registered item handles ordered by server handle then item handle.</summary>
public IReadOnlyList<RegisteredItemHandle> ItemHandles => itemHandles
.Values
.OrderBy(handle => handle.ServerHandle)
.ThenBy(handle => handle.ItemHandle)
.ToArray();
/// <summary>Gets a read-only list of registered advice handles ordered by server handle, item handle, and advice kind.</summary>
public IReadOnlyList<RegisteredAdviceHandle> AdviceHandles => adviceHandles
.Values
.OrderBy(handle => handle.ServerHandle)
@@ -28,6 +31,9 @@ public sealed class MxAccessHandleRegistry
.ThenBy(handle => handle.AdviceKind)
.ToArray();
/// <summary>Registers a server handle with the registry.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="clientName">Display name of the client that owns the server handle.</param>
public void RegisterServerHandle(
int serverHandle,
string clientName)
@@ -35,6 +41,8 @@ public sealed class MxAccessHandleRegistry
serverHandles[serverHandle] = new RegisteredServerHandle(serverHandle, clientName);
}
/// <summary>Unregisters a server handle and all associated item and advice handles from the registry.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
public void UnregisterServerHandle(int serverHandle)
{
serverHandles.Remove(serverHandle);
@@ -56,11 +64,19 @@ public sealed class MxAccessHandleRegistry
}
}
/// <summary>Checks if the registry contains the specified server handle.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
public bool ContainsServerHandle(int serverHandle)
{
return serverHandles.ContainsKey(serverHandle);
}
/// <summary>Registers an item handle with the registry.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="itemDefinition">Item definition name from MXAccess.</param>
/// <param name="itemContext">Item context from MXAccess, or empty string if none.</param>
/// <param name="hasItemContext">True if the item has a context; false otherwise.</param>
public void RegisterItemHandle(
int serverHandle,
int itemHandle,
@@ -76,6 +92,9 @@ public sealed class MxAccessHandleRegistry
hasItemContext);
}
/// <summary>Removes an item handle and all associated advice handles from the registry.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
public void RemoveItemHandle(
int serverHandle,
int itemHandle)
@@ -84,6 +103,9 @@ public sealed class MxAccessHandleRegistry
RemoveAdviceHandles(serverHandle, itemHandle);
}
/// <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>
public bool ContainsItemHandle(
int serverHandle,
int itemHandle)
@@ -91,6 +113,10 @@ public sealed class MxAccessHandleRegistry
return itemHandles.ContainsKey(CreateItemKey(serverHandle, itemHandle));
}
/// <summary>Registers an advice handle with the registry.</summary>
/// <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 register.</param>
public void RegisterAdviceHandle(
int serverHandle,
int itemHandle,
@@ -103,6 +129,9 @@ public sealed class MxAccessHandleRegistry
adviceKind);
}
/// <summary>Removes all advice handles for the specified server and item handles from the registry.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
public void RemoveAdviceHandles(
int serverHandle,
int itemHandle)
@@ -116,6 +145,10 @@ public sealed class MxAccessHandleRegistry
}
}
/// <summary>Checks if the registry contains the specified advice handle.</summary>
/// <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>
public bool ContainsAdviceHandle(
int serverHandle,
int itemHandle,
@@ -137,6 +170,10 @@ public sealed class MxAccessHandleRegistry
private readonly int itemHandle;
private readonly MxAccessAdviceKind adviceKind;
/// <summary>Initializes a new instance of the <see cref="AdviceHandleKey"/> struct.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="adviceKind">Type of advice.</param>
public AdviceHandleKey(
int serverHandle,
int itemHandle,
@@ -147,6 +184,7 @@ public sealed class MxAccessHandleRegistry
this.adviceKind = adviceKind;
}
/// <inheritdoc />
public bool Equals(AdviceHandleKey other)
{
return serverHandle == other.serverHandle
@@ -154,11 +192,13 @@ public sealed class MxAccessHandleRegistry
&& adviceKind == other.adviceKind;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is AdviceHandleKey other && Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
unchecked
@@ -3,25 +3,52 @@ using ArchestrA.MxAccess;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Constants and metadata for MXAccess COM interop.
/// </summary>
public static class MxAccessInteropInfo
{
/// <summary>
/// Versioned ProgID for the MXAccess COM server.
/// </summary>
public const string ProgId = "LMXProxy.LMXProxyServer.1";
/// <summary>
/// Version-independent ProgID for the MXAccess COM server.
/// </summary>
public const string VersionIndependentProgId = "LMXProxy.LMXProxyServer";
/// <summary>
/// Class ID (CLSID) of the MXAccess COM server.
/// </summary>
public const string Clsid = "{C30B52F5-2CB5-4760-AF0A-3A344A7EB5DC}";
/// <summary>
/// Path to the ArchestrA.MxAccess.dll interop assembly.
/// </summary>
public const string InteropAssemblyPath =
@"C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll";
/// <summary>
/// Path to the installed MXAccess COM server DLL.
/// </summary>
public const string RegisteredServerPath =
@"C:\Program Files (x86)\ArchestrA\Framework\Bin\LmxProxy.dll";
/// <summary>
/// Full qualified name of the COM class.
/// </summary>
public const string ComClassName = "ArchestrA.MxAccess.LMXProxyServerClass";
/// <summary>
/// Name of the MXAccess interop assembly.
/// </summary>
public static string InteropAssemblyName =>
typeof(LMXProxyServerClass).Assembly.GetName().Name ?? string.Empty;
/// <summary>
/// Version of the MXAccess interop assembly.
/// </summary>
public static Version InteropAssemblyVersion =>
typeof(LMXProxyServerClass).Assembly.GetName().Version ?? new Version(0, 0);
}
@@ -28,10 +28,14 @@ public sealed class MxAccessSession : IDisposable
CreationThreadId = creationThreadId;
}
/// <summary>The thread ID where this session was created.</summary>
public int CreationThreadId { get; }
/// <summary>The registry for tracking opened handles.</summary>
public MxAccessHandleRegistry HandleRegistry => handleRegistry;
/// <summary>Creates a WorkerReady message with session metadata.</summary>
/// <param name="workerProcessId">Process ID of the worker.</param>
public WorkerReady CreateWorkerReady(int workerProcessId)
{
return new WorkerReady
@@ -43,6 +47,10 @@ public sealed class MxAccessSession : IDisposable
};
}
/// <summary>Creates and initializes an MXAccess COM session.</summary>
/// <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>
public static MxAccessSession Create(
IMxAccessComObjectFactory factory,
IMxAccessEventSink eventSink,
@@ -97,6 +105,8 @@ 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>
public int Register(string clientName)
{
ThrowIfDisposed();
@@ -107,6 +117,8 @@ public sealed class MxAccessSession : IDisposable
return serverHandle;
}
/// <summary>Unregisters a client from MXAccess.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
public void Unregister(int serverHandle)
{
ThrowIfDisposed();
@@ -115,6 +127,9 @@ public sealed class MxAccessSession : IDisposable
handleRegistry.UnregisterServerHandle(serverHandle);
}
/// <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>
public int AddItem(
int serverHandle,
string itemDefinition)
@@ -132,6 +147,10 @@ public sealed class MxAccessSession : IDisposable
return itemHandle;
}
/// <summary>Adds an item with context 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>
/// <param name="itemContext">Context string for the item.</param>
public int AddItem2(
int serverHandle,
string itemDefinition,
@@ -150,6 +169,9 @@ public sealed class MxAccessSession : IDisposable
return itemHandle;
}
/// <summary>Removes an item from an MXAccess server.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
public void RemoveItem(
int serverHandle,
int itemHandle)
@@ -160,6 +182,9 @@ public sealed class MxAccessSession : IDisposable
handleRegistry.RemoveItemHandle(serverHandle, itemHandle);
}
/// <summary>Advises on item changes with plain subscription.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
public void Advise(
int serverHandle,
int itemHandle)
@@ -173,6 +198,9 @@ public sealed class MxAccessSession : IDisposable
MxAccessAdviceKind.Plain);
}
/// <summary>Removes plain advice subscription from an item.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
public void UnAdvise(
int serverHandle,
int itemHandle)
@@ -183,6 +211,9 @@ public sealed class MxAccessSession : IDisposable
handleRegistry.RemoveAdviceHandles(serverHandle, itemHandle);
}
/// <summary>Advises on item changes with supervisory subscription.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
public void AdviseSupervisory(
int serverHandle,
int itemHandle)
@@ -196,6 +227,9 @@ public sealed class MxAccessSession : IDisposable
MxAccessAdviceKind.Supervisory);
}
/// <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>
public IReadOnlyList<SubscribeResult> AddItemBulk(
int serverHandle,
IEnumerable<string> tagAddresses)
@@ -229,6 +263,9 @@ public sealed class MxAccessSession : IDisposable
return results;
}
/// <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>
public IReadOnlyList<SubscribeResult> AdviseItemBulk(
int serverHandle,
IEnumerable<int> itemHandles)
@@ -256,6 +293,9 @@ public sealed class MxAccessSession : IDisposable
return results;
}
/// <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>
public IReadOnlyList<SubscribeResult> RemoveItemBulk(
int serverHandle,
IEnumerable<int> itemHandles)
@@ -283,6 +323,9 @@ public sealed class MxAccessSession : IDisposable
return results;
}
/// <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>
public IReadOnlyList<SubscribeResult> UnAdviseItemBulk(
int serverHandle,
IEnumerable<int> itemHandles)
@@ -310,6 +353,9 @@ public sealed class MxAccessSession : IDisposable
return results;
}
/// <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>
public IReadOnlyList<SubscribeResult> SubscribeBulk(
int serverHandle,
IEnumerable<string> tagAddresses)
@@ -351,6 +397,9 @@ public sealed class MxAccessSession : IDisposable
return results;
}
/// <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>
public IReadOnlyList<SubscribeResult> UnsubscribeBulk(
int serverHandle,
IEnumerable<int> itemHandles)
@@ -392,6 +441,7 @@ public sealed class MxAccessSession : IDisposable
return results;
}
/// <summary>Gracefully shuts down the session, cleaning up all handles.</summary>
public MxAccessShutdownResult ShutdownGracefully()
{
if (disposed)
@@ -409,6 +459,7 @@ public sealed class MxAccessSession : IDisposable
return new MxAccessShutdownResult(failures);
}
/// <summary>Releases the MXAccess COM object and resources.</summary>
public void Dispose()
{
if (disposed)
@@ -2,8 +2,18 @@ using System;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Captures details about an MXAccess operation that failed during shutdown.
/// </summary>
public sealed class MxAccessShutdownFailure
{
/// <summary>
/// Initializes the shutdown failure record.
/// </summary>
/// <param name="operation">Name of the operation that failed.</param>
/// <param name="serverHandle">Server handle if applicable.</param>
/// <param name="itemHandle">Item handle if applicable.</param>
/// <param name="exception">Exception that was raised.</param>
public MxAccessShutdownFailure(
string operation,
int? serverHandle,
@@ -22,13 +32,28 @@ public sealed class MxAccessShutdownFailure
HResult = exception?.HResult;
}
/// <summary>
/// The operation that failed (e.g., Unregister, RemoveItem).
/// </summary>
public string Operation { get; }
/// <summary>
/// Server handle if applicable; otherwise null.
/// </summary>
public int? ServerHandle { get; }
/// <summary>
/// Item handle if applicable; otherwise null.
/// </summary>
public int? ItemHandle { get; }
/// <summary>
/// Full type name of the exception, or empty string if no exception.
/// </summary>
public string ExceptionType { get; }
/// <summary>
/// HResult code if the exception has one; otherwise null.
/// </summary>
public int? HResult { get; }
}
@@ -5,12 +5,16 @@ namespace MxGateway.Worker.MxAccess;
public sealed class MxAccessShutdownResult
{
/// <summary>Initializes a new instance of the <see cref="MxAccessShutdownResult"/> class.</summary>
/// <param name="failures">List of failures encountered during graceful shutdown.</param>
public MxAccessShutdownResult(IReadOnlyList<MxAccessShutdownFailure> failures)
{
Failures = failures ?? throw new ArgumentNullException(nameof(failures));
}
/// <summary>Gets the list of shutdown failures.</summary>
public IReadOnlyList<MxAccessShutdownFailure> Failures { get; }
/// <summary>Gets a value indicating whether the shutdown succeeded with no failures.</summary>
public bool Succeeded => Failures.Count == 0;
}
@@ -18,6 +18,9 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
private MxAccessSession? session;
private bool disposed;
/// <summary>
/// Initializes a new instance of <see cref="MxAccessStaSession"/> with default dependencies.
/// </summary>
public MxAccessStaSession()
: this(
new StaRuntime(),
@@ -26,6 +29,12 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
{
}
/// <summary>
/// Initializes a new instance of <see cref="MxAccessStaSession"/> with custom STA runtime and factory.
/// </summary>
/// <param name="staRuntime">STA thread runtime.</param>
/// <param name="factory">MXAccess COM object factory.</param>
/// <param name="eventSink">Event sink for MXAccess events.</param>
public MxAccessStaSession(
StaRuntime staRuntime,
IMxAccessComObjectFactory factory,
@@ -34,6 +43,12 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
{
}
/// <summary>
/// Initializes a new instance of <see cref="MxAccessStaSession"/> with custom event queue.
/// </summary>
/// <param name="staRuntime">STA thread runtime.</param>
/// <param name="factory">MXAccess COM object factory.</param>
/// <param name="eventQueue">Event queue for buffering MXAccess events.</param>
public MxAccessStaSession(
StaRuntime staRuntime,
IMxAccessComObjectFactory factory,
@@ -42,6 +57,13 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
{
}
/// <summary>
/// Initializes a new instance of <see cref="MxAccessStaSession"/> with all dependencies.
/// </summary>
/// <param name="staRuntime">STA thread runtime.</param>
/// <param name="factory">MXAccess COM object factory.</param>
/// <param name="eventSink">Event sink for MXAccess events.</param>
/// <param name="eventQueue">Event queue for buffering MXAccess events.</param>
public MxAccessStaSession(
StaRuntime staRuntime,
IMxAccessComObjectFactory factory,
@@ -54,8 +76,17 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
this.eventQueue = eventQueue ?? throw new ArgumentNullException(nameof(eventQueue));
}
/// <summary>
/// Gets the event queue for this session.
/// </summary>
public MxAccessEventQueue EventQueue => eventQueue;
/// <summary>
/// Starts the MXAccess COM session asynchronously.
/// </summary>
/// <param name="workerProcessId">Worker process identifier.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Worker ready message.</returns>
public Task<WorkerReady> StartAsync(
int workerProcessId,
CancellationToken cancellationToken = default)
@@ -63,6 +94,13 @@ 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>
public Task<WorkerReady> StartAsync(
string sessionId,
int workerProcessId,
@@ -88,6 +126,11 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
cancellationToken);
}
/// <summary>
/// Dispatches a command to the STA thread for execution asynchronously.
/// </summary>
/// <param name="command">The command to dispatch.</param>
/// <returns>Command reply.</returns>
public Task<MxCommandReply> DispatchAsync(StaCommand command)
{
if (commandDispatcher is null)
@@ -98,6 +141,10 @@ 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>
public WorkerRuntimeHeartbeatSnapshot CaptureHeartbeat()
{
uint pendingCommandCount = 0;
@@ -117,26 +164,48 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
currentCommandCorrelationId);
}
/// <summary>
/// Requests graceful shutdown of the command dispatcher.
/// </summary>
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>
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>
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>
public bool CancelCommand(string correlationId)
{
return commandDispatcher?.CancelQueuedCommand(correlationId) ?? false;
}
/// <summary>
/// Gets the registered server handles asynchronously.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Registered server handles.</returns>
public Task<IReadOnlyList<RegisteredServerHandle>> GetRegisteredServerHandlesAsync(
CancellationToken cancellationToken = default)
{
@@ -150,6 +219,11 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
cancellationToken);
}
/// <summary>
/// Gets the registered item handles asynchronously.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Registered item handles.</returns>
public Task<IReadOnlyList<RegisteredItemHandle>> GetRegisteredItemHandlesAsync(
CancellationToken cancellationToken = default)
{
@@ -163,6 +237,11 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
cancellationToken);
}
/// <summary>
/// Gets the registered advice handles asynchronously.
/// </summary>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Registered advice handles.</returns>
public Task<IReadOnlyList<RegisteredAdviceHandle>> GetRegisteredAdviceHandlesAsync(
CancellationToken cancellationToken = default)
{
@@ -176,6 +255,12 @@ 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>
public async Task<MxAccessShutdownResult> ShutdownGracefullyAsync(
TimeSpan timeout,
CancellationToken cancellationToken = default)
@@ -238,6 +323,7 @@ public sealed class MxAccessStaSession : IWorkerRuntimeSession
return result;
}
/// <summary>Releases resources and shuts down the session.</summary>
public void Dispose()
{
if (disposed)
@@ -2,6 +2,10 @@ namespace MxGateway.Worker.MxAccess;
public sealed class RegisteredAdviceHandle
{
/// <summary>Initializes a new instance of the <see cref="RegisteredAdviceHandle"/> class.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <param name="adviceKind">Type of advice.</param>
public RegisteredAdviceHandle(
int serverHandle,
int itemHandle,
@@ -12,9 +16,12 @@ public sealed class RegisteredAdviceHandle
AdviceKind = adviceKind;
}
/// <summary>Gets the server handle.</summary>
public int ServerHandle { get; }
/// <summary>Gets the item handle.</summary>
public int ItemHandle { get; }
/// <summary>Gets the advice kind.</summary>
public MxAccessAdviceKind AdviceKind { get; }
}
@@ -1,7 +1,18 @@
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Metadata for an item handle registered in an MXAccess session.
/// </summary>
public sealed class RegisteredItemHandle
{
/// <summary>
/// Initializes a registered item handle with complete metadata.
/// </summary>
/// <param name="serverHandle">Handle returned by Register.</param>
/// <param name="itemHandle">Handle returned by AddItem.</param>
/// <param name="itemDefinition">Item definition (tag address).</param>
/// <param name="itemContext">Item context string.</param>
/// <param name="hasItemContext">Whether this item has an associated context.</param>
public RegisteredItemHandle(
int serverHandle,
int itemHandle,
@@ -16,13 +27,28 @@ public sealed class RegisteredItemHandle
HasItemContext = hasItemContext;
}
/// <summary>
/// Gets the server handle that owns this item.
/// </summary>
public int ServerHandle { get; }
/// <summary>
/// Gets the item handle within the server.
/// </summary>
public int ItemHandle { get; }
/// <summary>
/// Gets the item definition (tag address).
/// </summary>
public string ItemDefinition { get; }
/// <summary>
/// Gets the item context.
/// </summary>
public string ItemContext { get; }
/// <summary>
/// Gets a value indicating whether this item has an associated context.
/// </summary>
public bool HasItemContext { get; }
}
@@ -2,6 +2,9 @@ namespace MxGateway.Worker.MxAccess;
public sealed class RegisteredServerHandle
{
/// <summary>Initializes a new registered server handle.</summary>
/// <param name="serverHandle">MXAccess server handle.</param>
/// <param name="clientName">Client name associated with the handle.</param>
public RegisteredServerHandle(
int serverHandle,
string clientName)
@@ -10,7 +13,9 @@ public sealed class RegisteredServerHandle
ClientName = clientName;
}
/// <summary>The MXAccess server handle.</summary>
public int ServerHandle { get; }
/// <summary>The client name associated with this server handle.</summary>
public string ClientName { get; }
}
@@ -4,6 +4,12 @@ namespace MxGateway.Worker.MxAccess;
public sealed class WorkerRuntimeHeartbeatSnapshot
{
/// <summary>Initializes a new instance of the <see cref="WorkerRuntimeHeartbeatSnapshot"/> class.</summary>
/// <param name="lastStaActivityUtc">Timestamp of the last STA thread activity in UTC.</param>
/// <param name="pendingCommandCount">Number of commands awaiting processing.</param>
/// <param name="outboundEventQueueDepth">Current depth of the worker event queue.</param>
/// <param name="lastEventSequence">Sequence number of the most recent event.</param>
/// <param name="currentCommandCorrelationId">Correlation ID of the in-flight command.</param>
public WorkerRuntimeHeartbeatSnapshot(
DateTimeOffset lastStaActivityUtc,
uint pendingCommandCount,
@@ -18,13 +24,18 @@ public sealed class WorkerRuntimeHeartbeatSnapshot
CurrentCommandCorrelationId = currentCommandCorrelationId ?? string.Empty;
}
/// <summary>Gets the last STA activity timestamp in UTC.</summary>
public DateTimeOffset LastStaActivityUtc { get; }
/// <summary>Gets the pending command count.</summary>
public uint PendingCommandCount { get; }
/// <summary>Gets the current depth of the worker event queue.</summary>
public uint OutboundEventQueueDepth { get; }
/// <summary>Gets the sequence number of the most recent event.</summary>
public ulong LastEventSequence { get; }
/// <summary>Gets the correlation ID of the in-flight command.</summary>
public string CurrentCommandCorrelationId { get; }
}
@@ -1,8 +1,17 @@
namespace MxGateway.Worker.Sta;
/// <summary>
/// Initializes and uninitializes the COM apartment for the STA thread.
/// </summary>
public interface IStaComApartmentInitializer
{
/// <summary>
/// Initializes the COM apartment on the STA thread.
/// </summary>
void Initialize();
/// <summary>
/// Uninitializes the COM apartment on the STA thread.
/// </summary>
void Uninitialize();
}
@@ -4,5 +4,8 @@ namespace MxGateway.Worker.Sta;
public interface IStaCommandExecutor
{
/// <summary>Executes a command on the STA thread.</summary>
/// <param name="command">The command to execute.</param>
/// <returns>The command reply.</returns>
MxCommandReply Execute(StaCommand command);
}
+2
View File
@@ -2,7 +2,9 @@ namespace MxGateway.Worker.Sta;
internal interface IStaWorkItem
{
/// <summary>Cancels the work item before it executes on the STA thread.</summary>
void CancelBeforeExecution();
/// <summary>Executes the work item on the STA thread.</summary>
void Execute();
}
@@ -9,6 +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>
public void Initialize()
{
int hresult = CoInitializeEx(IntPtr.Zero, CoInitializeApartmentThreaded);
@@ -18,6 +19,7 @@ public sealed class StaComApartmentInitializer : IStaComApartmentInitializer
}
}
/// <summary>Uninitializes the COM apartment.</summary>
public void Uninitialize()
{
CoUninitialize();
+13
View File
@@ -7,6 +7,12 @@ namespace MxGateway.Worker.Sta;
public sealed class StaCommand
{
/// <summary>Initializes a new instance of the <see cref="StaCommand"/> class.</summary>
/// <param name="sessionId">Identifier of the session.</param>
/// <param name="correlationId">Correlation identifier for the command.</param>
/// <param name="command">The MXAccess command to execute.</param>
/// <param name="enqueueTimestamp">Timestamp when the command was enqueued.</param>
/// <param name="cancellationToken">Token to cancel the asynchronous operation.</param>
public StaCommand(
string sessionId,
string correlationId,
@@ -31,17 +37,24 @@ public sealed class StaCommand
CancellationToken = cancellationToken;
}
/// <summary>Gets the session ID for the STA command.</summary>
public string SessionId { get; }
/// <summary>Gets the correlation ID for the STA command.</summary>
public string CorrelationId { get; }
/// <summary>Gets the MXAccess command to execute.</summary>
public MxCommand Command { get; }
/// <summary>Gets the timestamp when the command was enqueued.</summary>
public Timestamp EnqueueTimestamp { get; }
/// <summary>Gets the token to cancel the asynchronous operation.</summary>
public CancellationToken CancellationToken { get; }
/// <summary>Gets the kind of the MXAccess command.</summary>
public MxCommandKind Kind => Command.Kind;
/// <summary>Gets the method name of the command.</summary>
public string MethodName => Kind.ToString();
}
@@ -20,6 +20,11 @@ public sealed class StaCommandDispatcher
private bool shutdownRequested;
private string currentCommandCorrelationId = string.Empty;
/// <summary>
/// Initializes a new instance of <see cref="StaCommandDispatcher"/> with default converter.
/// </summary>
/// <param name="staRuntime">STA thread runtime.</param>
/// <param name="commandExecutor">Command executor.</param>
public StaCommandDispatcher(
StaRuntime staRuntime,
IStaCommandExecutor commandExecutor)
@@ -27,6 +32,12 @@ public sealed class StaCommandDispatcher
{
}
/// <summary>
/// Initializes a new instance of <see cref="StaCommandDispatcher"/> with custom converter.
/// </summary>
/// <param name="staRuntime">STA thread runtime.</param>
/// <param name="commandExecutor">Command executor.</param>
/// <param name="hresultConverter">HResult converter.</param>
public StaCommandDispatcher(
StaRuntime staRuntime,
IStaCommandExecutor commandExecutor,
@@ -35,6 +46,13 @@ public sealed class StaCommandDispatcher
{
}
/// <summary>
/// Initializes a new instance of <see cref="StaCommandDispatcher"/> with all parameters.
/// </summary>
/// <param name="staRuntime">STA thread runtime.</param>
/// <param name="commandExecutor">Command executor.</param>
/// <param name="hresultConverter">HResult converter.</param>
/// <param name="maxPendingCommands">Maximum pending commands allowed.</param>
public StaCommandDispatcher(
StaRuntime staRuntime,
IStaCommandExecutor commandExecutor,
@@ -54,6 +72,9 @@ public sealed class StaCommandDispatcher
this.maxPendingCommands = maxPendingCommands;
}
/// <summary>
/// Gets the count of pending commands in the queue.
/// </summary>
public int PendingCommandCount
{
get
@@ -65,6 +86,9 @@ public sealed class StaCommandDispatcher
}
}
/// <summary>
/// Gets the correlation ID of the currently executing command.
/// </summary>
public string CurrentCommandCorrelationId
{
get
@@ -76,6 +100,11 @@ public sealed class StaCommandDispatcher
}
}
/// <summary>
/// Dispatches a command to the queue for asynchronous STA execution.
/// </summary>
/// <param name="command">The command to dispatch.</param>
/// <returns>Task for the command reply.</returns>
public Task<MxCommandReply> DispatchAsync(StaCommand command)
{
if (command is null)
@@ -114,6 +143,11 @@ public sealed class StaCommandDispatcher
}
}
/// <summary>
/// Cancels a queued command by its correlation ID.
/// </summary>
/// <param name="correlationId">Correlation ID of the command to cancel.</param>
/// <returns>True if the command was canceled; otherwise false.</returns>
public bool CancelQueuedCommand(string correlationId)
{
if (string.IsNullOrWhiteSpace(correlationId))
@@ -159,6 +193,9 @@ public sealed class StaCommandDispatcher
}
}
/// <summary>
/// Requests graceful shutdown, rejecting all queued commands.
/// </summary>
public void RequestShutdown()
{
lock (gate)
@@ -175,6 +212,10 @@ public sealed class StaCommandDispatcher
}
}
/// <summary>
/// Populates the given heartbeat with current dispatcher state.
/// </summary>
/// <param name="heartbeat">Heartbeat to populate.</param>
public void PopulateHeartbeat(WorkerHeartbeat heartbeat)
{
if (heartbeat is null)
@@ -331,15 +372,29 @@ public sealed class StaCommandDispatcher
private readonly TaskCompletionSource<MxCommandReply> completion = new(
TaskCreationOptions.RunContinuationsAsynchronously);
/// <summary>
/// Initializes a new instance of <see cref="QueuedStaCommand"/>.
/// </summary>
/// <param name="command">The STA command to queue.</param>
public QueuedStaCommand(StaCommand command)
{
Command = command;
}
/// <summary>
/// Gets the queued STA command.
/// </summary>
public StaCommand Command { get; }
/// <summary>
/// Gets the task representing the command's completion.
/// </summary>
public Task<MxCommandReply> Task => completion.Task;
/// <summary>
/// Completes the command with the given reply.
/// </summary>
/// <param name="reply">The command reply.</param>
public void Complete(MxCommandReply reply)
{
completion.TrySetResult(reply);
@@ -5,6 +5,7 @@ using Microsoft.Win32.SafeHandles;
namespace MxGateway.Worker.Sta;
/// <summary>Pumps Windows messages on the STA thread to allow MXAccess COM events to deliver.</summary>
public sealed class StaMessagePump
{
private const uint Infinite = 0xFFFFFFFF;
@@ -13,6 +14,9 @@ public sealed class StaMessagePump
private const uint PmRemove = 0x0001;
private const uint QsAllInput = 0x04FF;
/// <summary>Waits for a command wake event or Windows messages, pumping any pending messages.</summary>
/// <param name="commandWakeEvent">Event to signal when work is available.</param>
/// <param name="timeout">Maximum time to wait; InfiniteTimeSpan waits indefinitely.</param>
public void WaitForWorkOrMessages(WaitHandle commandWakeEvent, TimeSpan timeout)
{
if (commandWakeEvent is null)
@@ -38,6 +42,7 @@ public sealed class StaMessagePump
}
}
/// <summary>Pumps and dispatches all pending Windows messages, returning the count processed.</summary>
public int PumpPendingMessages()
{
int pumpedMessages = 0;
+42
View File
@@ -23,11 +23,20 @@ public sealed class StaRuntime : IDisposable
private long lastActivityUtcTicks;
private bool comInitialized;
/// <summary>
/// Initializes a new instance of <see cref="StaRuntime"/> with default dependencies.
/// </summary>
public StaRuntime()
: this(new StaComApartmentInitializer(), new StaMessagePump(), TimeSpan.FromMilliseconds(50))
{
}
/// <summary>
/// Initializes a new instance of <see cref="StaRuntime"/> with custom dependencies.
/// </summary>
/// <param name="comApartmentInitializer">COM apartment initializer.</param>
/// <param name="messagePump">Message pump for the STA thread.</param>
/// <param name="idlePumpInterval">Interval for idle message pump waits.</param>
public StaRuntime(
IStaComApartmentInitializer comApartmentInitializer,
StaMessagePump messagePump,
@@ -54,13 +63,25 @@ public sealed class StaRuntime : IDisposable
staThread.SetApartmentState(ApartmentState.STA);
}
/// <summary>
/// Gets the managed thread ID of the STA thread.
/// </summary>
public int? StaThreadId { get; private set; }
/// <summary>
/// Gets the timestamp of the last STA thread activity.
/// </summary>
public DateTimeOffset LastActivityUtc =>
new(new DateTime(Volatile.Read(ref lastActivityUtcTicks), DateTimeKind.Utc));
/// <summary>
/// Gets a value indicating whether the STA runtime is currently running.
/// </summary>
public bool IsRunning => startedEvent.IsSet && !stoppedEvent.IsSet;
/// <summary>
/// Starts the STA thread.
/// </summary>
public void Start()
{
ThrowIfDisposed();
@@ -88,6 +109,12 @@ public sealed class StaRuntime : IDisposable
}
}
/// <summary>
/// Invokes an action on the STA thread asynchronously.
/// </summary>
/// <param name="command">Action to invoke on the STA thread.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Task that completes when the action executes.</returns>
public Task InvokeAsync(Action command, CancellationToken cancellationToken = default)
{
if (command is null)
@@ -104,6 +131,13 @@ public sealed class StaRuntime : IDisposable
cancellationToken);
}
/// <summary>
/// Invokes a function on the STA thread asynchronously.
/// </summary>
/// <typeparam name="T">Return type of the function.</typeparam>
/// <param name="command">Function to invoke on the STA thread.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Task that returns the function result.</returns>
public Task<T> InvokeAsync<T>(Func<T> command, CancellationToken cancellationToken = default)
{
if (command is null)
@@ -135,6 +169,11 @@ public sealed class StaRuntime : IDisposable
return workItem.Task;
}
/// <summary>
/// Requests graceful shutdown of the STA runtime within a timeout.
/// </summary>
/// <param name="timeout">Maximum time to wait for shutdown.</param>
/// <returns>True if shutdown completed; otherwise false.</returns>
public bool Shutdown(TimeSpan timeout)
{
if (timeout < TimeSpan.Zero && timeout != Timeout.InfiniteTimeSpan)
@@ -165,6 +204,9 @@ public sealed class StaRuntime : IDisposable
return stopped;
}
/// <summary>
/// Releases resources used by the STA runtime.
/// </summary>
public void Dispose()
{
if (disposed)
+9
View File
@@ -4,6 +4,9 @@ using System.Threading.Tasks;
namespace MxGateway.Worker.Sta;
/// <summary>
/// Encapsulates a work item to be executed on an STA thread with cancellation support.
/// </summary>
internal sealed class StaWorkItem<T> : IStaWorkItem
{
private readonly Func<T> command;
@@ -11,6 +14,9 @@ internal sealed class StaWorkItem<T> : IStaWorkItem
private readonly CancellationTokenRegistration cancellationRegistration;
private int started;
/// <summary>Initializes a work item with a command and cancellation token.</summary>
/// <param name="command">Function to execute on the STA thread.</param>
/// <param name="cancellationToken">Token to cancel the work item.</param>
public StaWorkItem(Func<T> command, CancellationToken cancellationToken)
{
this.command = command ?? throw new ArgumentNullException(nameof(command));
@@ -30,10 +36,12 @@ internal sealed class StaWorkItem<T> : IStaWorkItem
}
}
/// <summary>Gets the task that completes when work completes.</summary>
public Task<T> Task => Completion.Task;
private TaskCompletionSource<T> Completion { get; }
/// <summary>Cancels the work item before execution begins.</summary>
public void CancelBeforeExecution()
{
if (Interlocked.CompareExchange(ref started, 1, 0) == 0)
@@ -43,6 +51,7 @@ internal sealed class StaWorkItem<T> : IStaWorkItem
}
}
/// <summary>Executes the work item command.</summary>
public void Execute()
{
if (Interlocked.CompareExchange(ref started, 1, 0) != 0)
+12
View File
@@ -6,8 +6,11 @@ using MxGateway.Worker.Ipc;
namespace MxGateway.Worker;
/// <summary>Entry point for the worker process.</summary>
public static class WorkerApplication
{
/// <summary>Initializes and runs the worker with default environment and logging.</summary>
/// <param name="args">Command-line arguments.</param>
public static int Run(string[] args)
{
return Run(
@@ -16,6 +19,10 @@ public static class WorkerApplication
new WorkerConsoleLogger(Console.Error));
}
/// <summary>Initializes and runs the worker with custom environment and logging.</summary>
/// <param name="args">Command-line arguments.</param>
/// <param name="environment">Worker environment for resolving configuration.</param>
/// <param name="logger">Worker logger for diagnostics.</param>
public static int Run(
string[] args,
IWorkerEnvironment environment,
@@ -28,6 +35,11 @@ public static class WorkerApplication
new WorkerPipeClient(logger));
}
/// <summary>Parses arguments, bootstraps the handshake, and runs the worker until shutdown.</summary>
/// <param name="args">Command-line arguments.</param>
/// <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>
public static int Run(
string[] args,
IWorkerEnvironment environment,