feat(lmxproxy): phase 2 — host core (MxAccessClient, SessionManager, SubscriptionManager)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
using Serilog;
|
||||
|
||||
namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
|
||||
{
|
||||
/// <summary>
|
||||
/// Dedicated STA thread with a message pump for COM interop.
|
||||
/// All COM operations are dispatched to this thread via a BlockingCollection.
|
||||
/// </summary>
|
||||
public sealed class StaDispatchThread : IDisposable
|
||||
{
|
||||
private static readonly ILogger Log = Serilog.Log.ForContext<StaDispatchThread>();
|
||||
|
||||
private readonly BlockingCollection<Action> _workQueue = new BlockingCollection<Action>();
|
||||
private readonly Thread _staThread;
|
||||
private volatile bool _disposed;
|
||||
|
||||
public StaDispatchThread(string threadName = "MxAccess-STA")
|
||||
{
|
||||
_staThread = new Thread(StaThreadLoop)
|
||||
{
|
||||
Name = threadName,
|
||||
IsBackground = true
|
||||
};
|
||||
_staThread.SetApartmentState(ApartmentState.STA);
|
||||
_staThread.Start();
|
||||
Log.Information("STA dispatch thread '{ThreadName}' started", threadName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an action to the STA thread and returns a Task that completes
|
||||
/// when the action finishes.
|
||||
/// </summary>
|
||||
public Task DispatchAsync(Action action)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(StaDispatchThread));
|
||||
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_workQueue.Add(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
tcs.TrySetResult(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
});
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches a function to the STA thread and returns its result.
|
||||
/// </summary>
|
||||
public Task<T> DispatchAsync<T>(Func<T> func)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(StaDispatchThread));
|
||||
|
||||
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
_workQueue.Add(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = func();
|
||||
tcs.TrySetResult(result);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
tcs.TrySetException(ex);
|
||||
}
|
||||
});
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private void StaThreadLoop()
|
||||
{
|
||||
Log.Debug("STA thread loop started");
|
||||
|
||||
// Process the work queue. GetConsumingEnumerable blocks until
|
||||
// items are available or the collection is marked complete.
|
||||
foreach (var action in _workQueue.GetConsumingEnumerable())
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Should not happen — actions set TCS exceptions internally.
|
||||
Log.Error(ex, "Unhandled exception on STA thread");
|
||||
}
|
||||
|
||||
// Pump COM messages between work items
|
||||
Application.DoEvents();
|
||||
}
|
||||
|
||||
Log.Debug("STA thread loop exited");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
|
||||
_workQueue.CompleteAdding();
|
||||
|
||||
// Wait for the STA thread to drain and exit
|
||||
if (_staThread.IsAlive && !_staThread.Join(TimeSpan.FromSeconds(10)))
|
||||
{
|
||||
Log.Warning("STA thread did not exit within 10 seconds");
|
||||
}
|
||||
|
||||
_workQueue.Dispose();
|
||||
Log.Information("STA dispatch thread disposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user