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
@@ -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)