using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Google.Protobuf.WellKnownTypes;
using MxGateway.Contracts.Proto;
namespace MxGateway.Worker.MxAccess;
public sealed class MxAccessSession : IDisposable
{
private readonly object mxAccessComObject;
private readonly IMxAccessServer mxAccessServer;
private readonly IMxAccessEventSink eventSink;
private readonly MxAccessHandleRegistry handleRegistry;
private readonly MxAccessValueCache valueCache;
private bool disposed;
private MxAccessSession(
object mxAccessComObject,
IMxAccessServer mxAccessServer,
IMxAccessEventSink eventSink,
MxAccessHandleRegistry handleRegistry,
MxAccessValueCache valueCache,
int creationThreadId)
{
this.mxAccessComObject = mxAccessComObject ?? throw new ArgumentNullException(nameof(mxAccessComObject));
this.mxAccessServer = mxAccessServer ?? throw new ArgumentNullException(nameof(mxAccessServer));
this.eventSink = eventSink ?? throw new ArgumentNullException(nameof(eventSink));
this.handleRegistry = handleRegistry ?? throw new ArgumentNullException(nameof(handleRegistry));
this.valueCache = valueCache ?? throw new ArgumentNullException(nameof(valueCache));
CreationThreadId = creationThreadId;
}
/// The thread ID where this session was created.
public int CreationThreadId { get; }
/// The registry for tracking opened handles.
public MxAccessHandleRegistry HandleRegistry => handleRegistry;
///
/// Per-session last-value cache populated by the event sink. ReadBulk
/// consults this cache before falling back to its own snapshot
/// lifecycle so it can serve a "current value" for an already-advised
/// tag without touching the existing subscription.
///
public MxAccessValueCache ValueCache => valueCache;
/// Creates a WorkerReady message with session metadata.
/// Process ID of the worker.
public WorkerReady CreateWorkerReady(int workerProcessId)
{
return new WorkerReady
{
WorkerProcessId = workerProcessId,
MxaccessProgid = MxAccessInteropInfo.ProgId,
MxaccessClsid = MxAccessInteropInfo.Clsid,
ReadyTimestamp = Timestamp.FromDateTime(DateTime.UtcNow),
};
}
/// Creates and initializes an MXAccess COM session.
/// Factory to create the MXAccess COM object.
/// Event sink to attach to the COM object.
/// Identifier of the session.
public static MxAccessSession Create(
IMxAccessComObjectFactory factory,
IMxAccessEventSink eventSink,
string sessionId)
{
if (factory is null)
{
throw new ArgumentNullException(nameof(factory));
}
if (eventSink is null)
{
throw new ArgumentNullException(nameof(eventSink));
}
object? mxAccessComObject = null;
try
{
mxAccessComObject = factory.Create();
if (mxAccessComObject is null)
{
throw new InvalidOperationException("MXAccess COM factory returned null.");
}
eventSink.Attach(mxAccessComObject, sessionId);
// Share the event sink's value cache when one is wired (the
// production MxAccessBaseEventSink path) so OnDataChange writes and
// ReadBulk reads both see the same instance. Fall back to a fresh
// cache for test fakes that supply their own sink — ReadBulk simply
// never serves cached values in that case.
MxAccessValueCache valueCache = eventSink is MxAccessBaseEventSink baseSink
? baseSink.ValueCache
: new MxAccessValueCache();
return new MxAccessSession(
mxAccessComObject,
new MxAccessComServer(mxAccessComObject),
eventSink,
new MxAccessHandleRegistry(),
valueCache,
Environment.CurrentManagedThreadId);
}
catch (Exception exception)
{
try
{
eventSink.Detach();
}
catch
{
// Preserve the creation failure while still releasing the COM object below.
}
if (mxAccessComObject is not null && Marshal.IsComObject(mxAccessComObject))
{
Marshal.FinalReleaseComObject(mxAccessComObject);
}
throw MxAccessCreationException.From(exception);
}
}
/// Registers a client with MXAccess and returns the server handle.
/// Name of the client to register.
public int Register(string clientName)
{
ThrowIfDisposed();
int serverHandle = mxAccessServer.Register(clientName);
handleRegistry.RegisterServerHandle(serverHandle, clientName);
return serverHandle;
}
/// Unregisters a client from MXAccess.
/// Handle returned by the worker.
public void Unregister(int serverHandle)
{
ThrowIfDisposed();
mxAccessServer.Unregister(serverHandle);
handleRegistry.UnregisterServerHandle(serverHandle);
}
/// Adds an item to an MXAccess server and returns the item handle.
/// Handle returned by the worker.
/// Definition or address of the item to add.
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;
}
/// Adds an item with context to an MXAccess server and returns the item handle.
/// Handle returned by the worker.
/// Definition or address of the item to add.
/// Context string for the item.
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;
}
/// Removes an item from an MXAccess server.
/// Handle returned by the worker.
/// Handle returned by the worker.
public void RemoveItem(
int serverHandle,
int itemHandle)
{
ThrowIfDisposed();
mxAccessServer.RemoveItem(serverHandle, itemHandle);
handleRegistry.RemoveItemHandle(serverHandle, itemHandle);
// Evict the last-value entry so a future AddItem + Advise on the
// same handle id (which MXAccess may reuse) does not serve a stale
// OnDataChange snapshot from the previous lifetime.
valueCache.Remove(serverHandle, itemHandle);
}
/// Advises on item changes with plain subscription.
/// Handle returned by the worker.
/// Handle returned by the worker.
public void Advise(
int serverHandle,
int itemHandle)
{
ThrowIfDisposed();
mxAccessServer.Advise(serverHandle, itemHandle);
handleRegistry.RegisterAdviceHandle(
serverHandle,
itemHandle,
MxAccessAdviceKind.Plain);
}
/// Removes plain advice subscription from an item.
/// Handle returned by the worker.
/// Handle returned by the worker.
public void UnAdvise(
int serverHandle,
int itemHandle)
{
ThrowIfDisposed();
mxAccessServer.UnAdvise(serverHandle, itemHandle);
handleRegistry.RemoveAdviceHandles(serverHandle, itemHandle);
}
/// Advises on item changes with supervisory subscription.
/// Handle returned by the worker.
/// Handle returned by the worker.
public void AdviseSupervisory(
int serverHandle,
int itemHandle)
{
ThrowIfDisposed();
mxAccessServer.AdviseSupervisory(serverHandle, itemHandle);
handleRegistry.RegisterAdviceHandle(
serverHandle,
itemHandle,
MxAccessAdviceKind.Supervisory);
}
/// Writes a value to an item.
/// Handle returned by the worker.
/// Handle returned by the worker.
/// COM-marshalable value to write.
/// MXAccess user id (security classification) for the write.
public void Write(
int serverHandle,
int itemHandle,
object? value,
int userId)
{
ThrowIfDisposed();
mxAccessServer.Write(serverHandle, itemHandle, value, userId);
}
/// Writes a value with an explicit source timestamp to an item.
/// Handle returned by the worker.
/// Handle returned by the worker.
/// COM-marshalable value to write.
/// COM-marshalable source timestamp for the write.
/// MXAccess user id (security classification) for the write.
public void Write2(
int serverHandle,
int itemHandle,
object? value,
object? timestamp,
int userId)
{
ThrowIfDisposed();
mxAccessServer.Write2(serverHandle, itemHandle, value, timestamp, userId);
}
/// Performs a secured/verified write to an item.
/// Handle returned by the worker.
/// Handle returned by the worker.
/// MXAccess user id of the operator performing the write.
/// MXAccess user id of the verifier authorizing the write.
/// COM-marshalable value to write.
public void WriteSecured(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value)
{
ThrowIfDisposed();
mxAccessServer.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value);
}
/// Performs a secured/verified write with an explicit source timestamp.
/// Handle returned by the worker.
/// Handle returned by the worker.
/// MXAccess user id of the operator performing the write.
/// MXAccess user id of the verifier authorizing the write.
/// COM-marshalable value to write.
/// COM-marshalable source timestamp for the write.
public void WriteSecured2(
int serverHandle,
int itemHandle,
int currentUserId,
int verifierUserId,
object? value,
object? timestamp)
{
ThrowIfDisposed();
mxAccessServer.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp);
}
/// Adds multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item definitions to add.
public IReadOnlyList AddItemBulk(
int serverHandle,
IEnumerable tagAddresses)
{
ThrowIfDisposed();
if (tagAddresses is null)
{
throw new ArgumentNullException(nameof(tagAddresses));
}
List results = new();
foreach (string? tagAddress in tagAddresses)
{
if (string.IsNullOrWhiteSpace(tagAddress))
{
results.Add(Failed(serverHandle, tagAddress ?? string.Empty, itemHandle: 0, "Tag address is required."));
continue;
}
try
{
int itemHandle = AddItem(serverHandle, tagAddress);
results.Add(Succeeded(serverHandle, tagAddress, itemHandle));
}
catch (Exception exception)
{
results.Add(Failed(serverHandle, tagAddress, itemHandle: 0, exception.Message));
}
}
return results;
}
/// Advises on multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item handles to advise on.
public IReadOnlyList AdviseItemBulk(
int serverHandle,
IEnumerable itemHandles)
{
ThrowIfDisposed();
if (itemHandles is null)
{
throw new ArgumentNullException(nameof(itemHandles));
}
List results = new();
foreach (int itemHandle in itemHandles)
{
try
{
Advise(serverHandle, itemHandle);
results.Add(Succeeded(serverHandle, string.Empty, itemHandle));
}
catch (Exception exception)
{
results.Add(Failed(serverHandle, string.Empty, itemHandle, exception.Message));
}
}
return results;
}
/// Removes multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item handles to remove.
public IReadOnlyList RemoveItemBulk(
int serverHandle,
IEnumerable itemHandles)
{
ThrowIfDisposed();
if (itemHandles is null)
{
throw new ArgumentNullException(nameof(itemHandles));
}
List results = new();
foreach (int itemHandle in itemHandles)
{
try
{
RemoveItem(serverHandle, itemHandle);
results.Add(Succeeded(serverHandle, string.Empty, itemHandle));
}
catch (Exception exception)
{
results.Add(Failed(serverHandle, string.Empty, itemHandle, exception.Message));
}
}
return results;
}
/// Removes advice subscriptions from multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item handles to unadvise.
public IReadOnlyList UnAdviseItemBulk(
int serverHandle,
IEnumerable itemHandles)
{
ThrowIfDisposed();
if (itemHandles is null)
{
throw new ArgumentNullException(nameof(itemHandles));
}
List results = new();
foreach (int itemHandle in itemHandles)
{
try
{
UnAdvise(serverHandle, itemHandle);
results.Add(Succeeded(serverHandle, string.Empty, itemHandle));
}
catch (Exception exception)
{
results.Add(Failed(serverHandle, string.Empty, itemHandle, exception.Message));
}
}
return results;
}
/// Adds multiple items and subscribes to them in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item definitions to add and subscribe to.
public IReadOnlyList SubscribeBulk(
int serverHandle,
IEnumerable tagAddresses)
{
ThrowIfDisposed();
if (tagAddresses is null)
{
throw new ArgumentNullException(nameof(tagAddresses));
}
List results = new();
foreach (string? tagAddress in tagAddresses)
{
if (string.IsNullOrWhiteSpace(tagAddress))
{
results.Add(Failed(serverHandle, tagAddress ?? string.Empty, itemHandle: 0, "Tag address is required."));
continue;
}
int itemHandle = 0;
try
{
itemHandle = AddItem(serverHandle, tagAddress);
Advise(serverHandle, itemHandle);
results.Add(Succeeded(serverHandle, tagAddress, itemHandle));
}
catch (Exception exception)
{
string errorMessage = exception.Message;
if (itemHandle != 0)
{
errorMessage = AppendRemoveItemCleanup(serverHandle, itemHandle, errorMessage);
}
results.Add(Failed(serverHandle, tagAddress, itemHandle, errorMessage));
}
}
return results;
}
/// Unsubscribes from multiple items in bulk, returning success/failure results.
/// Handle returned by the worker.
/// Enumerable of item handles to unsubscribe from.
public IReadOnlyList UnsubscribeBulk(
int serverHandle,
IEnumerable itemHandles)
{
ThrowIfDisposed();
if (itemHandles is null)
{
throw new ArgumentNullException(nameof(itemHandles));
}
List results = new();
foreach (int itemHandle in itemHandles)
{
List errors = new();
try
{
UnAdvise(serverHandle, itemHandle);
}
catch (Exception exception)
{
errors.Add($"UnAdvise failed: {exception.Message}");
}
try
{
RemoveItem(serverHandle, itemHandle);
}
catch (Exception exception)
{
errors.Add($"RemoveItem failed: {exception.Message}");
}
results.Add(errors.Count == 0
? Succeeded(serverHandle, string.Empty, itemHandle)
: Failed(serverHandle, string.Empty, itemHandle, string.Join("; ", errors)));
}
return results;
}
///
/// Bulk write — runs sequentially for each entry.
/// Each entry's turns the protobuf
/// MxValue into a COM-marshalable variant. Per-item failures are
/// captured as entries with
/// was_successful = false; the loop never throws.
///
public IReadOnlyList WriteBulk(
int serverHandle,
IReadOnlyList entries,
Func convertValue)
{
ThrowIfDisposed();
if (entries is null)
{
throw new ArgumentNullException(nameof(entries));
}
if (convertValue is null)
{
throw new ArgumentNullException(nameof(convertValue));
}
List results = new(entries.Count);
foreach (WriteBulkEntry entry in entries)
{
results.Add(ExecuteBulkWriteEntry(
serverHandle,
entry.ItemHandle,
() => Write(serverHandle, entry.ItemHandle, convertValue(entry.Value), entry.UserId)));
}
return results;
}
/// Bulk Write2 — sequential MXAccess per entry.
public IReadOnlyList Write2Bulk(
int serverHandle,
IReadOnlyList entries,
Func convertValue)
{
ThrowIfDisposed();
if (entries is null)
{
throw new ArgumentNullException(nameof(entries));
}
if (convertValue is null)
{
throw new ArgumentNullException(nameof(convertValue));
}
List results = new(entries.Count);
foreach (Write2BulkEntry entry in entries)
{
results.Add(ExecuteBulkWriteEntry(
serverHandle,
entry.ItemHandle,
() => Write2(
serverHandle,
entry.ItemHandle,
convertValue(entry.Value),
convertValue(entry.TimestampValue),
entry.UserId)));
}
return results;
}
/// Bulk WriteSecured — sequential MXAccess per entry.
public IReadOnlyList WriteSecuredBulk(
int serverHandle,
IReadOnlyList entries,
Func convertValue)
{
ThrowIfDisposed();
if (entries is null)
{
throw new ArgumentNullException(nameof(entries));
}
if (convertValue is null)
{
throw new ArgumentNullException(nameof(convertValue));
}
List results = new(entries.Count);
foreach (WriteSecuredBulkEntry entry in entries)
{
results.Add(ExecuteBulkWriteEntry(
serverHandle,
entry.ItemHandle,
() => WriteSecured(
serverHandle,
entry.ItemHandle,
entry.CurrentUserId,
entry.VerifierUserId,
convertValue(entry.Value))));
}
return results;
}
/// Bulk WriteSecured2 — sequential MXAccess per entry.
public IReadOnlyList WriteSecured2Bulk(
int serverHandle,
IReadOnlyList entries,
Func convertValue)
{
ThrowIfDisposed();
if (entries is null)
{
throw new ArgumentNullException(nameof(entries));
}
if (convertValue is null)
{
throw new ArgumentNullException(nameof(convertValue));
}
List results = new(entries.Count);
foreach (WriteSecured2BulkEntry entry in entries)
{
results.Add(ExecuteBulkWriteEntry(
serverHandle,
entry.ItemHandle,
() => WriteSecured2(
serverHandle,
entry.ItemHandle,
entry.CurrentUserId,
entry.VerifierUserId,
convertValue(entry.Value),
convertValue(entry.TimestampValue))));
}
return results;
}
///
/// Bulk read snapshot. For each requested tag, returns the most recent
/// OnDataChange value if the tag is already advised AND a cached value
/// exists (no subscription side effects); otherwise takes the AddItem
/// + Advise + wait + UnAdvise + RemoveItem snapshot lifecycle itself.
/// bounds the wait per-tag in the snapshot
/// case; is invoked on every poll
/// iteration so the worker's STA can dispatch the incoming MXAccess
/// message that carries the value.
///
public IReadOnlyList ReadBulk(
int serverHandle,
IReadOnlyList tagAddresses,
TimeSpan timeout,
Action pumpStep)
{
ThrowIfDisposed();
if (tagAddresses is null)
{
throw new ArgumentNullException(nameof(tagAddresses));
}
if (pumpStep is null)
{
throw new ArgumentNullException(nameof(pumpStep));
}
List results = new(tagAddresses.Count);
foreach (string? tagAddress in tagAddresses)
{
if (string.IsNullOrWhiteSpace(tagAddress))
{
results.Add(FailedRead(serverHandle, tagAddress ?? string.Empty, itemHandle: 0, wasCached: false, "Tag address is required."));
continue;
}
results.Add(ReadOneTag(serverHandle, tagAddress, timeout, pumpStep));
}
return results;
}
private BulkReadResult ReadOneTag(
int serverHandle,
string tagAddress,
TimeSpan timeout,
Action pumpStep)
{
// 1. Cached-and-advised fast path: scan the registry for a live item
// handle matching this tag and check whether the value cache has a
// payload for it. If so, return the cached value without touching
// the existing subscription — the caller didn't create it, so
// ReadBulk must not tear it down.
if (TryGetCachedReadFor(serverHandle, tagAddress, out int cachedItemHandle, out MxAccessValueCache.CachedValue cachedValue))
{
return SucceededRead(
serverHandle,
tagAddress,
cachedItemHandle,
wasCached: true,
cachedValue);
}
// 2. Snapshot lifecycle. Reserve our own item handle, advise, pump
// until we see a fresh OnDataChange (or the deadline elapses),
// then tear it down.
int itemHandle = 0;
bool advised = false;
try
{
itemHandle = AddItem(serverHandle, tagAddress);
ulong baseline = valueCache.CurrentVersion(serverHandle, itemHandle);
Advise(serverHandle, itemHandle);
advised = true;
DateTime deadline = DateTime.UtcNow + timeout;
bool gotValue = valueCache.TryWaitForUpdate(
serverHandle,
itemHandle,
baseline,
deadline,
pumpStep,
out MxAccessValueCache.CachedValue snapshot);
return gotValue
? SucceededRead(serverHandle, tagAddress, itemHandle, wasCached: false, snapshot)
: FailedRead(
serverHandle,
tagAddress,
itemHandle,
wasCached: false,
$"ReadBulk timed out after {timeout.TotalMilliseconds:F0} ms waiting for first OnDataChange.");
}
catch (Exception exception)
{
return FailedRead(serverHandle, tagAddress, itemHandle, wasCached: false, exception.Message);
}
finally
{
// Snapshot teardown — best-effort. Errors here are noted on the
// diagnostic message of the original result (above) by appending
// a cleanup suffix; we never re-throw from finally.
if (advised)
{
try { UnAdvise(serverHandle, itemHandle); } catch { /* swallow — best effort */ }
}
if (itemHandle != 0)
{
try { RemoveItem(serverHandle, itemHandle); } catch { /* swallow — best effort */ }
}
}
}
private bool TryGetCachedReadFor(
int serverHandle,
string tagAddress,
out int itemHandle,
out MxAccessValueCache.CachedValue cachedValue)
{
// Linear scan — bulk-read sizes are small in practice and the registry
// is keyed by handle, not by tag. If profiling ever shows this hot, a
// reverse tag→handle map can be added on the registry side.
foreach (RegisteredItemHandle registered in handleRegistry.ItemHandles)
{
if (registered.ServerHandle != serverHandle)
{
continue;
}
if (!string.Equals(registered.ItemDefinition, tagAddress, StringComparison.Ordinal))
{
continue;
}
if (!handleRegistry.ContainsAdviceHandle(serverHandle, registered.ItemHandle, MxAccessAdviceKind.Plain)
&& !handleRegistry.ContainsAdviceHandle(serverHandle, registered.ItemHandle, MxAccessAdviceKind.Supervisory))
{
// Tag is added but not advised — no fresh OnDataChange will
// arrive without us advising. Fall through to the snapshot
// path which advises explicitly.
continue;
}
if (valueCache.TryGet(serverHandle, registered.ItemHandle, out cachedValue))
{
itemHandle = registered.ItemHandle;
return true;
}
}
itemHandle = 0;
cachedValue = default;
return false;
}
private BulkWriteResult ExecuteBulkWriteEntry(
int serverHandle,
int itemHandle,
Action invokeWrite)
{
try
{
invokeWrite();
return new BulkWriteResult
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
WasSuccessful = true,
ErrorMessage = string.Empty,
};
}
catch (System.Runtime.InteropServices.COMException comException)
{
BulkWriteResult result = new()
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
WasSuccessful = false,
ErrorMessage = comException.Message,
};
result.Hresult = comException.HResult;
return result;
}
catch (Exception exception)
{
return new BulkWriteResult
{
ServerHandle = serverHandle,
ItemHandle = itemHandle,
WasSuccessful = false,
ErrorMessage = exception.Message,
};
}
}
private static BulkReadResult SucceededRead(
int serverHandle,
string tagAddress,
int itemHandle,
bool wasCached,
MxAccessValueCache.CachedValue snapshot)
{
BulkReadResult result = new()
{
ServerHandle = serverHandle,
TagAddress = tagAddress,
ItemHandle = itemHandle,
WasSuccessful = true,
WasCached = wasCached,
Quality = snapshot.Quality,
ErrorMessage = string.Empty,
};
if (snapshot.Value is not null)
{
result.Value = snapshot.Value;
}
if (snapshot.SourceTimestamp is not null)
{
result.SourceTimestamp = snapshot.SourceTimestamp;
}
if (snapshot.Statuses is not null)
{
result.Statuses.Add(snapshot.Statuses);
}
return result;
}
private static BulkReadResult FailedRead(
int serverHandle,
string tagAddress,
int itemHandle,
bool wasCached,
string errorMessage)
{
return new BulkReadResult
{
ServerHandle = serverHandle,
TagAddress = tagAddress,
ItemHandle = itemHandle,
WasSuccessful = false,
WasCached = wasCached,
ErrorMessage = errorMessage,
};
}
/// Gracefully shuts down the session, cleaning up all handles.
public MxAccessShutdownResult ShutdownGracefully()
{
if (disposed)
{
return new MxAccessShutdownResult(Array.Empty());
}
List failures = new();
CleanupAdviceHandles(failures);
CleanupItemHandles(failures);
CleanupServerHandles(failures);
DisposeCore(failures);
return new MxAccessShutdownResult(failures);
}
/// Releases the MXAccess COM object and resources.
public void Dispose()
{
if (disposed)
{
return;
}
DisposeCore(failures: null);
}
private void CleanupAdviceHandles(ICollection failures)
{
HashSet cleanedPairs = new();
foreach (RegisteredAdviceHandle adviceHandle in handleRegistry.AdviceHandles)
{
long key = CreateItemKey(adviceHandle.ServerHandle, adviceHandle.ItemHandle);
if (!cleanedPairs.Add(key))
{
continue;
}
try
{
mxAccessServer.UnAdvise(adviceHandle.ServerHandle, adviceHandle.ItemHandle);
handleRegistry.RemoveAdviceHandles(adviceHandle.ServerHandle, adviceHandle.ItemHandle);
}
catch (Exception exception)
{
failures.Add(new MxAccessShutdownFailure(
nameof(UnAdvise),
adviceHandle.ServerHandle,
adviceHandle.ItemHandle,
exception));
}
}
}
private void CleanupItemHandles(ICollection failures)
{
foreach (RegisteredItemHandle itemHandle in handleRegistry.ItemHandles)
{
try
{
mxAccessServer.RemoveItem(itemHandle.ServerHandle, itemHandle.ItemHandle);
handleRegistry.RemoveItemHandle(itemHandle.ServerHandle, itemHandle.ItemHandle);
}
catch (Exception exception)
{
failures.Add(new MxAccessShutdownFailure(
nameof(RemoveItem),
itemHandle.ServerHandle,
itemHandle.ItemHandle,
exception));
}
}
}
private void CleanupServerHandles(ICollection failures)
{
foreach (RegisteredServerHandle serverHandle in handleRegistry.ServerHandles)
{
try
{
mxAccessServer.Unregister(serverHandle.ServerHandle);
handleRegistry.UnregisterServerHandle(serverHandle.ServerHandle);
}
catch (Exception exception)
{
failures.Add(new MxAccessShutdownFailure(
nameof(Unregister),
serverHandle.ServerHandle,
itemHandle: null,
exception));
}
}
}
private static long CreateItemKey(
int serverHandle,
int itemHandle)
{
return ((long)serverHandle << 32) | (uint)itemHandle;
}
private string AppendRemoveItemCleanup(
int serverHandle,
int itemHandle,
string errorMessage)
{
try
{
RemoveItem(serverHandle, itemHandle);
return $"{errorMessage}; cleanup RemoveItem succeeded.";
}
catch (Exception cleanupException)
{
return $"{errorMessage}; cleanup RemoveItem failed: {cleanupException.Message}";
}
}
private static SubscribeResult Succeeded(
int serverHandle,
string tagAddress,
int itemHandle)
{
return new SubscribeResult
{
ServerHandle = serverHandle,
TagAddress = tagAddress,
ItemHandle = itemHandle,
WasSuccessful = true,
ErrorMessage = string.Empty,
};
}
private static SubscribeResult Failed(
int serverHandle,
string tagAddress,
int itemHandle,
string errorMessage)
{
return new SubscribeResult
{
ServerHandle = serverHandle,
TagAddress = tagAddress,
ItemHandle = itemHandle,
WasSuccessful = false,
ErrorMessage = errorMessage,
};
}
private void DisposeCore(ICollection? failures)
{
Exception? detachException = null;
try
{
eventSink.Detach();
}
catch (Exception exception)
{
detachException = exception;
failures?.Add(new MxAccessShutdownFailure(
"DetachEvents",
serverHandle: null,
itemHandle: null,
exception));
}
try
{
if (Marshal.IsComObject(mxAccessComObject))
{
Marshal.FinalReleaseComObject(mxAccessComObject);
}
}
catch (Exception exception) when (failures is not null)
{
failures.Add(new MxAccessShutdownFailure(
"ReleaseComObject",
serverHandle: null,
itemHandle: null,
exception));
}
disposed = true;
if (detachException is not null && failures is null)
{
throw detachException;
}
}
private void ThrowIfDisposed()
{
if (disposed)
{
throw new ObjectDisposedException(nameof(MxAccessSession));
}
}
}