feat(worker): implement 6 MXAccess COM commands in executor

Wire up the previously-unimplemented Suspend, Activate, AuthenticateUser,
ArchestrAUserToId, AddBufferedItem, and SetBufferedUpdateInterval command
kinds in MxAccessCommandExecutor. These are real COM calls and run on the
STA via the executor.

- IMxAccessServer gains the 6 methods; MxAccessComServer routes them to the
  right interface version (Suspend/Activate -> ILMXProxyServer4 out MxStatus,
  AuthenticateUser -> base ILMXProxyServer, ArchestrAUserToId ->
  ILMXProxyServer2, AddBufferedItem/SetBufferedUpdateInterval ->
  ILMXProxyServer5).
- Suspend/Activate surface the native MxStatus, converted to MxStatusProxy
  via the existing MxStatusProxyConverter.
- AuthenticateUser hands the credential straight to MXAccess and never logs
  it; native HResult failures propagate via the dispatcher.
- MxAccessSession gains matching pass-throughs; AddBufferedItem registers
  the item handle in the handle registry.
- Unit tests (fake IMxAccessServer / fake COM object) cover each arm plus a
  password-non-leak assertion; existing IMxAccessServer fakes updated.

No proto changes (all request/reply messages already exist).
This commit is contained in:
Joseph Doherty
2026-06-15 10:41:22 -04:00
parent f94c206489
commit 29399325d5
8 changed files with 948 additions and 8 deletions
@@ -259,6 +259,21 @@ public sealed class LmxSubtagAlarmSourceTests
{
}
public object Suspend(int serverHandle, int itemHandle) => new object();
public object Activate(int serverHandle, int itemHandle) => new object();
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) => 0;
public int ArchestrAUserToId(int serverHandle, string userIdGuid) => 0;
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
=> AddItem(serverHandle, itemDefinition);
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
{
}
internal sealed class WriteRecord
{
public WriteRecord(int serverHandle, int itemHandle, object? value, int userId)
@@ -33,6 +33,39 @@ public sealed class MxAccessComServerTests
Assert.Equal(new[] { "Register:client-a", "Advise:77:9", "Unregister:77" }, typed.Calls);
}
/// <summary>
/// The MXAccess command methods added in the worker COM commands bundle
/// (Suspend/Activate/AuthenticateUser/ArchestrAUserToId/AddBufferedItem/
/// SetBufferedUpdateInterval) route through the typed interface with their
/// arguments preserved, and the credential is never echoed back.
/// </summary>
[Fact]
public void CommandMethods_WithTypedServer_RouteThroughTypedInterface()
{
RecordingMxAccessServer typed = new(registerHandle: 5);
MxAccessComServer adapter = new(typed);
adapter.Suspend(serverHandle: 5, itemHandle: 11);
adapter.Activate(serverHandle: 5, itemHandle: 12);
adapter.AuthenticateUser(serverHandle: 5, verifyUser: "Administrator", verifyUserPassword: "s3cret");
adapter.ArchestrAUserToId(serverHandle: 5, userIdGuid: "guid-1");
adapter.AddBufferedItem(serverHandle: 5, itemDefinition: "TestInt", itemContext: "TestChildObject");
adapter.SetBufferedUpdateInterval(serverHandle: 5, updateIntervalMilliseconds: 250);
Assert.Equal(
new[]
{
"Suspend:5:11",
"Activate:5:12",
"AuthenticateUser:5:Administrator",
"ArchestrAUserToId:5:guid-1",
"AddBufferedItem:5:TestInt:TestChildObject",
"SetBufferedUpdateInterval:5:250",
},
typed.Calls);
Assert.DoesNotContain(typed.Calls, call => call.Contains("s3cret", StringComparison.Ordinal));
}
/// <summary>
/// A COM object that implements neither the typed COM interface family
/// nor <see cref="IMxAccessServer"/> fails fast with a clear
@@ -207,5 +240,60 @@ public sealed class MxAccessComServerTests
{
calls.Add($"WriteSecured2:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}:{timestamp}");
}
/// <summary>Records a Suspend call and returns a canned status.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
public object Suspend(int serverHandle, int itemHandle)
{
calls.Add($"Suspend:{serverHandle}:{itemHandle}");
return new object();
}
/// <summary>Records an Activate call and returns a canned status.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemHandle">The MXAccess item handle.</param>
public object Activate(int serverHandle, int itemHandle)
{
calls.Add($"Activate:{serverHandle}:{itemHandle}");
return new object();
}
/// <summary>Records an AuthenticateUser call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="verifyUser">The user name to authenticate.</param>
/// <param name="verifyUserPassword">The credential; recorded only as a fixed marker, never echoed.</param>
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
{
calls.Add($"AuthenticateUser:{serverHandle}:{verifyUser}");
return 0;
}
/// <summary>Records an ArchestrAUserToId call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="userIdGuid">The ArchestrA user GUID to resolve.</param>
public int ArchestrAUserToId(int serverHandle, string userIdGuid)
{
calls.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}");
return 0;
}
/// <summary>Records an AddBufferedItem call and returns zero.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="itemDefinition">The item definition string to record.</param>
/// <param name="itemContext">The item context string to record.</param>
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
{
calls.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}");
return 0;
}
/// <summary>Records a SetBufferedUpdateInterval call.</summary>
/// <param name="serverHandle">The MXAccess server handle.</param>
/// <param name="updateIntervalMilliseconds">The buffered update interval in milliseconds.</param>
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
{
calls.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}");
}
}
}
@@ -952,6 +952,295 @@ public sealed class MxAccessCommandExecutorTests
Assert.Null(fakeComObject.WriteServerHandle);
}
/// <summary>Verifies Suspend calls MXAccess on the STA and maps the native status to MxStatusProxy.</summary>
[Fact]
public async Task DispatchAsync_Suspend_CallsMxAccessOnStaAndMapsStatus()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 200);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
await session.DispatchAsync(CreateRegisterCommand("register-before-suspend", "client-a"));
MxCommandReply reply = await session.DispatchAsync(CreateSuspendCommand("suspend-1", 200, 21));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.Equal(0, reply.Hresult);
Assert.NotNull(reply.Suspend);
Assert.NotNull(reply.Suspend.Status);
Assert.Equal(1, reply.Suspend.Status.Success);
Assert.Equal(MxStatusCategory.Ok, reply.Suspend.Status.Category);
Assert.Equal(200, fakeComObject.SuspendServerHandle);
Assert.Equal(21, fakeComObject.SuspendItemHandle);
Assert.Equal(runtime.StaThreadId, fakeComObject.SuspendThreadId);
}
/// <summary>Verifies Activate calls MXAccess on the STA and maps the native status to MxStatusProxy.</summary>
[Fact]
public async Task DispatchAsync_Activate_CallsMxAccessOnStaAndMapsStatus()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 201);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
await session.DispatchAsync(CreateRegisterCommand("register-before-activate", "client-a"));
MxCommandReply reply = await session.DispatchAsync(CreateActivateCommand("activate-1", 201, 22));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.NotNull(reply.Activate);
Assert.NotNull(reply.Activate.Status);
Assert.Equal(1, reply.Activate.Status.Success);
Assert.Equal(MxStatusCategory.Ok, reply.Activate.Status.Category);
Assert.Equal(201, fakeComObject.ActivateServerHandle);
Assert.Equal(22, fakeComObject.ActivateItemHandle);
Assert.Equal(runtime.StaThreadId, fakeComObject.ActivateThreadId);
}
/// <summary>Verifies AuthenticateUser passes credentials to MXAccess on the STA and returns the user id.</summary>
[Fact]
public async Task DispatchAsync_AuthenticateUser_CallsMxAccessOnStaAndReturnsUserId()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 202);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
await session.DispatchAsync(CreateRegisterCommand("register-before-auth", "client-a"));
MxCommandReply reply = await session.DispatchAsync(
CreateAuthenticateUserCommand("auth-1", 202, "Administrator", string.Empty));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.NotNull(reply.AuthenticateUser);
Assert.Equal(1, reply.AuthenticateUser.UserId);
Assert.Equal(202, fakeComObject.AuthenticateServerHandle);
Assert.Equal("Administrator", fakeComObject.AuthenticateUserName);
Assert.Equal(runtime.StaThreadId, fakeComObject.AuthenticateThreadId);
}
/// <summary>
/// Verifies the AuthenticateUser path never surfaces the credential into the
/// command reply or any recorded diagnostic — the password is only ever
/// handed straight to the MXAccess wrapper.
/// </summary>
[Fact]
public async Task DispatchAsync_AuthenticateUser_DoesNotLeakPassword()
{
const string secret = "sup3r-secret-pw";
FakeMxAccessComObject fakeComObject = new(registerHandle: 203);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
await session.DispatchAsync(CreateRegisterCommand("register-before-auth-leak", "client-a"));
MxCommandReply reply = await session.DispatchAsync(
CreateAuthenticateUserCommand("auth-leak", 203, "Administrator", secret));
// The wrapper still receives the credential verbatim...
Assert.Equal(secret, fakeComObject.AuthenticatePassword);
// ...but the reply (diagnostics, status text) and the fake's operation
// log must never contain it.
Assert.DoesNotContain(secret, reply.DiagnosticMessage ?? string.Empty, StringComparison.Ordinal);
Assert.DoesNotContain(secret, reply.ProtocolStatus.Message ?? string.Empty, StringComparison.Ordinal);
Assert.DoesNotContain(fakeComObject.OperationNames, name => name.Contains(secret, StringComparison.Ordinal));
}
/// <summary>Verifies ArchestrAUserToId calls MXAccess on the STA and returns the resolved user id.</summary>
[Fact]
public async Task DispatchAsync_ArchestrAUserToId_CallsMxAccessOnStaAndReturnsUserId()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 204);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
await session.DispatchAsync(CreateRegisterCommand("register-before-user-to-id", "client-a"));
MxCommandReply reply = await session.DispatchAsync(
CreateArchestrAUserToIdCommand("user-to-id-1", 204, "11112222-3333-4444-5555-666677778888"));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.NotNull(reply.ArchestraUserToId);
Assert.Equal(7, reply.ArchestraUserToId.UserId);
Assert.Equal(204, fakeComObject.ArchestrAUserToIdServerHandle);
Assert.Equal("11112222-3333-4444-5555-666677778888", fakeComObject.ArchestrAUserToIdGuid);
}
/// <summary>Verifies AddBufferedItem calls MXAccess on the STA and tracks the buffered item handle.</summary>
[Fact]
public async Task DispatchAsync_AddBufferedItem_CallsMxAccessOnStaAndTracksItemHandle()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 205);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
await session.DispatchAsync(CreateRegisterCommand("register-before-buffered", "client-a"));
MxCommandReply reply = await session.DispatchAsync(
CreateAddBufferedItemCommand("buffered-1", 205, "TestInt", "TestChildObject"));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.NotNull(reply.AddBufferedItem);
Assert.Equal(1, reply.AddBufferedItem.ItemHandle);
Assert.Equal(MxDataType.Integer, reply.ReturnValue.DataType);
Assert.Equal(1, reply.ReturnValue.Int32Value);
Assert.Equal(205, fakeComObject.AddBufferedItemServerHandle);
Assert.Equal("TestInt", fakeComObject.AddBufferedItemDefinition);
Assert.Equal("TestChildObject", fakeComObject.AddBufferedItemContext);
RegisteredItemHandle registeredItemHandle = Assert.Single(
await session.GetRegisteredItemHandlesAsync());
Assert.Equal(205, registeredItemHandle.ServerHandle);
Assert.Equal(1, registeredItemHandle.ItemHandle);
Assert.Equal("TestInt", registeredItemHandle.ItemDefinition);
Assert.Equal("TestChildObject", registeredItemHandle.ItemContext);
Assert.True(registeredItemHandle.HasItemContext);
}
/// <summary>Verifies SetBufferedUpdateInterval calls MXAccess on the STA and returns a base OK reply.</summary>
[Fact]
public async Task DispatchAsync_SetBufferedUpdateInterval_CallsMxAccessOnStaAndReturnsOk()
{
FakeMxAccessComObject fakeComObject = new(registerHandle: 206);
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
await session.DispatchAsync(CreateRegisterCommand("register-before-interval", "client-a"));
MxCommandReply reply = await session.DispatchAsync(
CreateSetBufferedUpdateIntervalCommand("interval-1", 206, 500));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.Equal(0, reply.Hresult);
Assert.Equal(206, fakeComObject.SetBufferedUpdateIntervalServerHandle);
Assert.Equal(500, fakeComObject.SetBufferedUpdateIntervalValue);
}
private static StaCommand CreateSuspendCommand(
string correlationId,
int serverHandle,
int itemHandle)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.Suspend,
Suspend = new SuspendCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
});
}
private static StaCommand CreateActivateCommand(
string correlationId,
int serverHandle,
int itemHandle)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.Activate,
Activate = new ActivateCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
});
}
private static StaCommand CreateAuthenticateUserCommand(
string correlationId,
int serverHandle,
string verifyUser,
string verifyUserPassword)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.AuthenticateUser,
AuthenticateUser = new AuthenticateUserCommand
{
ServerHandle = serverHandle,
VerifyUser = verifyUser,
VerifyUserPassword = verifyUserPassword,
},
});
}
private static StaCommand CreateArchestrAUserToIdCommand(
string correlationId,
int serverHandle,
string userIdGuid)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.ArchestraUserToId,
ArchestraUserToId = new ArchestrAUserToIdCommand
{
ServerHandle = serverHandle,
UserIdGuid = userIdGuid,
},
});
}
private static StaCommand CreateAddBufferedItemCommand(
string correlationId,
int serverHandle,
string itemDefinition,
string itemContext)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.AddBufferedItem,
AddBufferedItem = new AddBufferedItemCommand
{
ServerHandle = serverHandle,
ItemDefinition = itemDefinition,
ItemContext = itemContext,
},
});
}
private static StaCommand CreateSetBufferedUpdateIntervalCommand(
string correlationId,
int serverHandle,
int updateIntervalMilliseconds)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.SetBufferedUpdateInterval,
SetBufferedUpdateInterval = new SetBufferedUpdateIntervalCommand
{
ServerHandle = serverHandle,
UpdateIntervalMilliseconds = updateIntervalMilliseconds,
},
});
}
private static StaCommand CreateRegisterCommand(
string correlationId,
string clientName)
@@ -1810,6 +2099,151 @@ public sealed class MxAccessCommandExecutorTests
throw exception;
}
}
/// <summary>Gets the server handle passed to Suspend, if called.</summary>
public int? SuspendServerHandle { get; private set; }
/// <summary>Gets the item handle passed to Suspend, if called.</summary>
public int? SuspendItemHandle { get; private set; }
/// <summary>Gets the thread ID on which Suspend was called.</summary>
public int? SuspendThreadId { get; private set; }
/// <summary>Gets the server handle passed to Activate, if called.</summary>
public int? ActivateServerHandle { get; private set; }
/// <summary>Gets the item handle passed to Activate, if called.</summary>
public int? ActivateItemHandle { get; private set; }
/// <summary>Gets the thread ID on which Activate was called.</summary>
public int? ActivateThreadId { get; private set; }
/// <summary>Gets the server handle passed to AuthenticateUser, if called.</summary>
public int? AuthenticateServerHandle { get; private set; }
/// <summary>Gets the user name passed to AuthenticateUser, if called.</summary>
public string? AuthenticateUserName { get; private set; }
/// <summary>Gets the credential passed to AuthenticateUser, if called. Used only to prove non-logging.</summary>
public string? AuthenticatePassword { get; private set; }
/// <summary>Gets the thread ID on which AuthenticateUser was called.</summary>
public int? AuthenticateThreadId { get; private set; }
/// <summary>Gets the server handle passed to ArchestrAUserToId, if called.</summary>
public int? ArchestrAUserToIdServerHandle { get; private set; }
/// <summary>Gets the GUID passed to ArchestrAUserToId, if called.</summary>
public string? ArchestrAUserToIdGuid { get; private set; }
/// <summary>Gets the server handle passed to AddBufferedItem, if called.</summary>
public int? AddBufferedItemServerHandle { get; private set; }
/// <summary>Gets the item definition passed to AddBufferedItem, if called.</summary>
public string? AddBufferedItemDefinition { get; private set; }
/// <summary>Gets the item context passed to AddBufferedItem, if called.</summary>
public string? AddBufferedItemContext { get; private set; }
/// <summary>Gets the server handle passed to SetBufferedUpdateInterval, if called.</summary>
public int? SetBufferedUpdateIntervalServerHandle { get; private set; }
/// <summary>Gets the interval passed to SetBufferedUpdateInterval, if called.</summary>
public int? SetBufferedUpdateIntervalValue { get; private set; }
/// <summary>Suspends an item and returns a canned status whose fields drive MxStatusProxy conversion.</summary>
/// <param name="serverHandle">Server handle for the suspend.</param>
/// <param name="itemHandle">Item handle to suspend.</param>
/// <returns>A status stand-in with all-OK fields.</returns>
public object Suspend(int serverHandle, int itemHandle)
{
operationNames.Add($"Suspend:{serverHandle}:{itemHandle}");
SuspendServerHandle = serverHandle;
SuspendItemHandle = itemHandle;
SuspendThreadId = Environment.CurrentManagedThreadId;
return new FakeMxStatus { success = 1, category = 0, detectedBy = 0, detail = 0 };
}
/// <summary>Activates an item and returns a canned status whose fields drive MxStatusProxy conversion.</summary>
/// <param name="serverHandle">Server handle for the activate.</param>
/// <param name="itemHandle">Item handle to activate.</param>
/// <returns>A status stand-in with all-OK fields.</returns>
public object Activate(int serverHandle, int itemHandle)
{
operationNames.Add($"Activate:{serverHandle}:{itemHandle}");
ActivateServerHandle = serverHandle;
ActivateItemHandle = itemHandle;
ActivateThreadId = Environment.CurrentManagedThreadId;
return new FakeMxStatus { success = 1, category = 0, detectedBy = 0, detail = 0 };
}
/// <summary>Authenticates a user and returns a canned user id.</summary>
/// <param name="serverHandle">Server handle for the authentication.</param>
/// <param name="verifyUser">User name to authenticate.</param>
/// <param name="verifyUserPassword">Credential; recorded only to assert it is never logged.</param>
/// <returns>The canned MXAccess user id (1).</returns>
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
{
// Deliberately does NOT include the password in the operation log.
operationNames.Add($"AuthenticateUser:{serverHandle}:{verifyUser}");
AuthenticateServerHandle = serverHandle;
AuthenticateUserName = verifyUser;
AuthenticatePassword = verifyUserPassword;
AuthenticateThreadId = Environment.CurrentManagedThreadId;
return 1;
}
/// <summary>Resolves an ArchestrA user GUID and returns a canned user id.</summary>
/// <param name="serverHandle">Server handle for the resolution.</param>
/// <param name="userIdGuid">ArchestrA user GUID to resolve.</param>
/// <returns>The canned MXAccess user id (7).</returns>
public int ArchestrAUserToId(int serverHandle, string userIdGuid)
{
operationNames.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}");
ArchestrAUserToIdServerHandle = serverHandle;
ArchestrAUserToIdGuid = userIdGuid;
return 7;
}
/// <summary>Adds a buffered item and returns a canned item handle.</summary>
/// <param name="serverHandle">Server handle to add the item to.</param>
/// <param name="itemDefinition">Item definition string.</param>
/// <param name="itemContext">Item context string.</param>
/// <returns>The canned buffered item handle (1).</returns>
public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
{
operationNames.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}");
AddBufferedItemServerHandle = serverHandle;
AddBufferedItemDefinition = itemDefinition;
AddBufferedItemContext = itemContext;
return 1;
}
/// <summary>Sets the buffered update interval and tracks the operation.</summary>
/// <param name="serverHandle">Server handle for the interval change.</param>
/// <param name="updateIntervalMilliseconds">Buffered update interval in milliseconds.</param>
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
{
operationNames.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}");
SetBufferedUpdateIntervalServerHandle = serverHandle;
SetBufferedUpdateIntervalValue = updateIntervalMilliseconds;
}
/// <summary>Status stand-in reflected over by the worker's MxStatusProxy converter.</summary>
internal sealed class FakeMxStatus
{
/// <summary>Success indicator read by the status converter.</summary>
public int success;
/// <summary>Status category read by the status converter.</summary>
public int category;
/// <summary>Status detected-by read by the status converter.</summary>
public int detectedBy;
/// <summary>Status detail read by the status converter.</summary>
public int detail;
}
}
/// <summary>Factory for creating fake MXAccess COM objects in tests.</summary>
@@ -1,3 +1,4 @@
using ZB.MOM.WW.MxGateway.Worker.Conversion;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
namespace ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
@@ -55,14 +56,10 @@ internal sealed class NoopMxAccessServer : IMxAccessServer
}
/// <inheritdoc />
public void Suspend(int serverHandle, int itemHandle)
{
}
public object Suspend(int serverHandle, int itemHandle) => new FakeMxStatus();
/// <inheritdoc />
public void Activate(int serverHandle, int itemHandle)
{
}
public object Activate(int serverHandle, int itemHandle) => new FakeMxStatus();
/// <inheritdoc />
public void Write(int serverHandle, int itemHandle, object? value, int userId)
@@ -85,8 +82,29 @@ internal sealed class NoopMxAccessServer : IMxAccessServer
}
/// <inheritdoc />
public int AuthenticateUser(string userName, string password) => 0;
public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) => 0;
/// <inheritdoc />
public int ArchestrAUserToId(string userName) => 0;
public int ArchestrAUserToId(int serverHandle, string userIdGuid) => 0;
}
/// <summary>
/// Minimal stand-in for the native <c>ArchestrA.MxAccess.MxStatus</c> struct.
/// <see cref="MxStatusProxyConverter"/> reflects over the public
/// <c>success</c>, <c>category</c>, <c>detectedBy</c>, and <c>detail</c>
/// fields, so this fake exposes the same field shape with all-OK values.
/// </summary>
internal sealed class FakeMxStatus
{
/// <summary>Success indicator field read by the status converter.</summary>
public int success;
/// <summary>Status category field read by the status converter.</summary>
public int category;
/// <summary>Status detected-by field read by the status converter.</summary>
public int detectedBy;
/// <summary>Status detail field read by the status converter.</summary>
public int detail;
}
@@ -57,6 +57,68 @@ public interface IMxAccessServer
int serverHandle,
int itemHandle);
/// <summary>Suspends data acquisition for an advised item (ILMXProxyServer4).</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to suspend.</param>
/// <returns>
/// The native MXAccess <c>MxStatus</c> value (boxed) produced by the call.
/// Callers convert it to a protobuf <c>MxStatusProxy</c> via the worker's
/// status converter; the underlying type is reflected over, not cast.
/// </returns>
object Suspend(
int serverHandle,
int itemHandle);
/// <summary>Reactivates data acquisition for a suspended item (ILMXProxyServer4).</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to activate.</param>
/// <returns>
/// The native MXAccess <c>MxStatus</c> value (boxed) produced by the call.
/// Callers convert it to a protobuf <c>MxStatusProxy</c> via the worker's
/// status converter; the underlying type is reflected over, not cast.
/// </returns>
object Activate(
int serverHandle,
int itemHandle);
/// <summary>Authenticates an MXAccess user and returns its user id (base ILMXProxyServer).</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="verifyUser">MXAccess user name to authenticate.</param>
/// <param name="verifyUserPassword">
/// Raw MXAccess credential. Implementations must keep this value out of
/// logs, metrics, command lines, and diagnostics.
/// </param>
/// <returns>The MXAccess user id for the authenticated user.</returns>
int AuthenticateUser(
int serverHandle,
string verifyUser,
string verifyUserPassword);
/// <summary>Resolves an ArchestrA user GUID to an MXAccess user id (ILMXProxyServer2).</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="userIdGuid">ArchestrA user GUID to resolve.</param>
/// <returns>The MXAccess user id for the resolved user.</returns>
int ArchestrAUserToId(
int serverHandle,
string userIdGuid);
/// <summary>Adds a buffered item to a server and returns an item handle (ILMXProxyServer5).</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 buffered item.</returns>
int AddBufferedItem(
int serverHandle,
string itemDefinition,
string itemContext);
/// <summary>Sets the buffered-update interval for a server (ILMXProxyServer5).</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="updateIntervalMilliseconds">Buffered update interval in milliseconds.</param>
void SetBufferedUpdateInterval(
int serverHandle,
int updateIntervalMilliseconds);
/// <summary>Writes a value to an item.</summary>
/// <param name="serverHandle">Server handle identifying the registration.</param>
/// <param name="itemHandle">Item handle to write to.</param>
@@ -140,6 +140,89 @@ public sealed class MxAccessComServer : IMxAccessServer
AsProxyServer4().AdviseSupervisory(serverHandle, itemHandle);
}
/// <inheritdoc />
public object Suspend(
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
return typedFake.Suspend(serverHandle, itemHandle);
}
AsProxyServer4().Suspend(serverHandle, itemHandle, out MxStatus status);
return status;
}
/// <inheritdoc />
public object Activate(
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
return typedFake.Activate(serverHandle, itemHandle);
}
AsProxyServer4().Activate(serverHandle, itemHandle, out MxStatus status);
return status;
}
/// <inheritdoc />
public int AuthenticateUser(
int serverHandle,
string verifyUser,
string verifyUserPassword)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
return typedFake.AuthenticateUser(serverHandle, verifyUser, verifyUserPassword);
}
return AsProxyServer().AuthenticateUser(serverHandle, verifyUser, verifyUserPassword);
}
/// <inheritdoc />
public int ArchestrAUserToId(
int serverHandle,
string userIdGuid)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
return typedFake.ArchestrAUserToId(serverHandle, userIdGuid);
}
return AsProxyServer2().ArchestrAUserToId(serverHandle, userIdGuid);
}
/// <inheritdoc />
public int AddBufferedItem(
int serverHandle,
string itemDefinition,
string itemContext)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
return typedFake.AddBufferedItem(serverHandle, itemDefinition, itemContext);
}
return AsProxyServer5().AddBufferedItem(serverHandle, itemDefinition, itemContext);
}
/// <inheritdoc />
public void SetBufferedUpdateInterval(
int serverHandle,
int updateIntervalMilliseconds)
{
if (mxAccessComObject is IMxAccessServer typedFake)
{
typedFake.SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds);
return;
}
AsProxyServer5().SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds);
}
/// <inheritdoc />
public void Write(
int serverHandle,
@@ -216,6 +299,14 @@ public sealed class MxAccessComServer : IMxAccessServer
+ $"{nameof(ILMXProxyServer)} or {nameof(IMxAccessServer)}.");
}
private ILMXProxyServer2 AsProxyServer2()
{
return mxAccessComObject as ILMXProxyServer2
?? throw new InvalidOperationException(
$"MXAccess COM object of type '{mxAccessComObject.GetType().FullName}' does not implement "
+ $"{nameof(ILMXProxyServer2)} or {nameof(IMxAccessServer)}.");
}
private ILMXProxyServer3 AsProxyServer3()
{
return mxAccessComObject as ILMXProxyServer3
@@ -231,4 +322,12 @@ public sealed class MxAccessComServer : IMxAccessServer
$"MXAccess COM object of type '{mxAccessComObject.GetType().FullName}' does not implement "
+ $"{nameof(ILMXProxyServer4)} or {nameof(IMxAccessServer)}.");
}
private ILMXProxyServer5 AsProxyServer5()
{
return mxAccessComObject as ILMXProxyServer5
?? throw new InvalidOperationException(
$"MXAccess COM object of type '{mxAccessComObject.GetType().FullName}' does not implement "
+ $"{nameof(ILMXProxyServer5)} or {nameof(IMxAccessServer)}.");
}
}
@@ -16,6 +16,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
private readonly MxAccessSession session;
private readonly VariantConverter variantConverter;
private readonly MxStatusProxyConverter statusProxyConverter;
private readonly IAlarmCommandHandler? alarmCommandHandler;
private readonly Action pumpStep;
@@ -78,6 +79,7 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
{
this.session = session ?? throw new ArgumentNullException(nameof(session));
this.variantConverter = variantConverter ?? throw new ArgumentNullException(nameof(variantConverter));
this.statusProxyConverter = new MxStatusProxyConverter();
this.alarmCommandHandler = alarmCommandHandler;
this.pumpStep = pumpStep ?? (static () => { });
}
@@ -104,6 +106,12 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
MxCommandKind.Advise => ExecuteAdvise(command),
MxCommandKind.UnAdvise => ExecuteUnAdvise(command),
MxCommandKind.AdviseSupervisory => ExecuteAdviseSupervisory(command),
MxCommandKind.Suspend => ExecuteSuspend(command),
MxCommandKind.Activate => ExecuteActivate(command),
MxCommandKind.AuthenticateUser => ExecuteAuthenticateUser(command),
MxCommandKind.ArchestraUserToId => ExecuteArchestrAUserToId(command),
MxCommandKind.AddBufferedItem => ExecuteAddBufferedItem(command),
MxCommandKind.SetBufferedUpdateInterval => ExecuteSetBufferedUpdateInterval(command),
MxCommandKind.Write => ExecuteWrite(command),
MxCommandKind.Write2 => ExecuteWrite2(command),
MxCommandKind.WriteSecured => ExecuteWriteSecured(command),
@@ -262,6 +270,134 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
return CreateOkReply(command);
}
private MxCommandReply ExecuteSuspend(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Suspend)
{
return CreateInvalidRequestReply(command, "Suspend command payload is required.");
}
SuspendCommand suspendCommand = command.Command.Suspend;
object nativeStatus = session.Suspend(
suspendCommand.ServerHandle,
suspendCommand.ItemHandle);
MxCommandReply reply = CreateOkReply(command);
reply.Suspend = new SuspendReply
{
Status = statusProxyConverter.Convert(nativeStatus),
};
return reply;
}
private MxCommandReply ExecuteActivate(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Activate)
{
return CreateInvalidRequestReply(command, "Activate command payload is required.");
}
ActivateCommand activateCommand = command.Command.Activate;
object nativeStatus = session.Activate(
activateCommand.ServerHandle,
activateCommand.ItemHandle);
MxCommandReply reply = CreateOkReply(command);
reply.Activate = new ActivateReply
{
Status = statusProxyConverter.Convert(nativeStatus),
};
return reply;
}
private MxCommandReply ExecuteAuthenticateUser(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AuthenticateUser)
{
return CreateInvalidRequestReply(command, "AuthenticateUser command payload is required.");
}
AuthenticateUserCommand authenticateUserCommand = command.Command.AuthenticateUser;
// The credential (verify_user_password) is passed straight to MXAccess
// and is never written to logs, diagnostics, or the reply. MXAccess is
// allowed to fail authentication; the native HResult is surfaced by the
// dispatcher's exception path.
int userId = session.AuthenticateUser(
authenticateUserCommand.ServerHandle,
authenticateUserCommand.VerifyUser,
authenticateUserCommand.VerifyUserPassword);
MxCommandReply reply = CreateOkReply(command);
reply.AuthenticateUser = new AuthenticateUserReply
{
UserId = userId,
};
return reply;
}
private MxCommandReply ExecuteArchestrAUserToId(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.ArchestraUserToId)
{
return CreateInvalidRequestReply(command, "ArchestrAUserToId command payload is required.");
}
ArchestrAUserToIdCommand archestrAUserToIdCommand = command.Command.ArchestraUserToId;
int userId = session.ArchestrAUserToId(
archestrAUserToIdCommand.ServerHandle,
archestrAUserToIdCommand.UserIdGuid);
MxCommandReply reply = CreateOkReply(command);
reply.ArchestraUserToId = new ArchestrAUserToIdReply
{
UserId = userId,
};
return reply;
}
private MxCommandReply ExecuteAddBufferedItem(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AddBufferedItem)
{
return CreateInvalidRequestReply(command, "AddBufferedItem command payload is required.");
}
AddBufferedItemCommand addBufferedItemCommand = command.Command.AddBufferedItem;
int itemHandle = session.AddBufferedItem(
addBufferedItemCommand.ServerHandle,
addBufferedItemCommand.ItemDefinition,
addBufferedItemCommand.ItemContext);
MxCommandReply reply = CreateOkReply(command);
reply.ReturnValue = variantConverter.Convert(itemHandle);
reply.AddBufferedItem = new AddBufferedItemReply
{
ItemHandle = itemHandle,
};
return reply;
}
private MxCommandReply ExecuteSetBufferedUpdateInterval(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.SetBufferedUpdateInterval)
{
return CreateInvalidRequestReply(command, "SetBufferedUpdateInterval command payload is required.");
}
SetBufferedUpdateIntervalCommand setBufferedUpdateIntervalCommand = command.Command.SetBufferedUpdateInterval;
session.SetBufferedUpdateInterval(
setBufferedUpdateIntervalCommand.ServerHandle,
setBufferedUpdateIntervalCommand.UpdateIntervalMilliseconds);
return CreateOkReply(command);
}
private MxCommandReply ExecuteWrite(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Write)
@@ -300,6 +300,94 @@ public sealed class MxAccessSession : IDisposable
MxAccessAdviceKind.Supervisory);
}
/// <summary>Suspends data acquisition for an advised item and returns the native MXAccess status.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <returns>The boxed native MXAccess status produced by the call.</returns>
public object Suspend(
int serverHandle,
int itemHandle)
{
ThrowIfDisposed();
return mxAccessServer.Suspend(serverHandle, itemHandle);
}
/// <summary>Reactivates data acquisition for a suspended item and returns the native MXAccess status.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>
/// <returns>The boxed native MXAccess status produced by the call.</returns>
public object Activate(
int serverHandle,
int itemHandle)
{
ThrowIfDisposed();
return mxAccessServer.Activate(serverHandle, itemHandle);
}
/// <summary>Authenticates an MXAccess user and returns its user id.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="verifyUser">MXAccess user name to authenticate.</param>
/// <param name="verifyUserPassword">Raw MXAccess credential; never logged.</param>
/// <returns>The MXAccess user id for the authenticated user.</returns>
public int AuthenticateUser(
int serverHandle,
string verifyUser,
string verifyUserPassword)
{
ThrowIfDisposed();
return mxAccessServer.AuthenticateUser(serverHandle, verifyUser, verifyUserPassword);
}
/// <summary>Resolves an ArchestrA user GUID to an MXAccess user id.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="userIdGuid">ArchestrA user GUID to resolve.</param>
/// <returns>The MXAccess user id for the resolved user.</returns>
public int ArchestrAUserToId(
int serverHandle,
string userIdGuid)
{
ThrowIfDisposed();
return mxAccessServer.ArchestrAUserToId(serverHandle, userIdGuid);
}
/// <summary>Adds a buffered 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>
/// <param name="itemContext">Context string for the item.</param>
public int AddBufferedItem(
int serverHandle,
string itemDefinition,
string itemContext)
{
ThrowIfDisposed();
int itemHandle = mxAccessServer.AddBufferedItem(serverHandle, itemDefinition, itemContext);
handleRegistry.RegisterItemHandle(
serverHandle,
itemHandle,
itemDefinition,
itemContext,
hasItemContext: true);
return itemHandle;
}
/// <summary>Sets the buffered-update interval for an MXAccess server.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="updateIntervalMilliseconds">Buffered update interval in milliseconds.</param>
public void SetBufferedUpdateInterval(
int serverHandle,
int updateIntervalMilliseconds)
{
ThrowIfDisposed();
mxAccessServer.SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds);
}
/// <summary>Writes a value to an item.</summary>
/// <param name="serverHandle">Handle returned by the worker.</param>
/// <param name="itemHandle">Handle returned by the worker.</param>