fca978de07
Sweep of 203 source files resolving CommentChecker findings: add <summary>/<param>/<returns>/<inheritdoc> where missing, and remove resolved task/issue tracking markers (Tests-NNN, Worker-NNN, Server-NNN, Task N) from code comments. Comment/doc-only — no logic changes. Server+Tests build clean under TreatWarningsAsErrors.
1298 lines
46 KiB
C#
1298 lines
46 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.InteropServices;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
|
|
|
namespace ZB.MOM.WW.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;
|
|
}
|
|
|
|
/// <summary>The thread ID where this session was created.</summary>
|
|
public int CreationThreadId { get; }
|
|
|
|
/// <summary>The registry for tracking opened handles.</summary>
|
|
public MxAccessHandleRegistry HandleRegistry => handleRegistry;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public MxAccessValueCache ValueCache => valueCache;
|
|
|
|
/// <summary>Creates a WorkerReady message with session metadata.</summary>
|
|
/// <param name="workerProcessId">Process ID of the worker.</param>
|
|
/// <returns>The populated <see cref="WorkerReady"/> message.</returns>
|
|
public WorkerReady CreateWorkerReady(int workerProcessId)
|
|
{
|
|
return new WorkerReady
|
|
{
|
|
WorkerProcessId = workerProcessId,
|
|
MxaccessProgid = MxAccessInteropInfo.ProgId,
|
|
MxaccessClsid = MxAccessInteropInfo.Clsid,
|
|
ReadyTimestamp = Timestamp.FromDateTime(DateTime.UtcNow),
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Test-only seam: constructs a session that bypasses the live COM
|
|
/// factory. The caller supplies the <see cref="IMxAccessServer"/> and
|
|
/// <see cref="IMxAccessEventSink"/> directly so tests can exercise
|
|
/// session methods without touching MXAccess COM. This is exposed via
|
|
/// <c>InternalsVisibleTo("ZB.MOM.WW.MxGateway.Worker.Tests")</c>; production code
|
|
/// must use the <see cref="Create"/> factory.
|
|
///
|
|
/// A runtime guard rejects an <see cref="MxAccessBaseEventSink"/> —
|
|
/// the production sink wired by <see cref="Create"/> — because the
|
|
/// <c>new object()</c> stand-in this factory uses for the COM object
|
|
/// would silently bypass <see cref="System.Runtime.InteropServices.Marshal.IsComObject(object)"/>
|
|
/// during disposal and mask lifetime regressions.
|
|
/// </summary>
|
|
/// <param name="mxAccessServer">The server abstraction to drive.</param>
|
|
/// <param name="eventSink">The event sink to attach to the session.</param>
|
|
/// <param name="handleRegistry">Optional handle registry; a fresh one is created when null.</param>
|
|
/// <param name="valueCache">Optional value cache; a fresh one is created when null.</param>
|
|
/// <param name="creationThreadId">Optional creation thread id; defaults to the current managed thread id.</param>
|
|
/// <returns>A test-only <see cref="MxAccessSession"/> backed by the supplied test doubles.</returns>
|
|
/// <exception cref="ArgumentException">
|
|
/// Thrown when <paramref name="eventSink"/> is the production
|
|
/// <see cref="MxAccessBaseEventSink"/>. Tests must pass a test
|
|
/// double sink — production code must use <see cref="Create"/>.
|
|
/// </exception>
|
|
internal static MxAccessSession CreateForTesting(
|
|
IMxAccessServer mxAccessServer,
|
|
IMxAccessEventSink eventSink,
|
|
MxAccessHandleRegistry? handleRegistry = null,
|
|
MxAccessValueCache? valueCache = null,
|
|
int? creationThreadId = null)
|
|
{
|
|
if (eventSink is MxAccessBaseEventSink)
|
|
{
|
|
throw new ArgumentException(
|
|
"CreateForTesting must not be used with the production MxAccessBaseEventSink. "
|
|
+ "Use MxAccessSession.Create for production code; pass a test-double IMxAccessEventSink here.",
|
|
nameof(eventSink));
|
|
}
|
|
|
|
return new MxAccessSession(
|
|
new object(),
|
|
mxAccessServer,
|
|
eventSink,
|
|
handleRegistry ?? new MxAccessHandleRegistry(),
|
|
valueCache ?? new MxAccessValueCache(),
|
|
creationThreadId ?? Environment.CurrentManagedThreadId);
|
|
}
|
|
|
|
/// <summary>Creates and initializes an MXAccess COM session.</summary>
|
|
/// <param name="factory">Factory to create the MXAccess COM object.</param>
|
|
/// <param name="eventSink">Event sink to attach to the COM object.</param>
|
|
/// <param name="sessionId">Identifier of the session.</param>
|
|
/// <returns>The initialized <see cref="MxAccessSession"/>.</returns>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Registers a client with MXAccess and returns the server handle.</summary>
|
|
/// <param name="clientName">Name of the client to register.</param>
|
|
/// <returns>The server handle assigned to the registered client.</returns>
|
|
public int Register(string clientName)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
int serverHandle = mxAccessServer.Register(clientName);
|
|
handleRegistry.RegisterServerHandle(serverHandle, clientName);
|
|
|
|
return serverHandle;
|
|
}
|
|
|
|
/// <summary>Unregisters a client from MXAccess.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
public void Unregister(int serverHandle)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
mxAccessServer.Unregister(serverHandle);
|
|
handleRegistry.UnregisterServerHandle(serverHandle);
|
|
}
|
|
|
|
/// <summary>Adds an item to an MXAccess server and returns the item handle.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemDefinition">Definition or address of the item to add.</param>
|
|
/// <returns>The item handle assigned to the added item.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Adds an item with context to an MXAccess server and returns the item handle.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemDefinition">Definition or address of the item to add.</param>
|
|
/// <param name="itemContext">Context string for the item.</param>
|
|
/// <returns>The item handle assigned to the added item.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Removes an item from an MXAccess server.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Advises on item changes with plain subscription.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
public void Advise(
|
|
int serverHandle,
|
|
int itemHandle)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
mxAccessServer.Advise(serverHandle, itemHandle);
|
|
handleRegistry.RegisterAdviceHandle(
|
|
serverHandle,
|
|
itemHandle,
|
|
MxAccessAdviceKind.Plain);
|
|
}
|
|
|
|
/// <summary>Removes plain advice subscription from an item.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
public void UnAdvise(
|
|
int serverHandle,
|
|
int itemHandle)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
mxAccessServer.UnAdvise(serverHandle, itemHandle);
|
|
handleRegistry.RemoveAdviceHandles(serverHandle, itemHandle);
|
|
}
|
|
|
|
/// <summary>Advises on item changes with supervisory subscription.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
public void AdviseSupervisory(
|
|
int serverHandle,
|
|
int itemHandle)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
mxAccessServer.AdviseSupervisory(serverHandle, itemHandle);
|
|
handleRegistry.RegisterAdviceHandle(
|
|
serverHandle,
|
|
itemHandle,
|
|
MxAccessAdviceKind.Supervisory);
|
|
}
|
|
|
|
/// <summary>Suspends data acquisition for an advised item and returns the native MXAccess status.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <returns>The boxed native MXAccess status produced by the call.</returns>
|
|
public object Suspend(
|
|
int serverHandle,
|
|
int itemHandle)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
return mxAccessServer.Suspend(serverHandle, itemHandle);
|
|
}
|
|
|
|
/// <summary>Reactivates data acquisition for a suspended item and returns the native MXAccess status.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <returns>The boxed native MXAccess status produced by the call.</returns>
|
|
public object Activate(
|
|
int serverHandle,
|
|
int itemHandle)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
return mxAccessServer.Activate(serverHandle, itemHandle);
|
|
}
|
|
|
|
/// <summary>Authenticates an MXAccess user and returns its user id.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="verifyUser">MXAccess user name to authenticate.</param>
|
|
/// <param name="verifyUserPassword">Raw MXAccess credential; never logged.</param>
|
|
/// <returns>The MXAccess user id for the authenticated user.</returns>
|
|
public int AuthenticateUser(
|
|
int serverHandle,
|
|
string verifyUser,
|
|
string verifyUserPassword)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
return mxAccessServer.AuthenticateUser(serverHandle, verifyUser, verifyUserPassword);
|
|
}
|
|
|
|
/// <summary>Resolves an ArchestrA user GUID to an MXAccess user id.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="userIdGuid">ArchestrA user GUID to resolve.</param>
|
|
/// <returns>The MXAccess user id for the resolved user.</returns>
|
|
public int ArchestrAUserToId(
|
|
int serverHandle,
|
|
string userIdGuid)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
return mxAccessServer.ArchestrAUserToId(serverHandle, userIdGuid);
|
|
}
|
|
|
|
/// <summary>Adds a buffered item to an MXAccess server and returns the item handle.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemDefinition">Definition or address of the item to add.</param>
|
|
/// <param name="itemContext">Context string for the item.</param>
|
|
/// <returns>The item handle assigned to the added item.</returns>
|
|
public int AddBufferedItem(
|
|
int serverHandle,
|
|
string itemDefinition,
|
|
string itemContext)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
int itemHandle = mxAccessServer.AddBufferedItem(serverHandle, itemDefinition, itemContext);
|
|
handleRegistry.RegisterItemHandle(
|
|
serverHandle,
|
|
itemHandle,
|
|
itemDefinition,
|
|
itemContext,
|
|
hasItemContext: true);
|
|
|
|
return itemHandle;
|
|
}
|
|
|
|
/// <summary>Sets the buffered-update interval for an MXAccess server.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="updateIntervalMilliseconds">Buffered update interval in milliseconds.</param>
|
|
public void SetBufferedUpdateInterval(
|
|
int serverHandle,
|
|
int updateIntervalMilliseconds)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
mxAccessServer.SetBufferedUpdateInterval(serverHandle, updateIntervalMilliseconds);
|
|
}
|
|
|
|
/// <summary>Writes a value to an item.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="value">COM-marshalable value to write.</param>
|
|
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
|
|
public void Write(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
object? value,
|
|
int userId)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
mxAccessServer.Write(serverHandle, itemHandle, value, userId);
|
|
}
|
|
|
|
/// <summary>Writes a value with an explicit source timestamp to an item.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="value">COM-marshalable value to write.</param>
|
|
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
|
|
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
|
|
public void Write2(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
object? value,
|
|
object? timestamp,
|
|
int userId)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
mxAccessServer.Write2(serverHandle, itemHandle, value, timestamp, userId);
|
|
}
|
|
|
|
/// <summary>Performs a secured/verified write to an item.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
|
|
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
|
|
/// <param name="value">COM-marshalable value to write.</param>
|
|
public void WriteSecured(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int currentUserId,
|
|
int verifierUserId,
|
|
object? value)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
mxAccessServer.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value);
|
|
}
|
|
|
|
/// <summary>Performs a secured/verified write with an explicit source timestamp.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
|
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
|
|
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
|
|
/// <param name="value">COM-marshalable value to write.</param>
|
|
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
|
|
public void WriteSecured2(
|
|
int serverHandle,
|
|
int itemHandle,
|
|
int currentUserId,
|
|
int verifierUserId,
|
|
object? value,
|
|
object? timestamp)
|
|
{
|
|
ThrowIfDisposed();
|
|
|
|
mxAccessServer.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp);
|
|
}
|
|
|
|
/// <summary>Adds multiple items in bulk, returning success/failure results.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="tagAddresses">Enumerable of item definitions to add.</param>
|
|
/// <returns>The per-item success/failure results, in the order the tags were provided.</returns>
|
|
public IReadOnlyList<SubscribeResult> AddItemBulk(
|
|
int serverHandle,
|
|
IEnumerable<string> tagAddresses)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (tagAddresses is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(tagAddresses));
|
|
}
|
|
|
|
List<SubscribeResult> 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;
|
|
}
|
|
|
|
/// <summary>Advises on multiple items in bulk, returning success/failure results.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandles">Enumerable of item handles to advise on.</param>
|
|
/// <returns>The per-item success/failure results, in the order the handles were provided.</returns>
|
|
public IReadOnlyList<SubscribeResult> AdviseItemBulk(
|
|
int serverHandle,
|
|
IEnumerable<int> itemHandles)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (itemHandles is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(itemHandles));
|
|
}
|
|
|
|
List<SubscribeResult> 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;
|
|
}
|
|
|
|
/// <summary>Removes multiple items in bulk, returning success/failure results.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandles">Enumerable of item handles to remove.</param>
|
|
/// <returns>The per-item success/failure results, in the order the handles were provided.</returns>
|
|
public IReadOnlyList<SubscribeResult> RemoveItemBulk(
|
|
int serverHandle,
|
|
IEnumerable<int> itemHandles)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (itemHandles is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(itemHandles));
|
|
}
|
|
|
|
List<SubscribeResult> 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;
|
|
}
|
|
|
|
/// <summary>Removes advice subscriptions from multiple items in bulk, returning success/failure results.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandles">Enumerable of item handles to unadvise.</param>
|
|
/// <returns>The per-item success/failure results, in the order the handles were provided.</returns>
|
|
public IReadOnlyList<SubscribeResult> UnAdviseItemBulk(
|
|
int serverHandle,
|
|
IEnumerable<int> itemHandles)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (itemHandles is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(itemHandles));
|
|
}
|
|
|
|
List<SubscribeResult> 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;
|
|
}
|
|
|
|
/// <summary>Adds multiple items and subscribes to them in bulk, returning success/failure results.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="tagAddresses">Enumerable of item definitions to add and subscribe to.</param>
|
|
/// <returns>The per-item success/failure results, in the order the tags were provided.</returns>
|
|
public IReadOnlyList<SubscribeResult> SubscribeBulk(
|
|
int serverHandle,
|
|
IEnumerable<string> tagAddresses)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (tagAddresses is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(tagAddresses));
|
|
}
|
|
|
|
List<SubscribeResult> 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;
|
|
}
|
|
|
|
/// <summary>Unsubscribes from multiple items in bulk, returning success/failure results.</summary>
|
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
|
/// <param name="itemHandles">Enumerable of item handles to unsubscribe from.</param>
|
|
/// <returns>The per-item success/failure results, in the order the handles were provided.</returns>
|
|
public IReadOnlyList<SubscribeResult> UnsubscribeBulk(
|
|
int serverHandle,
|
|
IEnumerable<int> itemHandles)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (itemHandles is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(itemHandles));
|
|
}
|
|
|
|
List<SubscribeResult> results = new();
|
|
foreach (int itemHandle in itemHandles)
|
|
{
|
|
List<string> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bulk write — runs <see cref="Write"/> sequentially for each entry.
|
|
/// Each entry's <paramref name="convertValue"/> turns the protobuf
|
|
/// MxValue into a COM-marshalable variant. Per-item failures are
|
|
/// captured as <see cref="BulkWriteResult"/> entries with
|
|
/// <c>was_successful = false</c>; the loop never throws.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The MXAccess server handle.</param>
|
|
/// <param name="entries">The write entries to process.</param>
|
|
/// <param name="convertValue">Converts protobuf MxValue to COM-compatible variant.</param>
|
|
/// <returns>The per-entry write results, in the order the entries were provided.</returns>
|
|
public IReadOnlyList<BulkWriteResult> WriteBulk(
|
|
int serverHandle,
|
|
IReadOnlyList<WriteBulkEntry> entries,
|
|
Func<MxValue, object?> convertValue)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (entries is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(entries));
|
|
}
|
|
|
|
if (convertValue is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(convertValue));
|
|
}
|
|
|
|
List<BulkWriteResult> 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;
|
|
}
|
|
|
|
/// <summary>Bulk Write2 — sequential MXAccess <see cref="Write2"/> per entry.</summary>
|
|
/// <param name="serverHandle">The MXAccess server handle.</param>
|
|
/// <param name="entries">The write2 entries to process.</param>
|
|
/// <param name="convertValue">Converts protobuf MxValue to COM-compatible variant.</param>
|
|
/// <returns>The per-entry write results, in the order the entries were provided.</returns>
|
|
public IReadOnlyList<BulkWriteResult> Write2Bulk(
|
|
int serverHandle,
|
|
IReadOnlyList<Write2BulkEntry> entries,
|
|
Func<MxValue, object?> convertValue)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (entries is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(entries));
|
|
}
|
|
|
|
if (convertValue is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(convertValue));
|
|
}
|
|
|
|
List<BulkWriteResult> 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;
|
|
}
|
|
|
|
/// <summary>Bulk WriteSecured — sequential MXAccess <see cref="WriteSecured"/> per entry.</summary>
|
|
/// <param name="serverHandle">The MXAccess server handle.</param>
|
|
/// <param name="entries">The WriteSecured entries to process.</param>
|
|
/// <param name="convertValue">Converts protobuf MxValue to COM-compatible variant.</param>
|
|
/// <returns>The per-entry write results, in the order the entries were provided.</returns>
|
|
public IReadOnlyList<BulkWriteResult> WriteSecuredBulk(
|
|
int serverHandle,
|
|
IReadOnlyList<WriteSecuredBulkEntry> entries,
|
|
Func<MxValue, object?> convertValue)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (entries is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(entries));
|
|
}
|
|
|
|
if (convertValue is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(convertValue));
|
|
}
|
|
|
|
List<BulkWriteResult> 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;
|
|
}
|
|
|
|
/// <summary>Bulk WriteSecured2 — sequential MXAccess <see cref="WriteSecured2"/> per entry.</summary>
|
|
/// <param name="serverHandle">The MXAccess server handle.</param>
|
|
/// <param name="entries">The WriteSecured2 entries to process.</param>
|
|
/// <param name="convertValue">Converts protobuf MxValue to COM-compatible variant.</param>
|
|
/// <returns>The per-entry write results, in the order the entries were provided.</returns>
|
|
public IReadOnlyList<BulkWriteResult> WriteSecured2Bulk(
|
|
int serverHandle,
|
|
IReadOnlyList<WriteSecured2BulkEntry> entries,
|
|
Func<MxValue, object?> convertValue)
|
|
{
|
|
ThrowIfDisposed();
|
|
if (entries is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(entries));
|
|
}
|
|
|
|
if (convertValue is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(convertValue));
|
|
}
|
|
|
|
List<BulkWriteResult> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// <paramref name="timeout"/> bounds the wait per-tag in the snapshot
|
|
/// case; <paramref name="pumpStep"/> is invoked on every poll
|
|
/// iteration so the worker's STA can dispatch the incoming MXAccess
|
|
/// message that carries the value.
|
|
/// </summary>
|
|
/// <param name="serverHandle">The MXAccess server handle.</param>
|
|
/// <param name="tagAddresses">The tag addresses to read.</param>
|
|
/// <param name="timeout">The timeout per tag.</param>
|
|
/// <param name="pumpStep">Action invoked on each poll iteration.</param>
|
|
/// <returns>The per-tag read results, in the order the tags were provided.</returns>
|
|
public IReadOnlyList<BulkReadResult> ReadBulk(
|
|
int serverHandle,
|
|
IReadOnlyList<string> 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<BulkReadResult> 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,
|
|
};
|
|
}
|
|
|
|
/// <summary>Gracefully shuts down the session, cleaning up all handles.</summary>
|
|
/// <returns>The shutdown result, including any per-handle cleanup failures.</returns>
|
|
public MxAccessShutdownResult ShutdownGracefully()
|
|
{
|
|
if (disposed)
|
|
{
|
|
return new MxAccessShutdownResult(Array.Empty<MxAccessShutdownFailure>());
|
|
}
|
|
|
|
List<MxAccessShutdownFailure> failures = new();
|
|
|
|
CleanupAdviceHandles(failures);
|
|
CleanupItemHandles(failures);
|
|
CleanupServerHandles(failures);
|
|
DisposeCore(failures);
|
|
|
|
return new MxAccessShutdownResult(failures);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Dispose()
|
|
{
|
|
if (disposed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DisposeCore(failures: null);
|
|
}
|
|
|
|
private void CleanupAdviceHandles(ICollection<MxAccessShutdownFailure> failures)
|
|
{
|
|
HashSet<long> 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<MxAccessShutdownFailure> 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<MxAccessShutdownFailure> 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<MxAccessShutdownFailure>? 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));
|
|
}
|
|
}
|
|
}
|