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; } public int CreationThreadId { get; } public MxAccessHandleRegistry HandleRegistry => handleRegistry; public WorkerReady CreateWorkerReady(int workerProcessId) { return new WorkerReady { WorkerProcessId = workerProcessId, MxaccessProgid = MxAccessInteropInfo.ProgId, MxaccessClsid = MxAccessInteropInfo.Clsid, ReadyTimestamp = Timestamp.FromDateTime(DateTime.UtcNow), }; } 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) { eventSink.Detach(); if (mxAccessComObject is not null && Marshal.IsComObject(mxAccessComObject)) { Marshal.FinalReleaseComObject(mxAccessComObject); } throw MxAccessCreationException.From(exception); } } public int Register(string clientName) { ThrowIfDisposed(); int serverHandle = mxAccessServer.Register(clientName); handleRegistry.RegisterServerHandle(serverHandle, clientName); return serverHandle; } public void Unregister(int serverHandle) { ThrowIfDisposed(); mxAccessServer.Unregister(serverHandle); handleRegistry.UnregisterServerHandle(serverHandle); } 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; } 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; } public void RemoveItem( int serverHandle, int itemHandle) { ThrowIfDisposed(); mxAccessServer.RemoveItem(serverHandle, itemHandle); handleRegistry.RemoveItemHandle(serverHandle, itemHandle); } public void Advise( int serverHandle, int itemHandle) { ThrowIfDisposed(); mxAccessServer.Advise(serverHandle, itemHandle); handleRegistry.RegisterAdviceHandle( serverHandle, itemHandle, MxAccessAdviceKind.Plain); } public void UnAdvise( int serverHandle, int itemHandle) { ThrowIfDisposed(); mxAccessServer.UnAdvise(serverHandle, itemHandle); handleRegistry.RemoveAdviceHandles(serverHandle, itemHandle); } public void AdviseSupervisory( int serverHandle, int itemHandle) { ThrowIfDisposed(); mxAccessServer.AdviseSupervisory(serverHandle, itemHandle); handleRegistry.RegisterAdviceHandle( serverHandle, itemHandle, MxAccessAdviceKind.Supervisory); } public MxAccessShutdownResult ShutdownGracefully() { if (disposed) { return new MxAccessShutdownResult(Array.Empty()); } List failures = new(); CleanupAdviceHandles(failures); CleanupItemHandles(failures); CleanupServerHandles(failures); DisposeCore(failures); return new MxAccessShutdownResult(failures); } 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 void DisposeCore(ICollection? failures) { try { eventSink.Detach(); } catch (Exception exception) when (failures is not null) { 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; } private void ThrowIfDisposed() { if (disposed) { throw new ObjectDisposedException(nameof(MxAccessSession)); } } }