diff --git a/docs/implementation-plan-mxaccess-worker.md b/docs/implementation-plan-mxaccess-worker.md index 2f6cc73..3f0d9ae 100644 --- a/docs/implementation-plan-mxaccess-worker.md +++ b/docs/implementation-plan-mxaccess-worker.md @@ -218,6 +218,8 @@ Live tests: Labels: `area:worker`, `type:feature`, `priority:p0` +Status: implemented. + Deliverables: - `AddItem`, diff --git a/docs/mxaccess-worker-instance-design.md b/docs/mxaccess-worker-instance-design.md index 7f3ea2b..05bef4b 100644 --- a/docs/mxaccess-worker-instance-design.md +++ b/docs/mxaccess-worker-instance-design.md @@ -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 diff --git a/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs b/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs index 1a42f49..609f1f6 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs @@ -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 diff --git a/src/MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs b/src/MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs index f8ab696..ef2dffa 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs @@ -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 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); + } } diff --git a/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs b/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs index 5ad7525..b5f999d 100644 --- a/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs +++ b/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs @@ -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); } diff --git a/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs b/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs index e1a601b..7c687ed 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs @@ -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) diff --git a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs index 9c6ebb1..09d62e1 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs @@ -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 diff --git a/src/MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs b/src/MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs index 669acb3..fb398d3 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs @@ -6,12 +6,19 @@ namespace MxGateway.Worker.MxAccess; public sealed class MxAccessHandleRegistry { private readonly Dictionary serverHandles = new(); + private readonly Dictionary itemHandles = new(); public IReadOnlyList ServerHandles => serverHandles .Values .OrderBy(handle => handle.ServerHandle) .ToArray(); + public IReadOnlyList 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; + } } diff --git a/src/MxGateway.Worker/MxAccess/MxAccessSession.cs b/src/MxGateway.Worker/MxAccess/MxAccessSession.cs index 6874ef0..b8cd960 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessSession.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessSession.cs @@ -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) diff --git a/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs b/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs index 945633b..de522f3 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs @@ -81,6 +81,19 @@ public sealed class MxAccessStaSession : IDisposable cancellationToken); } + public Task> 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) diff --git a/src/MxGateway.Worker/MxAccess/RegisteredItemHandle.cs b/src/MxGateway.Worker/MxAccess/RegisteredItemHandle.cs new file mode 100644 index 0000000..15267d9 --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/RegisteredItemHandle.cs @@ -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; } +}