Implement worker AddItem commands

This commit is contained in:
Joseph Doherty
2026-04-26 18:26:44 -04:00
parent 653f17c669
commit 59c710d789
11 changed files with 803 additions and 2 deletions
@@ -218,6 +218,8 @@ Live tests:
Labels: `area:worker`, `type:feature`, `priority:p0`
Status: implemented.
Deliverables:
- `AddItem`,
+25
View File
@@ -432,6 +432,25 @@ HRESULT and converts the reply to `ProtocolStatusCode.MxaccessFailure`.
`MxAccessStaSession.GetRegisteredServerHandlesAsync` returns an STA-read
snapshot of tracked server handles for diagnostics and future cleanup logic.
`MxAccessCommandExecutor` also implements the item lifecycle commands:
- `AddItem` calls `LMXProxyServerClass.AddItem` with the requested server
handle and item definition. It preserves the returned item handle in both
`ReturnValue` and `AddItemReply.ItemHandle`.
- `AddItem2` calls `LMXProxyServerClass.AddItem2` with the requested server
handle, item definition, and context string. The context string is passed to
MXAccess exactly as received.
- `RemoveItem` calls `LMXProxyServerClass.RemoveItem` with the requested server
handle and item handle. The reply has no method-specific payload because the
public MXAccess method returns `void`.
The worker records item handles only after `AddItem` or `AddItem2` returns
normally, and removes item handles only after `RemoveItem` returns normally.
The registry does not prevalidate server or item handles, so invalid and
cross-server handle behavior remains owned by MXAccess. COM exceptions continue
through `StaCommandDispatcher`, which preserves the HRESULT and leaves
diagnostic registry state unchanged for failed cleanup calls.
## Handle Registry
The worker should track MXAccess state for diagnostics and cleanup, while still
@@ -454,6 +473,8 @@ Rules:
- Do not rewrite handles returned by MXAccess.
- Record server handles only after `Register` succeeds.
- Remove server handles only after `Unregister` succeeds.
- Record item handles only after `AddItem` or `AddItem2` succeeds.
- Remove item handles only after `RemoveItem` succeeds.
- Preserve invalid-handle behavior from MXAccess.
- Preserve cross-server handle behavior from MXAccess.
- Use registry state for cleanup and diagnostics, not semantic correction.
@@ -697,6 +718,10 @@ Live MXAccess tests:
Live tests should be opt-in and clearly marked because they depend on installed
MXAccess COM and provider state.
The worker test suite uses `MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1` for these
tests. `AddItem` uses `TestChildObject.TestInt` by default and accepts an
override through `MXGATEWAY_LIVE_MXACCESS_ITEM`; `AddItem2` uses the captured
parity fixture shape `AddItem2("TestInt", "TestChildObject")`.
## Initial Implementation Slice
@@ -78,6 +78,166 @@ public sealed class MxAccessCommandExecutorTests
Assert.Equal(44, registeredServerHandle.ServerHandle);
}
[Fact]
public async Task DispatchAsync_AddItem_CallsMxAccessOnStaAndTracksItemHandle()
{
FakeMxAccessComObject fakeComObject = new(
registerHandle: 46,
addItemHandle: 501);
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-add", "client-a"));
MxCommandReply reply = await session.DispatchAsync(CreateAddItemCommand(
"add-item",
46,
"Galaxy.Tag.Value"));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.True(reply.HasHresult);
Assert.Equal(0, reply.Hresult);
Assert.Equal(501, reply.AddItem.ItemHandle);
Assert.Equal(MxDataType.Integer, reply.ReturnValue.DataType);
Assert.Equal(501, reply.ReturnValue.Int32Value);
Assert.Equal(46, fakeComObject.AddItemServerHandle);
Assert.Equal("Galaxy.Tag.Value", fakeComObject.AddItemDefinition);
Assert.Equal(runtime.StaThreadId, fakeComObject.AddItemThreadId);
RegisteredItemHandle registeredItemHandle = Assert.Single(
await session.GetRegisteredItemHandlesAsync());
Assert.Equal(46, registeredItemHandle.ServerHandle);
Assert.Equal(501, registeredItemHandle.ItemHandle);
Assert.Equal("Galaxy.Tag.Value", registeredItemHandle.ItemDefinition);
Assert.Equal(string.Empty, registeredItemHandle.ItemContext);
Assert.False(registeredItemHandle.HasItemContext);
}
[Fact]
public async Task DispatchAsync_AddItem2_PassesContextExactlyAndTracksItemHandle()
{
FakeMxAccessComObject fakeComObject = new(
registerHandle: 47,
addItem2Handle: 502);
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-add2", "client-a"));
MxCommandReply reply = await session.DispatchAsync(CreateAddItem2Command(
"add-item2",
47,
"TestInt",
"TestChildObject"));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.Equal(502, reply.AddItem2.ItemHandle);
Assert.Equal(MxDataType.Integer, reply.ReturnValue.DataType);
Assert.Equal(502, reply.ReturnValue.Int32Value);
Assert.Equal(47, fakeComObject.AddItem2ServerHandle);
Assert.Equal("TestInt", fakeComObject.AddItem2Definition);
Assert.Equal("TestChildObject", fakeComObject.AddItem2Context);
Assert.Equal(runtime.StaThreadId, fakeComObject.AddItem2ThreadId);
RegisteredItemHandle registeredItemHandle = Assert.Single(
await session.GetRegisteredItemHandlesAsync());
Assert.Equal(47, registeredItemHandle.ServerHandle);
Assert.Equal(502, registeredItemHandle.ItemHandle);
Assert.Equal("TestInt", registeredItemHandle.ItemDefinition);
Assert.Equal("TestChildObject", registeredItemHandle.ItemContext);
Assert.True(registeredItemHandle.HasItemContext);
}
[Fact]
public async Task DispatchAsync_RemoveItem_CallsMxAccessOnStaAndRemovesTrackedItemHandle()
{
FakeMxAccessComObject fakeComObject = new(
registerHandle: 48,
addItemHandle: 503);
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-remove", "client-a"));
await session.DispatchAsync(CreateAddItemCommand("add-before-remove", 48, "Galaxy.Tag.Value"));
MxCommandReply reply = await session.DispatchAsync(CreateRemoveItemCommand(
"remove-item",
48,
503));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.True(reply.HasHresult);
Assert.Equal(0, reply.Hresult);
Assert.Equal(48, fakeComObject.RemoveItemServerHandle);
Assert.Equal(503, fakeComObject.RemovedItemHandle);
Assert.Equal(runtime.StaThreadId, fakeComObject.RemoveItemThreadId);
Assert.Empty(await session.GetRegisteredItemHandlesAsync());
}
[Fact]
public async Task DispatchAsync_RemoveItemWithCrossServerHandle_PreservesHResultAndKeepsTrackedItemHandle()
{
const int hresult = unchecked((int)0x80070057);
FakeMxAccessComObject fakeComObject = new(
registerHandle: 49,
addItemHandle: 504,
removeItemException: new COMException("Invalid item handle.", hresult));
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-remove-failure", "client-a"));
await session.DispatchAsync(CreateAddItemCommand("add-before-remove-failure", 49, "Galaxy.Tag.Value"));
MxCommandReply reply = await session.DispatchAsync(CreateRemoveItemCommand(
"remove-item-failure",
999,
504));
Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code);
Assert.True(reply.HasHresult);
Assert.Equal(hresult, reply.Hresult);
Assert.Contains("0x80070057", reply.DiagnosticMessage);
Assert.Equal(999, fakeComObject.RemoveItemServerHandle);
Assert.Equal(504, fakeComObject.RemovedItemHandle);
RegisteredItemHandle registeredItemHandle = Assert.Single(
await session.GetRegisteredItemHandlesAsync());
Assert.Equal(49, registeredItemHandle.ServerHandle);
Assert.Equal(504, registeredItemHandle.ItemHandle);
}
[Fact]
public async Task DispatchAsync_AddItem2WhenMxAccessThrows_PreservesHResultAndDoesNotTrackItemHandle()
{
const int hresult = unchecked((int)0x80070057);
FakeMxAccessComObject fakeComObject = new(
registerHandle: 50,
addItem2Exception: new COMException("Invalid server handle.", hresult));
FakeMxAccessComObjectFactory factory = new(fakeComObject);
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
MxCommandReply reply = await session.DispatchAsync(CreateAddItem2Command(
"add-item2-failure",
9001,
"TestInt",
"TestChildObject"));
Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code);
Assert.True(reply.HasHresult);
Assert.Equal(hresult, reply.Hresult);
Assert.Contains("0x80070057", reply.DiagnosticMessage);
Assert.Equal(9001, fakeComObject.AddItem2ServerHandle);
Assert.Equal("TestInt", fakeComObject.AddItem2Definition);
Assert.Equal("TestChildObject", fakeComObject.AddItem2Context);
Assert.Empty(await session.GetRegisteredItemHandlesAsync());
}
[Fact]
public async Task DispatchAsync_RegisterWithoutPayload_ReturnsInvalidRequest()
{
@@ -98,6 +258,26 @@ public sealed class MxAccessCommandExecutorTests
Assert.Null(factory.FakeComObject.RegisteredClientName);
}
[Fact]
public async Task DispatchAsync_AddItemWithoutPayload_ReturnsInvalidRequest()
{
FakeMxAccessComObjectFactory factory = new(new FakeMxAccessComObject(registerHandle: 51));
using StaRuntime runtime = CreateRuntime();
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
await session.StartAsync(workerProcessId: 1234);
MxCommandReply reply = await session.DispatchAsync(new StaCommand(
"session-1",
"missing-add-payload",
new MxCommand
{
Kind = MxCommandKind.AddItem,
}));
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
Assert.Null(factory.FakeComObject.AddItemDefinition);
}
private static StaCommand CreateRegisterCommand(
string correlationId,
string clientName)
@@ -132,6 +312,65 @@ public sealed class MxAccessCommandExecutorTests
});
}
private static StaCommand CreateAddItemCommand(
string correlationId,
int serverHandle,
string itemDefinition)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.AddItem,
AddItem = new AddItemCommand
{
ServerHandle = serverHandle,
ItemDefinition = itemDefinition,
},
});
}
private static StaCommand CreateAddItem2Command(
string correlationId,
int serverHandle,
string itemDefinition,
string itemContext)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.AddItem2,
AddItem2 = new AddItem2Command
{
ServerHandle = serverHandle,
ItemDefinition = itemDefinition,
ItemContext = itemContext,
},
});
}
private static StaCommand CreateRemoveItemCommand(
string correlationId,
int serverHandle,
int itemHandle)
{
return new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.RemoveItem,
RemoveItem = new RemoveItemCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
});
}
private static StaRuntime CreateRuntime()
{
return new StaRuntime(
@@ -143,14 +382,29 @@ public sealed class MxAccessCommandExecutorTests
private sealed class FakeMxAccessComObject
{
private readonly int registerHandle;
private readonly int addItemHandle;
private readonly int addItem2Handle;
private readonly Exception? unregisterException;
private readonly Exception? addItemException;
private readonly Exception? addItem2Exception;
private readonly Exception? removeItemException;
public FakeMxAccessComObject(
int registerHandle,
Exception? unregisterException = null)
int addItemHandle = 0,
int addItem2Handle = 0,
Exception? unregisterException = null,
Exception? addItemException = null,
Exception? addItem2Exception = null,
Exception? removeItemException = null)
{
this.registerHandle = registerHandle;
this.addItemHandle = addItemHandle;
this.addItem2Handle = addItem2Handle;
this.unregisterException = unregisterException;
this.addItemException = addItemException;
this.addItem2Exception = addItem2Exception;
this.removeItemException = removeItemException;
}
public string? RegisteredClientName { get; private set; }
@@ -161,6 +415,26 @@ public sealed class MxAccessCommandExecutorTests
public int? UnregisterThreadId { get; private set; }
public int? AddItemServerHandle { get; private set; }
public string? AddItemDefinition { get; private set; }
public int? AddItemThreadId { get; private set; }
public int? AddItem2ServerHandle { get; private set; }
public string? AddItem2Definition { get; private set; }
public string? AddItem2Context { get; private set; }
public int? AddItem2ThreadId { get; private set; }
public int? RemoveItemServerHandle { get; private set; }
public int? RemovedItemHandle { get; private set; }
public int? RemoveItemThreadId { get; private set; }
public int Register(string clientName)
{
RegisteredClientName = clientName;
@@ -179,6 +453,54 @@ public sealed class MxAccessCommandExecutorTests
throw unregisterException;
}
}
public int AddItem(
int serverHandle,
string itemDefinition)
{
AddItemServerHandle = serverHandle;
AddItemDefinition = itemDefinition;
AddItemThreadId = Environment.CurrentManagedThreadId;
if (addItemException is not null)
{
throw addItemException;
}
return addItemHandle;
}
public int AddItem2(
int serverHandle,
string itemDefinition,
string itemContext)
{
AddItem2ServerHandle = serverHandle;
AddItem2Definition = itemDefinition;
AddItem2Context = itemContext;
AddItem2ThreadId = Environment.CurrentManagedThreadId;
if (addItem2Exception is not null)
{
throw addItem2Exception;
}
return addItem2Handle;
}
public void RemoveItem(
int serverHandle,
int itemHandle)
{
RemoveItemServerHandle = serverHandle;
RemovedItemHandle = itemHandle;
RemoveItemThreadId = Environment.CurrentManagedThreadId;
if (removeItemException is not null)
{
throw removeItemException;
}
}
}
private sealed class FakeMxAccessComObjectFactory : IMxAccessComObjectFactory
@@ -8,6 +8,11 @@ namespace MxGateway.Worker.Tests.MxAccess;
public sealed class MxAccessLiveComCreationTests
{
private const string LiveClientName = "MxGateway.Worker.Tests";
private const string DefaultLiveAddItemReference = "TestChildObject.TestInt";
private const string DefaultLiveAddItem2Definition = "TestInt";
private const string DefaultLiveAddItem2Context = "TestChildObject";
[Fact]
public async Task StartAsync_WhenOptedIn_CreatesInstalledMxAccessComObjectOnSta()
{
@@ -43,7 +48,7 @@ public sealed class MxAccessLiveComCreationTests
Kind = MxCommandKind.Register,
Register = new RegisterCommand
{
ClientName = "MxGateway.Worker.Tests",
ClientName = LiveClientName,
},
}));
@@ -65,6 +70,151 @@ public sealed class MxAccessLiveComCreationTests
Assert.Equal(ProtocolStatusCode.Ok, unregisterReply.ProtocolStatus.Code);
}
[Fact]
public async Task AddItemAndRemoveItem_WhenOptedIn_RoundTripsInstalledMxAccessItemHandle()
{
if (!RunLiveMxAccessTests())
{
return;
}
using MxAccessStaSession session = new();
await session.StartAsync(workerProcessId: 1234);
MxCommandReply registerReply = await RegisterLiveSessionAsync(session, "live-add-register");
int serverHandle = registerReply.Register.ServerHandle;
int itemHandle = 0;
try
{
MxCommandReply addItemReply = await session.DispatchAsync(new StaCommand(
"session-1",
"live-add-item",
new MxCommand
{
Kind = MxCommandKind.AddItem,
AddItem = new AddItemCommand
{
ServerHandle = serverHandle,
ItemDefinition = GetLiveAddItemReference(),
},
}));
Assert.Equal(ProtocolStatusCode.Ok, addItemReply.ProtocolStatus.Code);
Assert.True(addItemReply.AddItem.ItemHandle > 0);
itemHandle = addItemReply.AddItem.ItemHandle;
MxCommandReply removeItemReply = await session.DispatchAsync(new StaCommand(
"session-1",
"live-remove-item",
new MxCommand
{
Kind = MxCommandKind.RemoveItem,
RemoveItem = new RemoveItemCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
}));
Assert.Equal(ProtocolStatusCode.Ok, removeItemReply.ProtocolStatus.Code);
itemHandle = 0;
}
finally
{
if (itemHandle > 0)
{
await session.DispatchAsync(new StaCommand(
"session-1",
"live-remove-item-cleanup",
new MxCommand
{
Kind = MxCommandKind.RemoveItem,
RemoveItem = new RemoveItemCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
}));
}
await UnregisterLiveSessionAsync(session, serverHandle, "live-add-unregister");
}
}
[Fact]
public async Task AddItem2AndRemoveItem_WhenOptedIn_PreservesContextForInstalledMxAccess()
{
if (!RunLiveMxAccessTests())
{
return;
}
using MxAccessStaSession session = new();
await session.StartAsync(workerProcessId: 1234);
MxCommandReply registerReply = await RegisterLiveSessionAsync(session, "live-add2-register");
int serverHandle = registerReply.Register.ServerHandle;
int itemHandle = 0;
try
{
MxCommandReply addItem2Reply = await session.DispatchAsync(new StaCommand(
"session-1",
"live-add-item2",
new MxCommand
{
Kind = MxCommandKind.AddItem2,
AddItem2 = new AddItem2Command
{
ServerHandle = serverHandle,
ItemDefinition = DefaultLiveAddItem2Definition,
ItemContext = DefaultLiveAddItem2Context,
},
}));
Assert.Equal(ProtocolStatusCode.Ok, addItem2Reply.ProtocolStatus.Code);
Assert.True(addItem2Reply.AddItem2.ItemHandle > 0);
itemHandle = addItem2Reply.AddItem2.ItemHandle;
MxCommandReply removeItemReply = await session.DispatchAsync(new StaCommand(
"session-1",
"live-remove-item2",
new MxCommand
{
Kind = MxCommandKind.RemoveItem,
RemoveItem = new RemoveItemCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
}));
Assert.Equal(ProtocolStatusCode.Ok, removeItemReply.ProtocolStatus.Code);
itemHandle = 0;
}
finally
{
if (itemHandle > 0)
{
await session.DispatchAsync(new StaCommand(
"session-1",
"live-remove-item2-cleanup",
new MxCommand
{
Kind = MxCommandKind.RemoveItem,
RemoveItem = new RemoveItemCommand
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
},
}));
}
await UnregisterLiveSessionAsync(session, serverHandle, "live-add2-unregister");
}
}
private static bool RunLiveMxAccessTests()
{
return string.Equals(
@@ -72,4 +222,55 @@ public sealed class MxAccessLiveComCreationTests
"1",
StringComparison.Ordinal);
}
private static string GetLiveAddItemReference()
{
string itemReference = Environment.GetEnvironmentVariable("MXGATEWAY_LIVE_MXACCESS_ITEM");
return string.IsNullOrWhiteSpace(itemReference)
? DefaultLiveAddItemReference
: itemReference;
}
private static async Task<MxCommandReply> RegisterLiveSessionAsync(
MxAccessStaSession session,
string correlationId)
{
MxCommandReply reply = await session.DispatchAsync(new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.Register,
Register = new RegisterCommand
{
ClientName = LiveClientName,
},
}));
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
Assert.True(reply.Register.ServerHandle > 0);
return reply;
}
private static async Task UnregisterLiveSessionAsync(
MxAccessStaSession session,
int serverHandle,
string correlationId)
{
MxCommandReply unregisterReply = await session.DispatchAsync(new StaCommand(
"session-1",
correlationId,
new MxCommand
{
Kind = MxCommandKind.Unregister,
Unregister = new UnregisterCommand
{
ServerHandle = serverHandle,
},
}));
Assert.Equal(ProtocolStatusCode.Ok, unregisterReply.ProtocolStatus.Code);
}
}
@@ -5,4 +5,17 @@ public interface IMxAccessServer
int Register(string clientName);
void Unregister(int serverHandle);
int AddItem(
int serverHandle,
string itemDefinition);
int AddItem2(
int serverHandle,
string itemDefinition,
string itemContext);
void RemoveItem(
int serverHandle,
int itemHandle);
}
@@ -35,6 +35,44 @@ public sealed class MxAccessComServer : IMxAccessServer
Invoke(nameof(Unregister), serverHandle);
}
public int AddItem(
int serverHandle,
string itemDefinition)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
{
return mxAccessServer.AddItem(serverHandle, itemDefinition);
}
return (int)Invoke(nameof(AddItem), serverHandle, itemDefinition);
}
public int AddItem2(
int serverHandle,
string itemDefinition,
string itemContext)
{
if (mxAccessComObject is ILMXProxyServer3 mxAccessServer)
{
return mxAccessServer.AddItem2(serverHandle, itemDefinition, itemContext);
}
return (int)Invoke(nameof(AddItem2), serverHandle, itemDefinition, itemContext);
}
public void RemoveItem(
int serverHandle,
int itemHandle)
{
if (mxAccessComObject is ILMXProxyServer mxAccessServer)
{
mxAccessServer.RemoveItem(serverHandle, itemHandle);
return;
}
Invoke(nameof(RemoveItem), serverHandle, itemHandle);
}
private object Invoke(
string methodName,
params object[] arguments)
@@ -34,6 +34,9 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
{
MxCommandKind.Register => ExecuteRegister(command),
MxCommandKind.Unregister => ExecuteUnregister(command),
MxCommandKind.AddItem => ExecuteAddItem(command),
MxCommandKind.AddItem2 => ExecuteAddItem2(command),
MxCommandKind.RemoveItem => ExecuteRemoveItem(command),
_ => CreateInvalidRequestReply(command, $"Unsupported MXAccess command kind {command.Kind}."),
};
}
@@ -67,6 +70,66 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
return CreateOkReply(command);
}
private MxCommandReply ExecuteAddItem(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AddItem)
{
return CreateInvalidRequestReply(command, "AddItem command payload is required.");
}
AddItemCommand addItemCommand = command.Command.AddItem;
int itemHandle = session.AddItem(
addItemCommand.ServerHandle,
addItemCommand.ItemDefinition);
MxCommandReply reply = CreateOkReply(command);
reply.ReturnValue = variantConverter.Convert(itemHandle);
reply.AddItem = new AddItemReply
{
ItemHandle = itemHandle,
};
return reply;
}
private MxCommandReply ExecuteAddItem2(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AddItem2)
{
return CreateInvalidRequestReply(command, "AddItem2 command payload is required.");
}
AddItem2Command addItem2Command = command.Command.AddItem2;
int itemHandle = session.AddItem2(
addItem2Command.ServerHandle,
addItem2Command.ItemDefinition,
addItem2Command.ItemContext);
MxCommandReply reply = CreateOkReply(command);
reply.ReturnValue = variantConverter.Convert(itemHandle);
reply.AddItem2 = new AddItem2Reply
{
ItemHandle = itemHandle,
};
return reply;
}
private MxCommandReply ExecuteRemoveItem(StaCommand command)
{
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.RemoveItem)
{
return CreateInvalidRequestReply(command, "RemoveItem command payload is required.");
}
RemoveItemCommand removeItemCommand = command.Command.RemoveItem;
session.RemoveItem(
removeItemCommand.ServerHandle,
removeItemCommand.ItemHandle);
return CreateOkReply(command);
}
private static MxCommandReply CreateOkReply(StaCommand command)
{
return new MxCommandReply
@@ -6,12 +6,19 @@ namespace MxGateway.Worker.MxAccess;
public sealed class MxAccessHandleRegistry
{
private readonly Dictionary<int, RegisteredServerHandle> serverHandles = new();
private readonly Dictionary<long, RegisteredItemHandle> itemHandles = new();
public IReadOnlyList<RegisteredServerHandle> ServerHandles => serverHandles
.Values
.OrderBy(handle => handle.ServerHandle)
.ToArray();
public IReadOnlyList<RegisteredItemHandle> ItemHandles => itemHandles
.Values
.OrderBy(handle => handle.ServerHandle)
.ThenBy(handle => handle.ItemHandle)
.ToArray();
public void RegisterServerHandle(
int serverHandle,
string clientName)
@@ -22,10 +29,54 @@ public sealed class MxAccessHandleRegistry
public void UnregisterServerHandle(int serverHandle)
{
serverHandles.Remove(serverHandle);
foreach (long key in itemHandles
.Where(pair => pair.Value.ServerHandle == serverHandle)
.Select(pair => pair.Key)
.ToArray())
{
itemHandles.Remove(key);
}
}
public bool ContainsServerHandle(int serverHandle)
{
return serverHandles.ContainsKey(serverHandle);
}
public void RegisterItemHandle(
int serverHandle,
int itemHandle,
string itemDefinition,
string itemContext,
bool hasItemContext)
{
itemHandles[CreateItemKey(serverHandle, itemHandle)] = new RegisteredItemHandle(
serverHandle,
itemHandle,
itemDefinition,
itemContext,
hasItemContext);
}
public void RemoveItemHandle(
int serverHandle,
int itemHandle)
{
itemHandles.Remove(CreateItemKey(serverHandle, itemHandle));
}
public bool ContainsItemHandle(
int serverHandle,
int itemHandle)
{
return itemHandles.ContainsKey(CreateItemKey(serverHandle, itemHandle));
}
private static long CreateItemKey(
int serverHandle,
int itemHandle)
{
return ((long)serverHandle << 32) | (uint)itemHandle;
}
}
@@ -106,6 +106,51 @@ public sealed class MxAccessSession : IDisposable
handleRegistry.UnregisterServerHandle(serverHandle);
}
public int AddItem(
int serverHandle,
string itemDefinition)
{
ThrowIfDisposed();
int itemHandle = mxAccessServer.AddItem(serverHandle, itemDefinition);
handleRegistry.RegisterItemHandle(
serverHandle,
itemHandle,
itemDefinition,
string.Empty,
hasItemContext: false);
return itemHandle;
}
public int AddItem2(
int serverHandle,
string itemDefinition,
string itemContext)
{
ThrowIfDisposed();
int itemHandle = mxAccessServer.AddItem2(serverHandle, itemDefinition, itemContext);
handleRegistry.RegisterItemHandle(
serverHandle,
itemHandle,
itemDefinition,
itemContext,
hasItemContext: true);
return itemHandle;
}
public void RemoveItem(
int serverHandle,
int itemHandle)
{
ThrowIfDisposed();
mxAccessServer.RemoveItem(serverHandle, itemHandle);
handleRegistry.RemoveItemHandle(serverHandle, itemHandle);
}
public void Dispose()
{
if (disposed)
@@ -81,6 +81,19 @@ public sealed class MxAccessStaSession : IDisposable
cancellationToken);
}
public Task<IReadOnlyList<RegisteredItemHandle>> GetRegisteredItemHandlesAsync(
CancellationToken cancellationToken = default)
{
if (session is null)
{
throw new InvalidOperationException("MXAccess COM session has not been started.");
}
return staRuntime.InvokeAsync(
() => session.HandleRegistry.ItemHandles,
cancellationToken);
}
public void Dispose()
{
if (disposed)
@@ -0,0 +1,28 @@
namespace MxGateway.Worker.MxAccess;
public sealed class RegisteredItemHandle
{
public RegisteredItemHandle(
int serverHandle,
int itemHandle,
string itemDefinition,
string itemContext,
bool hasItemContext)
{
ServerHandle = serverHandle;
ItemHandle = itemHandle;
ItemDefinition = itemDefinition;
ItemContext = itemContext;
HasItemContext = hasItemContext;
}
public int ServerHandle { get; }
public int ItemHandle { get; }
public string ItemDefinition { get; }
public string ItemContext { get; }
public bool HasItemContext { get; }
}