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)); } } }