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 bool disposed;
private MxAccessSession(
object mxAccessComObject,
IMxAccessServer mxAccessServer,
IMxAccessEventSink eventSink,
MxAccessHandleRegistry handleRegistry,
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));
CreationThreadId = creationThreadId;
}
/// The thread ID where this session was created.
public int CreationThreadId { get; }
/// The registry for tracking opened handles.
public MxAccessHandleRegistry HandleRegistry => handleRegistry;
/// 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);
return new MxAccessSession(
mxAccessComObject,
new MxAccessComServer(mxAccessComObject),
eventSink,
new MxAccessHandleRegistry(),
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);
}
/// 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;
}
/// 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));
}
}
}