using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using System.Windows.Forms; using ArchestrA.MxAccess; namespace MxAccess.Cli.Mx { /// Wraps the MxAccess COM proxy with caller-friendly primitives. /// /// MxAccess events are dispatched as COM messages on the apartment that /// called Register. Pure Monitor / AutoResetEvent waits do *not* drain /// those messages reliably even on STA, so MxSession exposes a /// `WaitForUpdate` that polls Application.DoEvents() instead. This is the /// pattern Object Viewer / WindowViewer / aaTagViewer all use under the /// hood — see docs/api-notes.md "Threading model". public sealed class MxSession : IDisposable { private readonly LMXProxyServerClass _proxy; private readonly int _hServer; private readonly object _lock = new object(); private readonly Dictionary _itemsByHandle = new Dictionary(); private readonly ConcurrentQueue _updates = new ConcurrentQueue(); private bool _disposed; public MxSession(string clientName) { _proxy = new LMXProxyServerClass(); _proxy.OnDataChange += OnDataChange; _proxy.OnWriteComplete += OnWriteComplete; _proxy.OperationComplete += OnOperationComplete; _hServer = _proxy.Register(string.IsNullOrWhiteSpace(clientName) ? "mxa" : clientName); } public int ServerHandle => _hServer; public MxItem AddItem(string itemRef) { if (string.IsNullOrWhiteSpace(itemRef)) throw new ArgumentException("Item reference must be non-empty.", nameof(itemRef)); var hItem = _proxy.AddItem(_hServer, itemRef); var item = new MxItem(this, _proxy, _hServer, hItem, itemRef); lock (_lock) _itemsByHandle[hItem] = item; return item; } /// Authenticate a user against the galaxy and return the user id /// to pass to subsequent Write/WriteSecured calls. Returns 0 on /// failure (bad credentials, unknown user, galaxy auth disabled). /// /// `verifyUser` is the credential string the proxy expects for its /// configured galaxy authentication mode: /// - osAuthenticationMode → `\` /// - galaxyAuthenticationMode → `` only /// - mixed/AAD → `` (e.g. `user@example.com`) public int Authenticate(string verifyUser, string password) { if (string.IsNullOrEmpty(verifyUser)) throw new ArgumentException("verifyUser must be non-empty.", nameof(verifyUser)); // Some galaxy configurations (e.g. eOSUserBased) cause the proxy to // throw `ArgumentException: Value does not fall within the expected // range` for bad credentials instead of returning 0 like the // permissive (eNone) configuration does. Both shapes mean "auth // failed"; the caller distinguishes via a non-zero return value. try { return _proxy.AuthenticateUser(_hServer, verifyUser, password ?? string.Empty); } catch (ArgumentException) { return 0; } } /// Pump COM messages while watching for an update that matches the predicate. /// Returns true when one is captured, false on timeout. public bool WaitForUpdate(Predicate match, TimeSpan timeout, out MxUpdate captured) { captured = null; var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { Application.DoEvents(); while (_updates.TryDequeue(out var u)) { if (match(u)) { captured = u; return true; } } Thread.Sleep(20); } // One last drain after the deadline so we don't miss an event that arrived // between the final Sleep and the loop exit. Application.DoEvents(); while (_updates.TryDequeue(out var u)) { if (match(u)) { captured = u; return true; } } return false; } /// Drain all pending updates without blocking. Caller must call PumpOnce() /// in their own loop to keep the COM message queue moving. public IEnumerable DrainUpdates() { while (_updates.TryDequeue(out var u)) yield return u; } /// Pump COM messages once. Used by streaming subscribers between drains. public void PumpOnce(TimeSpan slice) { Application.DoEvents(); if (slice > TimeSpan.Zero) Thread.Sleep(slice); } internal void RemoveItem(int hItem) { lock (_lock) _itemsByHandle.Remove(hItem); } // ---- Event plumbing ---- private void OnDataChange(int hServer, int hItem, object value, int quality, object timestamp, ref MXSTATUS_PROXY[] vars) { string itemRef; lock (_lock) itemRef = _itemsByHandle.TryGetValue(hItem, out var it) ? it.Reference : null; _updates.Enqueue(new MxUpdate { Kind = MxUpdateKind.DataChange, ItemHandle = hItem, ItemReference = itemRef, Value = value, Quality = quality, Timestamp = TryFiletimeToDateTime(timestamp), Statuses = MxStatusInfo.From(vars), }); } private void OnWriteComplete(int hServer, int hItem, ref MXSTATUS_PROXY[] vars) { string itemRef; lock (_lock) itemRef = _itemsByHandle.TryGetValue(hItem, out var it) ? it.Reference : null; _updates.Enqueue(new MxUpdate { Kind = MxUpdateKind.WriteComplete, ItemHandle = hItem, ItemReference = itemRef, Statuses = MxStatusInfo.From(vars), }); } private void OnOperationComplete(int hServer, int hItem, ref MXSTATUS_PROXY[] vars) { string itemRef; lock (_lock) itemRef = _itemsByHandle.TryGetValue(hItem, out var it) ? it.Reference : null; _updates.Enqueue(new MxUpdate { Kind = MxUpdateKind.OperationComplete, ItemHandle = hItem, ItemReference = itemRef, Statuses = MxStatusInfo.From(vars), }); } private static DateTime? TryFiletimeToDateTime(object ft) { if (ft == null) return null; try { var asLong = Convert.ToInt64(ft); return DateTime.FromFileTimeUtc(asLong).ToLocalTime(); } catch { return null; } } public void Dispose() { if (_disposed) return; _disposed = true; MxItem[] items; lock (_lock) { items = new MxItem[_itemsByHandle.Count]; _itemsByHandle.Values.CopyTo(items, 0); _itemsByHandle.Clear(); } foreach (var it in items) { try { it.Dispose(); } catch { } } try { _proxy.Unregister(_hServer); } catch { } } } }