Resolve Worker-004, -005, -006, -007, -008 code-review findings

Worker-004: post-watchdog-fault heartbeats reported a non-faulted state.
ReportWatchdogFaultIfNeededAsync now sets _state = Faulted before writing
the StaHung fault.

Worker-005 (re-triaged): the cited OnPoll site was removed by Worker-001;
the real silent-failure bug was in MxAccessStaSession.RunAlarmPollLoopAsync,
which caught only graceful-stop exceptions. A failing PollOnce now records a
WorkerFault on the event queue instead of vanishing on a non-awaited task.

Worker-006: RunAsync's finally skipped runtime disposal when shutdown timed
out, leaking the STA thread and COM object. It now always disposes
(MxAccessStaSession.Dispose is idempotent and bounded).

Worker-007 (re-triaged): replaced MxAccessComServer's Type.InvokeMember
reflection fallback with an IMxAccessServer fast path plus typed
ILMXProxyServer* casts; a non-conforming object now fails fast.

Worker-008: alarm consumer STA affinity was unenforced. MxAccessStaSession
records the alarm consumer's STA thread id and asserts every PollOnce runs
on it via a unit-testable guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-18 21:31:23 -04:00
parent 1d9e3afadd
commit 54325343bd
8 changed files with 519 additions and 68 deletions
@@ -1,13 +1,22 @@
using System;
using System.Reflection;
using System.Runtime.ExceptionServices;
using ArchestrA.MxAccess;
namespace MxGateway.Worker.MxAccess;
/// <summary>
/// Adapter exposing MXAccess COM object methods through the IMxAccessServer interface.
/// Adapter exposing MXAccess COM object methods through the <see cref="IMxAccessServer"/>
/// interface.
/// </summary>
/// <remarks>
/// The supplied object must implement the typed MXAccess COM interface contract.
/// In production it is the <c>LMXProxyServerClass</c> RCW, which implements
/// <see cref="ILMXProxyServer"/> / <see cref="ILMXProxyServer3"/> /
/// <see cref="ILMXProxyServer4"/>. Tests substitute a typed fake that
/// implements <see cref="IMxAccessServer"/> directly. The earlier late-bound
/// <c>Type.InvokeMember</c> reflection fallback was removed: it bypassed the
/// typed interface contract, boxed value-type handles on every call, and only
/// ever served test doubles — a typed fake is the supported test seam now.
/// </remarks>
public sealed class MxAccessComServer : IMxAccessServer
{
private readonly object mxAccessComObject;
@@ -15,7 +24,11 @@ public sealed class MxAccessComServer : IMxAccessServer
/// <summary>
/// Initializes the adapter with the MXAccess COM object.
/// </summary>
/// <param name="mxAccessComObject">MXAccess COM object instance.</param>
/// <param name="mxAccessComObject">
/// MXAccess COM object instance. Must implement either the typed
/// <see cref="ILMXProxyServer"/> COM interface family (production) or
/// <see cref="IMxAccessServer"/> directly (test fakes).
/// </param>
public MxAccessComServer(object mxAccessComObject)
{
this.mxAccessComObject = mxAccessComObject ?? throw new ArgumentNullException(nameof(mxAccessComObject));
@@ -24,24 +37,24 @@ public sealed class MxAccessComServer : IMxAccessServer
/// <inheritdoc />
public int Register(string clientName)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
if (mxAccessComObject is IMxAccessServer typedFake)
{
return mxAccessServer.Register(clientName);
return typedFake.Register(clientName);
}
return (int)Invoke(nameof(Register), clientName);
return AsProxyServer().Register(clientName);
}
/// <inheritdoc />
public void Unregister(int serverHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
if (mxAccessComObject is IMxAccessServer typedFake)
{
mxAccessServer.Unregister(serverHandle);
typedFake.Unregister(serverHandle);
return;
}
Invoke(nameof(Unregister), serverHandle);
AsProxyServer().Unregister(serverHandle);
}
/// <inheritdoc />
@@ -49,12 +62,12 @@ public sealed class MxAccessComServer : IMxAccessServer
int serverHandle,
string itemDefinition)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
if (mxAccessComObject is IMxAccessServer typedFake)
{
return mxAccessServer.AddItem(serverHandle, itemDefinition);
return typedFake.AddItem(serverHandle, itemDefinition);
}
return (int)Invoke(nameof(AddItem), serverHandle, itemDefinition);
return AsProxyServer().AddItem(serverHandle, itemDefinition);
}
/// <inheritdoc />
@@ -63,12 +76,12 @@ public sealed class MxAccessComServer : IMxAccessServer
string itemDefinition,
string itemContext)
{
if (mxAccessComObject is ILMXProxyServer3 mxAccessServer)
if (mxAccessComObject is IMxAccessServer typedFake)
{
return mxAccessServer.AddItem2(serverHandle, itemDefinition, itemContext);
return typedFake.AddItem2(serverHandle, itemDefinition, itemContext);
}
return (int)Invoke(nameof(AddItem2), serverHandle, itemDefinition, itemContext);
return AsProxyServer3().AddItem2(serverHandle, itemDefinition, itemContext);
}
/// <inheritdoc />
@@ -76,13 +89,13 @@ public sealed class MxAccessComServer : IMxAccessServer
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
if (mxAccessComObject is IMxAccessServer typedFake)
{
mxAccessServer.RemoveItem(serverHandle, itemHandle);
typedFake.RemoveItem(serverHandle, itemHandle);
return;
}
Invoke(nameof(RemoveItem), serverHandle, itemHandle);
AsProxyServer().RemoveItem(serverHandle, itemHandle);
}
/// <inheritdoc />
@@ -90,13 +103,13 @@ public sealed class MxAccessComServer : IMxAccessServer
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
if (mxAccessComObject is IMxAccessServer typedFake)
{
mxAccessServer.Advise(serverHandle, itemHandle);
typedFake.Advise(serverHandle, itemHandle);
return;
}
Invoke(nameof(Advise), serverHandle, itemHandle);
AsProxyServer().Advise(serverHandle, itemHandle);
}
/// <inheritdoc />
@@ -104,13 +117,13 @@ public sealed class MxAccessComServer : IMxAccessServer
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
if (mxAccessComObject is IMxAccessServer typedFake)
{
mxAccessServer.UnAdvise(serverHandle, itemHandle);
typedFake.UnAdvise(serverHandle, itemHandle);
return;
}
Invoke(nameof(UnAdvise), serverHandle, itemHandle);
AsProxyServer().UnAdvise(serverHandle, itemHandle);
}
/// <inheritdoc />
@@ -118,34 +131,36 @@ public sealed class MxAccessComServer : IMxAccessServer
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is ILMXProxyServer4 mxAccessServer)
if (mxAccessComObject is IMxAccessServer typedFake)
{
mxAccessServer.AdviseSupervisory(serverHandle, itemHandle);
typedFake.AdviseSupervisory(serverHandle, itemHandle);
return;
}
Invoke(nameof(AdviseSupervisory), serverHandle, itemHandle);
AsProxyServer4().AdviseSupervisory(serverHandle, itemHandle);
}
private object Invoke(
string methodName,
params object[] arguments)
private ILMXProxyServer AsProxyServer()
{
try
{
return mxAccessComObject
.GetType()
.InvokeMember(
methodName,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod,
binder: null,
target: mxAccessComObject,
args: arguments);
}
catch (TargetInvocationException exception) when (exception.InnerException is not null)
{
ExceptionDispatchInfo.Capture(exception.InnerException).Throw();
throw;
}
return mxAccessComObject as ILMXProxyServer
?? throw new InvalidOperationException(
$"MXAccess COM object of type '{mxAccessComObject.GetType().FullName}' does not implement "
+ $"{nameof(ILMXProxyServer)} or {nameof(IMxAccessServer)}.");
}
private ILMXProxyServer3 AsProxyServer3()
{
return mxAccessComObject as ILMXProxyServer3
?? throw new InvalidOperationException(
$"MXAccess COM object of type '{mxAccessComObject.GetType().FullName}' does not implement "
+ $"{nameof(ILMXProxyServer3)} or {nameof(IMxAccessServer)}.");
}
private ILMXProxyServer4 AsProxyServer4()
{
return mxAccessComObject as ILMXProxyServer4
?? throw new InvalidOperationException(
$"MXAccess COM object of type '{mxAccessComObject.GetType().FullName}' does not implement "
+ $"{nameof(ILMXProxyServer4)} or {nameof(IMxAccessServer)}.");
}
}