68eb9adae7
Under eOSUserBased galaxy security mode the LMXProxyServer raises
ArgumentException("Value does not fall within the expected range")
for bad credentials instead of silently returning 0 like permissive
(eNone) galaxies do. Both shapes mean "auth failed"; MxSession.Authenticate
now normalizes them into a 0 return so WriteCommand reports a clean
"authentication-failed" envelope and exits 1 — instead of crashing
with a stack trace.
Verified live against the ZB galaxy in eOSUserBased mode:
bad password -> ok=false, error="authentication-failed", exit 1
good password -> ok=true, auth_user_id=1, exit 0
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
196 lines
7.6 KiB
C#
196 lines
7.6 KiB
C#
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<int, MxItem> _itemsByHandle = new Dictionary<int, MxItem>();
|
|
private readonly ConcurrentQueue<MxUpdate> _updates = new ConcurrentQueue<MxUpdate>();
|
|
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 → `<domain-or-host>\<username>`
|
|
/// - galaxyAuthenticationMode → `<username>` only
|
|
/// - mixed/AAD → `<UPN>` (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<MxUpdate> 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<MxUpdate> 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 { }
|
|
}
|
|
}
|
|
}
|