fix(worker): narrow AlarmAckWriter ArgumentException interop filter
Resolve the operator user, password and comment before the interop try blocks, and match ArgumentException as an MXAccess failure only when it carries E_INVALIDARG and is not ArgumentNullException or ArgumentOutOfRangeException. A programming error inside the try blocks now propagates instead of being reported as an authentication or write failure. Fixes #164 Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
This commit is contained in:
@@ -34,6 +34,9 @@ public sealed class AlarmAckWriter : IAlarmAckWriter
|
||||
/// </summary>
|
||||
public const int DefaultMaxCachedItems = 256;
|
||||
|
||||
/// <summary>E_INVALIDARG, which the interop raises as ArgumentException.</summary>
|
||||
private const int EInvalidArg = unchecked((int)0x80070057);
|
||||
|
||||
private readonly IMxAccessComObjectFactory? factory;
|
||||
private readonly MxAccessWriteCompletionCache completionCache;
|
||||
private readonly Action pumpStep;
|
||||
@@ -104,12 +107,20 @@ public sealed class AlarmAckWriter : IAlarmAckWriter
|
||||
if (string.IsNullOrWhiteSpace(itemAddress)) throw new ArgumentException("Ack item address is required.", nameof(itemAddress));
|
||||
if (ackOperator is null) throw new ArgumentNullException(nameof(ackOperator));
|
||||
|
||||
// Resolve every argument before the interop try blocks: their catch
|
||||
// filters accept E_INVALIDARG ArgumentExceptions as MXAccess failures, so
|
||||
// an argument error raised by this code must fail before reaching them.
|
||||
if (ackOperator.UserName is null) throw new ArgumentException("Operator user name is required.", nameof(ackOperator));
|
||||
string verifyUser = ackOperator.VerifyUser;
|
||||
string password = ackOperator.Password ?? string.Empty;
|
||||
string commentValue = comment ?? string.Empty;
|
||||
|
||||
IMxAccessServer mx = EnsureRegistered();
|
||||
|
||||
int userId;
|
||||
try
|
||||
{
|
||||
userId = mx.AuthenticateUser(serverHandle, ackOperator.VerifyUser, ackOperator.Password ?? string.Empty);
|
||||
userId = mx.AuthenticateUser(serverHandle, verifyUser, password);
|
||||
}
|
||||
catch (Exception ex) when (IsInteropFailure(ex))
|
||||
{
|
||||
@@ -130,7 +141,7 @@ public sealed class AlarmAckWriter : IAlarmAckWriter
|
||||
return AlarmAckResult.Create(
|
||||
AlarmAckOutcome.AuthenticationFailed,
|
||||
ex.HResult,
|
||||
$"Operator authentication failed for '{ackOperator.VerifyUser}' (HRESULT 0x{ex.HResult:X8}).");
|
||||
$"Operator authentication failed for '{verifyUser}' (HRESULT 0x{ex.HResult:X8}).");
|
||||
}
|
||||
|
||||
if (userId == 0)
|
||||
@@ -138,7 +149,7 @@ public sealed class AlarmAckWriter : IAlarmAckWriter
|
||||
return AlarmAckResult.Create(
|
||||
AlarmAckOutcome.AuthenticationFailed,
|
||||
-1,
|
||||
$"Operator authentication failed for '{ackOperator.VerifyUser}' (no user id returned).");
|
||||
$"Operator authentication failed for '{verifyUser}' (no user id returned).");
|
||||
}
|
||||
|
||||
ulong baselineVersion = 0;
|
||||
@@ -177,7 +188,7 @@ public sealed class AlarmAckWriter : IAlarmAckWriter
|
||||
pumpStep();
|
||||
step = "Write";
|
||||
baselineVersion = completionCache.CurrentVersion(serverHandle, itemHandle);
|
||||
mx.Write(serverHandle, itemHandle, comment ?? string.Empty, userId);
|
||||
mx.Write(serverHandle, itemHandle, commentValue, userId);
|
||||
}
|
||||
catch (Exception ex) when (IsInteropFailure(ex))
|
||||
{
|
||||
@@ -249,10 +260,23 @@ public sealed class AlarmAckWriter : IAlarmAckWriter
|
||||
/// ArgumentException, E_ACCESSDENIED to UnauthorizedAccessException) and
|
||||
/// the rest to COMException. Each carries the HRESULT in Exception.HResult.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The ArgumentException arm exists because a wrong password or unknown
|
||||
/// user makes <c>AuthenticateUser</c> throw ArgumentException with
|
||||
/// E_INVALIDARG (0x80070057), observed live on windev on 2026-10-19. It
|
||||
/// is narrowed to that HRESULT, and excludes ArgumentNullException and
|
||||
/// ArgumentOutOfRangeException, so a programming error inside the try
|
||||
/// blocks propagates as a bug instead of being reported as an MXAccess
|
||||
/// authentication or write failure. The interop never raises those two
|
||||
/// subtypes for a COM HRESULT.
|
||||
/// </remarks>
|
||||
private static bool IsInteropFailure(Exception ex) =>
|
||||
ex is ExternalException
|
||||
|| ex is ArgumentException
|
||||
|| ex is UnauthorizedAccessException;
|
||||
|| ex is UnauthorizedAccessException
|
||||
|| (ex is ArgumentException
|
||||
&& ex is not ArgumentNullException
|
||||
&& ex is not ArgumentOutOfRangeException
|
||||
&& ex.HResult == EInvalidArg);
|
||||
|
||||
private static bool IsServerGone(int hresult) =>
|
||||
hresult == unchecked((int)0x80010108) // RPC_E_DISCONNECTED
|
||||
|
||||
Reference in New Issue
Block a user