diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs
index 08995f9..57c0f81 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/LmxSubtagAlarmSourceTests.cs
@@ -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)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs
index e821cac..5a795b0 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessComServerTests.cs
@@ -33,6 +33,39 @@ public sealed class MxAccessComServerTests
Assert.Equal(new[] { "Register:client-a", "Advise:77:9", "Unregister:77" }, typed.Calls);
}
+ ///
+ /// 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.
+ ///
+ [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));
+ }
+
///
/// A COM object that implements neither the typed COM interface family
/// nor fails fast with a clear
@@ -207,5 +240,60 @@ public sealed class MxAccessComServerTests
{
calls.Add($"WriteSecured2:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}:{timestamp}");
}
+
+ /// Records a Suspend call and returns a canned status.
+ /// The MXAccess server handle.
+ /// The MXAccess item handle.
+ public object Suspend(int serverHandle, int itemHandle)
+ {
+ calls.Add($"Suspend:{serverHandle}:{itemHandle}");
+ return new object();
+ }
+
+ /// Records an Activate call and returns a canned status.
+ /// The MXAccess server handle.
+ /// The MXAccess item handle.
+ public object Activate(int serverHandle, int itemHandle)
+ {
+ calls.Add($"Activate:{serverHandle}:{itemHandle}");
+ return new object();
+ }
+
+ /// Records an AuthenticateUser call and returns zero.
+ /// The MXAccess server handle.
+ /// The user name to authenticate.
+ /// The credential; recorded only as a fixed marker, never echoed.
+ public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword)
+ {
+ calls.Add($"AuthenticateUser:{serverHandle}:{verifyUser}");
+ return 0;
+ }
+
+ /// Records an ArchestrAUserToId call and returns zero.
+ /// The MXAccess server handle.
+ /// The ArchestrA user GUID to resolve.
+ public int ArchestrAUserToId(int serverHandle, string userIdGuid)
+ {
+ calls.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}");
+ return 0;
+ }
+
+ /// Records an AddBufferedItem call and returns zero.
+ /// The MXAccess server handle.
+ /// The item definition string to record.
+ /// The item context string to record.
+ public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
+ {
+ calls.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}");
+ return 0;
+ }
+
+ /// Records a SetBufferedUpdateInterval call.
+ /// The MXAccess server handle.
+ /// The buffered update interval in milliseconds.
+ public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
+ {
+ calls.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}");
+ }
}
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs
index f9d19fa..2bcf8c1 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs
@@ -952,6 +952,295 @@ public sealed class MxAccessCommandExecutorTests
Assert.Null(fakeComObject.WriteServerHandle);
}
+ /// Verifies Suspend calls MXAccess on the STA and maps the native status to MxStatusProxy.
+ [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);
+ }
+
+ /// Verifies Activate calls MXAccess on the STA and maps the native status to MxStatusProxy.
+ [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);
+ }
+
+ /// Verifies AuthenticateUser passes credentials to MXAccess on the STA and returns the user id.
+ [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);
+ }
+
+ ///
+ /// 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.
+ ///
+ [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));
+ }
+
+ /// Verifies ArchestrAUserToId calls MXAccess on the STA and returns the resolved user id.
+ [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);
+ }
+
+ /// Verifies AddBufferedItem calls MXAccess on the STA and tracks the buffered item handle.
+ [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);
+ }
+
+ /// Verifies SetBufferedUpdateInterval calls MXAccess on the STA and returns a base OK reply.
+ [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;
}
}
+
+ /// Gets the server handle passed to Suspend, if called.
+ public int? SuspendServerHandle { get; private set; }
+
+ /// Gets the item handle passed to Suspend, if called.
+ public int? SuspendItemHandle { get; private set; }
+
+ /// Gets the thread ID on which Suspend was called.
+ public int? SuspendThreadId { get; private set; }
+
+ /// Gets the server handle passed to Activate, if called.
+ public int? ActivateServerHandle { get; private set; }
+
+ /// Gets the item handle passed to Activate, if called.
+ public int? ActivateItemHandle { get; private set; }
+
+ /// Gets the thread ID on which Activate was called.
+ public int? ActivateThreadId { get; private set; }
+
+ /// Gets the server handle passed to AuthenticateUser, if called.
+ public int? AuthenticateServerHandle { get; private set; }
+
+ /// Gets the user name passed to AuthenticateUser, if called.
+ public string? AuthenticateUserName { get; private set; }
+
+ /// Gets the credential passed to AuthenticateUser, if called. Used only to prove non-logging.
+ public string? AuthenticatePassword { get; private set; }
+
+ /// Gets the thread ID on which AuthenticateUser was called.
+ public int? AuthenticateThreadId { get; private set; }
+
+ /// Gets the server handle passed to ArchestrAUserToId, if called.
+ public int? ArchestrAUserToIdServerHandle { get; private set; }
+
+ /// Gets the GUID passed to ArchestrAUserToId, if called.
+ public string? ArchestrAUserToIdGuid { get; private set; }
+
+ /// Gets the server handle passed to AddBufferedItem, if called.
+ public int? AddBufferedItemServerHandle { get; private set; }
+
+ /// Gets the item definition passed to AddBufferedItem, if called.
+ public string? AddBufferedItemDefinition { get; private set; }
+
+ /// Gets the item context passed to AddBufferedItem, if called.
+ public string? AddBufferedItemContext { get; private set; }
+
+ /// Gets the server handle passed to SetBufferedUpdateInterval, if called.
+ public int? SetBufferedUpdateIntervalServerHandle { get; private set; }
+
+ /// Gets the interval passed to SetBufferedUpdateInterval, if called.
+ public int? SetBufferedUpdateIntervalValue { get; private set; }
+
+ /// Suspends an item and returns a canned status whose fields drive MxStatusProxy conversion.
+ /// Server handle for the suspend.
+ /// Item handle to suspend.
+ /// A status stand-in with all-OK fields.
+ 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 };
+ }
+
+ /// Activates an item and returns a canned status whose fields drive MxStatusProxy conversion.
+ /// Server handle for the activate.
+ /// Item handle to activate.
+ /// A status stand-in with all-OK fields.
+ 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 };
+ }
+
+ /// Authenticates a user and returns a canned user id.
+ /// Server handle for the authentication.
+ /// User name to authenticate.
+ /// Credential; recorded only to assert it is never logged.
+ /// The canned MXAccess user id (1).
+ 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;
+ }
+
+ /// Resolves an ArchestrA user GUID and returns a canned user id.
+ /// Server handle for the resolution.
+ /// ArchestrA user GUID to resolve.
+ /// The canned MXAccess user id (7).
+ public int ArchestrAUserToId(int serverHandle, string userIdGuid)
+ {
+ operationNames.Add($"ArchestrAUserToId:{serverHandle}:{userIdGuid}");
+ ArchestrAUserToIdServerHandle = serverHandle;
+ ArchestrAUserToIdGuid = userIdGuid;
+ return 7;
+ }
+
+ /// Adds a buffered item and returns a canned item handle.
+ /// Server handle to add the item to.
+ /// Item definition string.
+ /// Item context string.
+ /// The canned buffered item handle (1).
+ public int AddBufferedItem(int serverHandle, string itemDefinition, string itemContext)
+ {
+ operationNames.Add($"AddBufferedItem:{serverHandle}:{itemDefinition}:{itemContext}");
+ AddBufferedItemServerHandle = serverHandle;
+ AddBufferedItemDefinition = itemDefinition;
+ AddBufferedItemContext = itemContext;
+ return 1;
+ }
+
+ /// Sets the buffered update interval and tracks the operation.
+ /// Server handle for the interval change.
+ /// Buffered update interval in milliseconds.
+ public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds)
+ {
+ operationNames.Add($"SetBufferedUpdateInterval:{serverHandle}:{updateIntervalMilliseconds}");
+ SetBufferedUpdateIntervalServerHandle = serverHandle;
+ SetBufferedUpdateIntervalValue = updateIntervalMilliseconds;
+ }
+
+ /// Status stand-in reflected over by the worker's MxStatusProxy converter.
+ internal sealed class FakeMxStatus
+ {
+ /// Success indicator read by the status converter.
+ public int success;
+
+ /// Status category read by the status converter.
+ public int category;
+
+ /// Status detected-by read by the status converter.
+ public int detectedBy;
+
+ /// Status detail read by the status converter.
+ public int detail;
+ }
}
/// Factory for creating fake MXAccess COM objects in tests.
diff --git a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs
index 1754ed2..b2964a3 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker.Tests/TestSupport/NoopMxAccessServer.cs
@@ -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
}
///
- public void Suspend(int serverHandle, int itemHandle)
- {
- }
+ public object Suspend(int serverHandle, int itemHandle) => new FakeMxStatus();
///
- public void Activate(int serverHandle, int itemHandle)
- {
- }
+ public object Activate(int serverHandle, int itemHandle) => new FakeMxStatus();
///
public void Write(int serverHandle, int itemHandle, object? value, int userId)
@@ -85,8 +82,29 @@ internal sealed class NoopMxAccessServer : IMxAccessServer
}
///
- public int AuthenticateUser(string userName, string password) => 0;
+ public int AuthenticateUser(int serverHandle, string verifyUser, string verifyUserPassword) => 0;
///
- public int ArchestrAUserToId(string userName) => 0;
+ public int ArchestrAUserToId(int serverHandle, string userIdGuid) => 0;
+}
+
+///
+/// Minimal stand-in for the native ArchestrA.MxAccess.MxStatus struct.
+/// reflects over the public
+/// success, category, detectedBy, and detail
+/// fields, so this fake exposes the same field shape with all-OK values.
+///
+internal sealed class FakeMxStatus
+{
+ /// Success indicator field read by the status converter.
+ public int success;
+
+ /// Status category field read by the status converter.
+ public int category;
+
+ /// Status detected-by field read by the status converter.
+ public int detectedBy;
+
+ /// Status detail field read by the status converter.
+ public int detail;
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessServer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessServer.cs
index ed5beb3..62f7d0d 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessServer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/IMxAccessServer.cs
@@ -57,6 +57,68 @@ public interface IMxAccessServer
int serverHandle,
int itemHandle);
+ /// Suspends data acquisition for an advised item (ILMXProxyServer4).
+ /// Server handle identifying the registration.
+ /// Item handle to suspend.
+ ///
+ /// The native MXAccess MxStatus value (boxed) produced by the call.
+ /// Callers convert it to a protobuf MxStatusProxy via the worker's
+ /// status converter; the underlying type is reflected over, not cast.
+ ///
+ object Suspend(
+ int serverHandle,
+ int itemHandle);
+
+ /// Reactivates data acquisition for a suspended item (ILMXProxyServer4).
+ /// Server handle identifying the registration.
+ /// Item handle to activate.
+ ///
+ /// The native MXAccess MxStatus value (boxed) produced by the call.
+ /// Callers convert it to a protobuf MxStatusProxy via the worker's
+ /// status converter; the underlying type is reflected over, not cast.
+ ///
+ object Activate(
+ int serverHandle,
+ int itemHandle);
+
+ /// Authenticates an MXAccess user and returns its user id (base ILMXProxyServer).
+ /// Server handle identifying the registration.
+ /// MXAccess user name to authenticate.
+ ///
+ /// Raw MXAccess credential. Implementations must keep this value out of
+ /// logs, metrics, command lines, and diagnostics.
+ ///
+ /// The MXAccess user id for the authenticated user.
+ int AuthenticateUser(
+ int serverHandle,
+ string verifyUser,
+ string verifyUserPassword);
+
+ /// Resolves an ArchestrA user GUID to an MXAccess user id (ILMXProxyServer2).
+ /// Server handle identifying the registration.
+ /// ArchestrA user GUID to resolve.
+ /// The MXAccess user id for the resolved user.
+ int ArchestrAUserToId(
+ int serverHandle,
+ string userIdGuid);
+
+ /// Adds a buffered item to a server and returns an item handle (ILMXProxyServer5).
+ /// Server handle identifying the registration.
+ /// Item definition string.
+ /// Item context string.
+ /// Item handle for the added buffered item.
+ int AddBufferedItem(
+ int serverHandle,
+ string itemDefinition,
+ string itemContext);
+
+ /// Sets the buffered-update interval for a server (ILMXProxyServer5).
+ /// Server handle identifying the registration.
+ /// Buffered update interval in milliseconds.
+ void SetBufferedUpdateInterval(
+ int serverHandle,
+ int updateIntervalMilliseconds);
+
/// Writes a value to an item.
/// Server handle identifying the registration.
/// Item handle to write to.
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessComServer.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessComServer.cs
index db159c1..117cfdf 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessComServer.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessComServer.cs
@@ -140,6 +140,89 @@ public sealed class MxAccessComServer : IMxAccessServer
AsProxyServer4().AdviseSupervisory(serverHandle, itemHandle);
}
+ ///
+ 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;
+ }
+
+ ///
+ 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;
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ public int ArchestrAUserToId(
+ int serverHandle,
+ string userIdGuid)
+ {
+ if (mxAccessComObject is IMxAccessServer typedFake)
+ {
+ return typedFake.ArchestrAUserToId(serverHandle, userIdGuid);
+ }
+
+ return AsProxyServer2().ArchestrAUserToId(serverHandle, userIdGuid);
+ }
+
+ ///
+ 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);
+ }
+
+ ///
+ public void SetBufferedUpdateInterval(
+ int serverHandle,
+ int updateIntervalMilliseconds)
+ {
+ if (mxAccessComObject is IMxAccessServer typedFake)
+ {
+ typedFake.SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds);
+ return;
+ }
+
+ AsProxyServer5().SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds);
+ }
+
///
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)}.");
+ }
}
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs
index 1dfd718..325a9cc 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs
@@ -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)
diff --git a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs
index c89c667..77940b1 100644
--- a/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs
+++ b/src/ZB.MOM.WW.MxGateway.Worker/MxAccess/MxAccessSession.cs
@@ -300,6 +300,94 @@ public sealed class MxAccessSession : IDisposable
MxAccessAdviceKind.Supervisory);
}
+ /// Suspends data acquisition for an advised item and returns the native MXAccess status.
+ /// Handle returned by the worker.
+ /// Handle returned by the worker.
+ /// The boxed native MXAccess status produced by the call.
+ public object Suspend(
+ int serverHandle,
+ int itemHandle)
+ {
+ ThrowIfDisposed();
+
+ return mxAccessServer.Suspend(serverHandle, itemHandle);
+ }
+
+ /// Reactivates data acquisition for a suspended item and returns the native MXAccess status.
+ /// Handle returned by the worker.
+ /// Handle returned by the worker.
+ /// The boxed native MXAccess status produced by the call.
+ public object Activate(
+ int serverHandle,
+ int itemHandle)
+ {
+ ThrowIfDisposed();
+
+ return mxAccessServer.Activate(serverHandle, itemHandle);
+ }
+
+ /// Authenticates an MXAccess user and returns its user id.
+ /// Handle returned by the worker.
+ /// MXAccess user name to authenticate.
+ /// Raw MXAccess credential; never logged.
+ /// The MXAccess user id for the authenticated user.
+ public int AuthenticateUser(
+ int serverHandle,
+ string verifyUser,
+ string verifyUserPassword)
+ {
+ ThrowIfDisposed();
+
+ return mxAccessServer.AuthenticateUser(serverHandle, verifyUser, verifyUserPassword);
+ }
+
+ /// Resolves an ArchestrA user GUID to an MXAccess user id.
+ /// Handle returned by the worker.
+ /// ArchestrA user GUID to resolve.
+ /// The MXAccess user id for the resolved user.
+ public int ArchestrAUserToId(
+ int serverHandle,
+ string userIdGuid)
+ {
+ ThrowIfDisposed();
+
+ return mxAccessServer.ArchestrAUserToId(serverHandle, userIdGuid);
+ }
+
+ /// Adds a buffered item to an MXAccess server and returns the item handle.
+ /// Handle returned by the worker.
+ /// Definition or address of the item to add.
+ /// Context string for the item.
+ 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;
+ }
+
+ /// Sets the buffered-update interval for an MXAccess server.
+ /// Handle returned by the worker.
+ /// Buffered update interval in milliseconds.
+ public void SetBufferedUpdateInterval(
+ int serverHandle,
+ int updateIntervalMilliseconds)
+ {
+ ThrowIfDisposed();
+
+ mxAccessServer.SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds);
+ }
+
/// Writes a value to an item.
/// Handle returned by the worker.
/// Handle returned by the worker.