Implement worker advise commands

This commit is contained in:
Joseph Doherty
2026-04-26 18:41:10 -04:00
parent 0fd954d94c
commit a871f2f2e5
11 changed files with 734 additions and 1 deletions
+23
View File
@@ -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.
@@ -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
@@ -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(
@@ -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);
}
@@ -0,0 +1,7 @@
namespace MxGateway.Worker.MxAccess;
public enum MxAccessAdviceKind
{
Plain = 1,
Supervisory = 2,
}
@@ -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)
@@ -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
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
@@ -7,6 +8,7 @@ public sealed class MxAccessHandleRegistry
{
private readonly Dictionary<int, RegisteredServerHandle> serverHandles = new();
private readonly Dictionary<long, RegisteredItemHandle> itemHandles = new();
private readonly Dictionary<AdviceHandleKey, RegisteredAdviceHandle> adviceHandles = new();
public IReadOnlyList<RegisteredServerHandle> ServerHandles => serverHandles
.Values
@@ -19,6 +21,13 @@ public sealed class MxAccessHandleRegistry
.ThenBy(handle => handle.ItemHandle)
.ToArray();
public IReadOnlyList<RegisteredAdviceHandle> 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<AdviceHandleKey>
{
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;
}
}
}
}
@@ -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)
@@ -94,6 +94,19 @@ public sealed class MxAccessStaSession : IDisposable
cancellationToken);
}
public Task<IReadOnlyList<RegisteredAdviceHandle>> 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)
@@ -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; }
}