diff --git a/docs/mxaccess-worker-instance-design.md b/docs/mxaccess-worker-instance-design.md index 05bef4b..2ec5ec5 100644 --- a/docs/mxaccess-worker-instance-design.md +++ b/docs/mxaccess-worker-instance-design.md @@ -451,6 +451,26 @@ 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. +`MxAccessCommandExecutor` implements advice lifecycle commands on the same STA +path: + +- `Advise` calls `LMXProxyServerClass.Advise` with the requested server handle + and item handle. +- `AdviseSupervisory` calls `LMXProxyServerClass.AdviseSupervisory` with the + requested server handle and item handle. This remains a distinct command from + plain `Advise` even though observed scalar captures share the same lower-level + subscription body. +- `UnAdvise` calls `LMXProxyServerClass.UnAdvise` with the requested server + handle and item handle. + +The worker records plain and supervisory advice separately only after the COM +call returns normally. Successful `UnAdvise` removes all tracked advice for the +server and item pair because the public MXAccess cleanup method has no plain +versus supervisory selector. Successful `RemoveItem` and `Unregister` also clear +related advice state from the worker registry. Failed advice and cleanup calls +leave registry state unchanged so diagnostics continue to reflect the last +successful MXAccess-owned state transition. + ## Handle Registry The worker should track MXAccess state for diagnostics and cleanup, while still @@ -475,6 +495,9 @@ Rules: - Remove server handles only after `Unregister` succeeds. - Record item handles only after `AddItem` or `AddItem2` succeeds. - Remove item handles only after `RemoveItem` succeeds. +- Record advice state only after `Advise` or `AdviseSupervisory` succeeds. +- Remove advice state only after `UnAdvise`, `RemoveItem`, or `Unregister` + succeeds. - Preserve invalid-handle behavior from MXAccess. - Preserve cross-server handle behavior from MXAccess. - Use registry state for cleanup and diagnostics, not semantic correction. diff --git a/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs b/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs index 609f1f6..af542b8 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/MxAccessCommandExecutorTests.cs @@ -177,6 +177,30 @@ public sealed class MxAccessCommandExecutorTests Assert.Empty(await session.GetRegisteredItemHandlesAsync()); } + [Fact] + public async Task DispatchAsync_RemoveItemWithAdvisedHandle_RemovesTrackedAdviceAfterMxAccessSucceeds() + { + FakeMxAccessComObject fakeComObject = new( + registerHandle: 148, + addItemHandle: 603); + 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-advised-remove", "client-a")); + await session.DispatchAsync(CreateAddItemCommand("add-before-advised-remove", 148, "Galaxy.Tag.Value")); + await session.DispatchAsync(CreateAdviseCommand("advise-before-remove", 148, 603)); + + MxCommandReply reply = await session.DispatchAsync(CreateRemoveItemCommand( + "remove-advised-item", + 148, + 603)); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.Empty(await session.GetRegisteredItemHandlesAsync()); + Assert.Empty(await session.GetRegisteredAdviceHandlesAsync()); + } + [Fact] public async Task DispatchAsync_RemoveItemWithCrossServerHandle_PreservesHResultAndKeepsTrackedItemHandle() { @@ -238,6 +262,158 @@ public sealed class MxAccessCommandExecutorTests Assert.Empty(await session.GetRegisteredItemHandlesAsync()); } + [Fact] + public async Task DispatchAsync_Advise_CallsMxAccessOnStaAndTracksPlainAdvice() + { + FakeMxAccessComObject fakeComObject = new( + registerHandle: 52, + addItemHandle: 505); + 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-advise", "client-a")); + await session.DispatchAsync(CreateAddItemCommand("add-before-advise", 52, "Galaxy.Tag.Value")); + + MxCommandReply reply = await session.DispatchAsync(CreateAdviseCommand( + "advise", + 52, + 505)); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.True(reply.HasHresult); + Assert.Equal(0, reply.Hresult); + Assert.Equal(52, fakeComObject.AdviseServerHandle); + Assert.Equal(505, fakeComObject.AdvisedItemHandle); + Assert.Equal(runtime.StaThreadId, fakeComObject.AdviseThreadId); + + RegisteredAdviceHandle adviceHandle = Assert.Single( + await session.GetRegisteredAdviceHandlesAsync()); + Assert.Equal(52, adviceHandle.ServerHandle); + Assert.Equal(505, adviceHandle.ItemHandle); + Assert.Equal(MxAccessAdviceKind.Plain, adviceHandle.AdviceKind); + } + + [Fact] + public async Task DispatchAsync_AdviseSupervisory_CallsDistinctMxAccessMethodAndTracksSupervisoryAdvice() + { + FakeMxAccessComObject fakeComObject = new( + registerHandle: 53, + addItemHandle: 506); + 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-supervisory", "client-a")); + await session.DispatchAsync(CreateAddItemCommand("add-before-supervisory", 53, "Galaxy.Tag.Value")); + + MxCommandReply reply = await session.DispatchAsync(CreateAdviseSupervisoryCommand( + "advise-supervisory", + 53, + 506)); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.Equal(53, fakeComObject.AdviseSupervisoryServerHandle); + Assert.Equal(506, fakeComObject.AdviseSupervisoryItemHandle); + Assert.Equal(runtime.StaThreadId, fakeComObject.AdviseSupervisoryThreadId); + Assert.Null(fakeComObject.AdviseServerHandle); + + RegisteredAdviceHandle adviceHandle = Assert.Single( + await session.GetRegisteredAdviceHandlesAsync()); + Assert.Equal(53, adviceHandle.ServerHandle); + Assert.Equal(506, adviceHandle.ItemHandle); + Assert.Equal(MxAccessAdviceKind.Supervisory, adviceHandle.AdviceKind); + } + + [Fact] + public async Task DispatchAsync_UnAdvise_CallsMxAccessOnStaAndRemovesTrackedAdvice() + { + FakeMxAccessComObject fakeComObject = new( + registerHandle: 54, + addItemHandle: 507); + 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-unadvise", "client-a")); + await session.DispatchAsync(CreateAddItemCommand("add-before-unadvise", 54, "Galaxy.Tag.Value")); + await session.DispatchAsync(CreateAdviseCommand("advise-before-unadvise", 54, 507)); + await session.DispatchAsync(CreateAdviseSupervisoryCommand("supervisory-before-unadvise", 54, 507)); + + MxCommandReply reply = await session.DispatchAsync(CreateUnAdviseCommand( + "unadvise", + 54, + 507)); + + Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code); + Assert.Equal(54, fakeComObject.UnAdviseServerHandle); + Assert.Equal(507, fakeComObject.UnAdvisedItemHandle); + Assert.Equal(runtime.StaThreadId, fakeComObject.UnAdviseThreadId); + Assert.Empty(await session.GetRegisteredAdviceHandlesAsync()); + } + + [Fact] + public async Task DispatchAsync_AdviseWhenMxAccessThrows_PreservesHResultAndDoesNotTrackAdvice() + { + const int hresult = unchecked((int)0x80070057); + FakeMxAccessComObject fakeComObject = new( + registerHandle: 55, + addItemHandle: 508, + adviseException: 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-advise-failure", "client-a")); + await session.DispatchAsync(CreateAddItemCommand("add-before-advise-failure", 55, "Galaxy.Tag.Value")); + + MxCommandReply reply = await session.DispatchAsync(CreateAdviseCommand( + "advise-failure", + 55, + 999)); + + Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); + Assert.True(reply.HasHresult); + Assert.Equal(hresult, reply.Hresult); + Assert.Contains("0x80070057", reply.DiagnosticMessage); + Assert.Equal(55, fakeComObject.AdviseServerHandle); + Assert.Equal(999, fakeComObject.AdvisedItemHandle); + Assert.Empty(await session.GetRegisteredAdviceHandlesAsync()); + } + + [Fact] + public async Task DispatchAsync_UnAdviseWhenMxAccessThrows_PreservesHResultAndKeepsTrackedAdvice() + { + const int hresult = unchecked((int)0x80070057); + FakeMxAccessComObject fakeComObject = new( + registerHandle: 56, + addItemHandle: 509, + unAdviseException: 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-unadvise-failure", "client-a")); + await session.DispatchAsync(CreateAddItemCommand("add-before-unadvise-failure", 56, "Galaxy.Tag.Value")); + await session.DispatchAsync(CreateAdviseCommand("advise-before-unadvise-failure", 56, 509)); + + MxCommandReply reply = await session.DispatchAsync(CreateUnAdviseCommand( + "unadvise-failure", + 56, + 509)); + + Assert.Equal(ProtocolStatusCode.MxaccessFailure, reply.ProtocolStatus.Code); + Assert.True(reply.HasHresult); + Assert.Equal(hresult, reply.Hresult); + Assert.Contains("0x80070057", reply.DiagnosticMessage); + Assert.Equal(56, fakeComObject.UnAdviseServerHandle); + Assert.Equal(509, fakeComObject.UnAdvisedItemHandle); + + RegisteredAdviceHandle adviceHandle = Assert.Single( + await session.GetRegisteredAdviceHandlesAsync()); + Assert.Equal(MxAccessAdviceKind.Plain, adviceHandle.AdviceKind); + } + [Fact] public async Task DispatchAsync_RegisterWithoutPayload_ReturnsInvalidRequest() { @@ -278,6 +454,26 @@ public sealed class MxAccessCommandExecutorTests Assert.Null(factory.FakeComObject.AddItemDefinition); } + [Fact] + public async Task DispatchAsync_AdviseWithoutPayload_ReturnsInvalidRequest() + { + FakeMxAccessComObjectFactory factory = new(new FakeMxAccessComObject(registerHandle: 57)); + 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-advise-payload", + new MxCommand + { + Kind = MxCommandKind.Advise, + })); + + Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code); + Assert.Null(factory.FakeComObject.AdviseServerHandle); + } + private static StaCommand CreateRegisterCommand( string correlationId, string clientName) @@ -371,6 +567,63 @@ public sealed class MxAccessCommandExecutorTests }); } + private static StaCommand CreateAdviseCommand( + string correlationId, + int serverHandle, + int itemHandle) + { + return new StaCommand( + "session-1", + correlationId, + new MxCommand + { + Kind = MxCommandKind.Advise, + Advise = new AdviseCommand + { + ServerHandle = serverHandle, + ItemHandle = itemHandle, + }, + }); + } + + private static StaCommand CreateUnAdviseCommand( + string correlationId, + int serverHandle, + int itemHandle) + { + return new StaCommand( + "session-1", + correlationId, + new MxCommand + { + Kind = MxCommandKind.UnAdvise, + UnAdvise = new UnAdviseCommand + { + ServerHandle = serverHandle, + ItemHandle = itemHandle, + }, + }); + } + + private static StaCommand CreateAdviseSupervisoryCommand( + string correlationId, + int serverHandle, + int itemHandle) + { + return new StaCommand( + "session-1", + correlationId, + new MxCommand + { + Kind = MxCommandKind.AdviseSupervisory, + AdviseSupervisory = new AdviseSupervisoryCommand + { + ServerHandle = serverHandle, + ItemHandle = itemHandle, + }, + }); + } + private static StaRuntime CreateRuntime() { return new StaRuntime( @@ -388,6 +641,9 @@ public sealed class MxAccessCommandExecutorTests private readonly Exception? addItemException; private readonly Exception? addItem2Exception; private readonly Exception? removeItemException; + private readonly Exception? adviseException; + private readonly Exception? unAdviseException; + private readonly Exception? adviseSupervisoryException; public FakeMxAccessComObject( int registerHandle, @@ -396,7 +652,10 @@ public sealed class MxAccessCommandExecutorTests Exception? unregisterException = null, Exception? addItemException = null, Exception? addItem2Exception = null, - Exception? removeItemException = null) + Exception? removeItemException = null, + Exception? adviseException = null, + Exception? unAdviseException = null, + Exception? adviseSupervisoryException = null) { this.registerHandle = registerHandle; this.addItemHandle = addItemHandle; @@ -405,6 +664,9 @@ public sealed class MxAccessCommandExecutorTests this.addItemException = addItemException; this.addItem2Exception = addItem2Exception; this.removeItemException = removeItemException; + this.adviseException = adviseException; + this.unAdviseException = unAdviseException; + this.adviseSupervisoryException = adviseSupervisoryException; } public string? RegisteredClientName { get; private set; } @@ -435,6 +697,24 @@ public sealed class MxAccessCommandExecutorTests public int? RemoveItemThreadId { get; private set; } + public int? AdviseServerHandle { get; private set; } + + public int? AdvisedItemHandle { get; private set; } + + public int? AdviseThreadId { get; private set; } + + public int? UnAdviseServerHandle { get; private set; } + + public int? UnAdvisedItemHandle { get; private set; } + + public int? UnAdviseThreadId { get; private set; } + + public int? AdviseSupervisoryServerHandle { get; private set; } + + public int? AdviseSupervisoryItemHandle { get; private set; } + + public int? AdviseSupervisoryThreadId { get; private set; } + public int Register(string clientName) { RegisteredClientName = clientName; @@ -501,6 +781,48 @@ public sealed class MxAccessCommandExecutorTests throw removeItemException; } } + + public void Advise( + int serverHandle, + int itemHandle) + { + AdviseServerHandle = serverHandle; + AdvisedItemHandle = itemHandle; + AdviseThreadId = Environment.CurrentManagedThreadId; + + if (adviseException is not null) + { + throw adviseException; + } + } + + public void UnAdvise( + int serverHandle, + int itemHandle) + { + UnAdviseServerHandle = serverHandle; + UnAdvisedItemHandle = itemHandle; + UnAdviseThreadId = Environment.CurrentManagedThreadId; + + if (unAdviseException is not null) + { + throw unAdviseException; + } + } + + public void AdviseSupervisory( + int serverHandle, + int itemHandle) + { + AdviseSupervisoryServerHandle = serverHandle; + AdviseSupervisoryItemHandle = itemHandle; + AdviseSupervisoryThreadId = Environment.CurrentManagedThreadId; + + if (adviseSupervisoryException is not null) + { + throw adviseSupervisoryException; + } + } } private sealed class FakeMxAccessComObjectFactory : IMxAccessComObjectFactory diff --git a/src/MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs b/src/MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs index ef2dffa..2593e19 100644 --- a/src/MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs +++ b/src/MxGateway.Worker.Tests/MxAccess/MxAccessLiveComCreationTests.cs @@ -215,6 +215,127 @@ public sealed class MxAccessLiveComCreationTests } } + [Fact] + public async Task AdviseAndUnAdvise_WhenOptedIn_RoundTripsInstalledMxAccessSubscription() + { + if (!RunLiveMxAccessTests()) + { + return; + } + + using MxAccessStaSession session = new(); + await session.StartAsync(workerProcessId: 1234); + + MxCommandReply registerReply = await RegisterLiveSessionAsync(session, "live-advise-register"); + int serverHandle = registerReply.Register.ServerHandle; + int itemHandle = 0; + bool advised = false; + + try + { + MxCommandReply addItemReply = await session.DispatchAsync(new StaCommand( + "session-1", + "live-advise-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 adviseReply = await session.DispatchAsync(new StaCommand( + "session-1", + "live-advise", + new MxCommand + { + Kind = MxCommandKind.Advise, + Advise = new AdviseCommand + { + ServerHandle = serverHandle, + ItemHandle = itemHandle, + }, + })); + + Assert.Equal(ProtocolStatusCode.Ok, adviseReply.ProtocolStatus.Code); + advised = true; + + MxCommandReply unAdviseReply = await session.DispatchAsync(new StaCommand( + "session-1", + "live-unadvise", + new MxCommand + { + Kind = MxCommandKind.UnAdvise, + UnAdvise = new UnAdviseCommand + { + ServerHandle = serverHandle, + ItemHandle = itemHandle, + }, + })); + + Assert.Equal(ProtocolStatusCode.Ok, unAdviseReply.ProtocolStatus.Code); + advised = false; + + MxCommandReply removeItemReply = await session.DispatchAsync(new StaCommand( + "session-1", + "live-advise-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 (advised && itemHandle > 0) + { + await session.DispatchAsync(new StaCommand( + "session-1", + "live-unadvise-cleanup", + new MxCommand + { + Kind = MxCommandKind.UnAdvise, + UnAdvise = new UnAdviseCommand + { + ServerHandle = serverHandle, + ItemHandle = itemHandle, + }, + })); + } + + if (itemHandle > 0) + { + await session.DispatchAsync(new StaCommand( + "session-1", + "live-advise-remove-item-cleanup", + new MxCommand + { + Kind = MxCommandKind.RemoveItem, + RemoveItem = new RemoveItemCommand + { + ServerHandle = serverHandle, + ItemHandle = itemHandle, + }, + })); + } + + await UnregisterLiveSessionAsync(session, serverHandle, "live-advise-unregister"); + } + } + private static bool RunLiveMxAccessTests() { return string.Equals( diff --git a/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs b/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs index b5f999d..ff506b3 100644 --- a/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs +++ b/src/MxGateway.Worker/MxAccess/IMxAccessServer.cs @@ -18,4 +18,16 @@ public interface IMxAccessServer void RemoveItem( int serverHandle, int itemHandle); + + void Advise( + int serverHandle, + int itemHandle); + + void UnAdvise( + int serverHandle, + int itemHandle); + + void AdviseSupervisory( + int serverHandle, + int itemHandle); } diff --git a/src/MxGateway.Worker/MxAccess/MxAccessAdviceKind.cs b/src/MxGateway.Worker/MxAccess/MxAccessAdviceKind.cs new file mode 100644 index 0000000..a3e1b25 --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/MxAccessAdviceKind.cs @@ -0,0 +1,7 @@ +namespace MxGateway.Worker.MxAccess; + +public enum MxAccessAdviceKind +{ + Plain = 1, + Supervisory = 2, +} diff --git a/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs b/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs index 7c687ed..4553b7a 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessComServer.cs @@ -73,6 +73,45 @@ public sealed class MxAccessComServer : IMxAccessServer Invoke(nameof(RemoveItem), serverHandle, itemHandle); } + public void Advise( + int serverHandle, + int itemHandle) + { + if (mxAccessComObject is ILMXProxyServer mxAccessServer) + { + mxAccessServer.Advise(serverHandle, itemHandle); + return; + } + + Invoke(nameof(Advise), serverHandle, itemHandle); + } + + public void UnAdvise( + int serverHandle, + int itemHandle) + { + if (mxAccessComObject is ILMXProxyServer mxAccessServer) + { + mxAccessServer.UnAdvise(serverHandle, itemHandle); + return; + } + + Invoke(nameof(UnAdvise), serverHandle, itemHandle); + } + + public void AdviseSupervisory( + int serverHandle, + int itemHandle) + { + if (mxAccessComObject is ILMXProxyServer4 mxAccessServer) + { + mxAccessServer.AdviseSupervisory(serverHandle, itemHandle); + return; + } + + Invoke(nameof(AdviseSupervisory), 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 09d62e1..71a0480 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessCommandExecutor.cs @@ -37,6 +37,9 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor MxCommandKind.AddItem => ExecuteAddItem(command), MxCommandKind.AddItem2 => ExecuteAddItem2(command), MxCommandKind.RemoveItem => ExecuteRemoveItem(command), + MxCommandKind.Advise => ExecuteAdvise(command), + MxCommandKind.UnAdvise => ExecuteUnAdvise(command), + MxCommandKind.AdviseSupervisory => ExecuteAdviseSupervisory(command), _ => CreateInvalidRequestReply(command, $"Unsupported MXAccess command kind {command.Kind}."), }; } @@ -130,6 +133,51 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor return CreateOkReply(command); } + private MxCommandReply ExecuteAdvise(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Advise) + { + return CreateInvalidRequestReply(command, "Advise command payload is required."); + } + + AdviseCommand adviseCommand = command.Command.Advise; + session.Advise( + adviseCommand.ServerHandle, + adviseCommand.ItemHandle); + + return CreateOkReply(command); + } + + private MxCommandReply ExecuteUnAdvise(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.UnAdvise) + { + return CreateInvalidRequestReply(command, "UnAdvise command payload is required."); + } + + UnAdviseCommand unAdviseCommand = command.Command.UnAdvise; + session.UnAdvise( + unAdviseCommand.ServerHandle, + unAdviseCommand.ItemHandle); + + return CreateOkReply(command); + } + + private MxCommandReply ExecuteAdviseSupervisory(StaCommand command) + { + if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AdviseSupervisory) + { + return CreateInvalidRequestReply(command, "AdviseSupervisory command payload is required."); + } + + AdviseSupervisoryCommand adviseSupervisoryCommand = command.Command.AdviseSupervisory; + session.AdviseSupervisory( + adviseSupervisoryCommand.ServerHandle, + adviseSupervisoryCommand.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 fb398d3..f6d94b0 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessHandleRegistry.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -7,6 +8,7 @@ public sealed class MxAccessHandleRegistry { private readonly Dictionary serverHandles = new(); private readonly Dictionary itemHandles = new(); + private readonly Dictionary adviceHandles = new(); public IReadOnlyList ServerHandles => serverHandles .Values @@ -19,6 +21,13 @@ public sealed class MxAccessHandleRegistry .ThenBy(handle => handle.ItemHandle) .ToArray(); + public IReadOnlyList AdviceHandles => adviceHandles + .Values + .OrderBy(handle => handle.ServerHandle) + .ThenBy(handle => handle.ItemHandle) + .ThenBy(handle => handle.AdviceKind) + .ToArray(); + public void RegisterServerHandle( int serverHandle, string clientName) @@ -37,6 +46,14 @@ public sealed class MxAccessHandleRegistry { itemHandles.Remove(key); } + + foreach (AdviceHandleKey key in adviceHandles + .Where(pair => pair.Value.ServerHandle == serverHandle) + .Select(pair => pair.Key) + .ToArray()) + { + adviceHandles.Remove(key); + } } public bool ContainsServerHandle(int serverHandle) @@ -64,6 +81,7 @@ public sealed class MxAccessHandleRegistry int itemHandle) { itemHandles.Remove(CreateItemKey(serverHandle, itemHandle)); + RemoveAdviceHandles(serverHandle, itemHandle); } public bool ContainsItemHandle( @@ -73,10 +91,84 @@ public sealed class MxAccessHandleRegistry return itemHandles.ContainsKey(CreateItemKey(serverHandle, itemHandle)); } + public void RegisterAdviceHandle( + int serverHandle, + int itemHandle, + MxAccessAdviceKind adviceKind) + { + AdviceHandleKey key = new(serverHandle, itemHandle, adviceKind); + adviceHandles[key] = new RegisteredAdviceHandle( + serverHandle, + itemHandle, + adviceKind); + } + + public void RemoveAdviceHandles( + int serverHandle, + int itemHandle) + { + foreach (AdviceHandleKey key in adviceHandles + .Where(pair => pair.Value.ServerHandle == serverHandle && pair.Value.ItemHandle == itemHandle) + .Select(pair => pair.Key) + .ToArray()) + { + adviceHandles.Remove(key); + } + } + + public bool ContainsAdviceHandle( + int serverHandle, + int itemHandle, + MxAccessAdviceKind adviceKind) + { + return adviceHandles.ContainsKey(new AdviceHandleKey(serverHandle, itemHandle, adviceKind)); + } + private static long CreateItemKey( int serverHandle, int itemHandle) { return ((long)serverHandle << 32) | (uint)itemHandle; } + + private readonly struct AdviceHandleKey : IEquatable + { + private readonly int serverHandle; + private readonly int itemHandle; + private readonly MxAccessAdviceKind adviceKind; + + public AdviceHandleKey( + int serverHandle, + int itemHandle, + MxAccessAdviceKind adviceKind) + { + this.serverHandle = serverHandle; + this.itemHandle = itemHandle; + this.adviceKind = adviceKind; + } + + public bool Equals(AdviceHandleKey other) + { + return serverHandle == other.serverHandle + && itemHandle == other.itemHandle + && adviceKind == other.adviceKind; + } + + public override bool Equals(object? obj) + { + return obj is AdviceHandleKey other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + int hashCode = serverHandle; + hashCode = (hashCode * 397) ^ itemHandle; + hashCode = (hashCode * 397) ^ (int)adviceKind; + + return hashCode; + } + } + } } diff --git a/src/MxGateway.Worker/MxAccess/MxAccessSession.cs b/src/MxGateway.Worker/MxAccess/MxAccessSession.cs index b8cd960..b4bc984 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessSession.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessSession.cs @@ -151,6 +151,42 @@ public sealed class MxAccessSession : IDisposable handleRegistry.RemoveItemHandle(serverHandle, itemHandle); } + public void Advise( + int serverHandle, + int itemHandle) + { + ThrowIfDisposed(); + + mxAccessServer.Advise(serverHandle, itemHandle); + handleRegistry.RegisterAdviceHandle( + serverHandle, + itemHandle, + MxAccessAdviceKind.Plain); + } + + public void UnAdvise( + int serverHandle, + int itemHandle) + { + ThrowIfDisposed(); + + mxAccessServer.UnAdvise(serverHandle, itemHandle); + handleRegistry.RemoveAdviceHandles(serverHandle, itemHandle); + } + + public void AdviseSupervisory( + int serverHandle, + int itemHandle) + { + ThrowIfDisposed(); + + mxAccessServer.AdviseSupervisory(serverHandle, itemHandle); + handleRegistry.RegisterAdviceHandle( + serverHandle, + itemHandle, + MxAccessAdviceKind.Supervisory); + } + public void Dispose() { if (disposed) diff --git a/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs b/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs index de522f3..6fc9757 100644 --- a/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs +++ b/src/MxGateway.Worker/MxAccess/MxAccessStaSession.cs @@ -94,6 +94,19 @@ public sealed class MxAccessStaSession : IDisposable cancellationToken); } + public Task> GetRegisteredAdviceHandlesAsync( + CancellationToken cancellationToken = default) + { + if (session is null) + { + throw new InvalidOperationException("MXAccess COM session has not been started."); + } + + return staRuntime.InvokeAsync( + () => session.HandleRegistry.AdviceHandles, + cancellationToken); + } + public void Dispose() { if (disposed) diff --git a/src/MxGateway.Worker/MxAccess/RegisteredAdviceHandle.cs b/src/MxGateway.Worker/MxAccess/RegisteredAdviceHandle.cs new file mode 100644 index 0000000..0d3e280 --- /dev/null +++ b/src/MxGateway.Worker/MxAccess/RegisteredAdviceHandle.cs @@ -0,0 +1,20 @@ +namespace MxGateway.Worker.MxAccess; + +public sealed class RegisteredAdviceHandle +{ + public RegisteredAdviceHandle( + int serverHandle, + int itemHandle, + MxAccessAdviceKind adviceKind) + { + ServerHandle = serverHandle; + ItemHandle = itemHandle; + AdviceKind = adviceKind; + } + + public int ServerHandle { get; } + + public int ItemHandle { get; } + + public MxAccessAdviceKind AdviceKind { get; } +}