207 lines
6.8 KiB
C#
207 lines
6.8 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Sta;
|
|
|
|
/// <summary>
|
|
/// Dedicated STA thread with a Win32 message pump that owns all <c>LMXProxyServer</c> COM
|
|
/// instances. Lifted from v1 <c>StaComThread</c> per CLAUDE.md "Reference Implementation".
|
|
/// Per <c>driver-stability.md</c> Galaxy deep dive §"STA thread + Win32 message pump":
|
|
/// work items dispatched via <c>PostThreadMessage(WM_APP)</c>; <c>WM_APP+1</c> requests a
|
|
/// graceful drain → <c>WM_QUIT</c>; supervisor escalates to <c>Environment.Exit(2)</c> if the
|
|
/// pump doesn't drain within the recycle grace window.
|
|
/// </summary>
|
|
public sealed class StaPump : IDisposable
|
|
{
|
|
private const uint WM_APP = 0x8000;
|
|
private const uint WM_DRAIN_AND_QUIT = WM_APP + 1;
|
|
private const uint PM_NOREMOVE = 0x0000;
|
|
|
|
private readonly Thread _thread;
|
|
private readonly ConcurrentQueue<WorkItem> _workItems = new();
|
|
private readonly TaskCompletionSource<bool> _started = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
|
|
private volatile uint _nativeThreadId;
|
|
private volatile bool _pumpExited;
|
|
private volatile bool _disposed;
|
|
|
|
public int ThreadId => _thread.ManagedThreadId;
|
|
public DateTime LastDispatchedUtc { get; private set; } = DateTime.MinValue;
|
|
public int QueueDepth => _workItems.Count;
|
|
public bool IsRunning => _nativeThreadId != 0 && !_disposed && !_pumpExited;
|
|
|
|
public StaPump(string name = "Galaxy.Sta")
|
|
{
|
|
_thread = new Thread(PumpLoop) { Name = name, IsBackground = true };
|
|
_thread.SetApartmentState(ApartmentState.STA);
|
|
_thread.Start();
|
|
}
|
|
|
|
public Task WaitForStartedAsync() => _started.Task;
|
|
|
|
/// <summary>Posts a work item; resolves once it's executed on the STA thread.</summary>
|
|
public Task<T> InvokeAsync<T>(Func<T> work)
|
|
{
|
|
if (_disposed) throw new ObjectDisposedException(nameof(StaPump));
|
|
if (_pumpExited) throw new InvalidOperationException("STA pump has exited");
|
|
|
|
var tcs = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
_workItems.Enqueue(new WorkItem(
|
|
() =>
|
|
{
|
|
try { tcs.TrySetResult(work()); }
|
|
catch (Exception ex) { tcs.TrySetException(ex); }
|
|
},
|
|
ex => tcs.TrySetException(ex)));
|
|
|
|
if (!PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero))
|
|
{
|
|
_pumpExited = true;
|
|
DrainAndFaultQueue();
|
|
}
|
|
|
|
return tcs.Task;
|
|
}
|
|
|
|
public Task InvokeAsync(Action work) => InvokeAsync(() => { work(); return 0; });
|
|
|
|
/// <summary>
|
|
/// Health probe — returns true if a no-op work item round-trips within
|
|
/// <paramref name="timeout"/>. Used by the supervisor; timeout means the pump is wedged
|
|
/// and a recycle is warranted (Task B.2 acceptance).
|
|
/// </summary>
|
|
public async Task<bool> IsResponsiveAsync(TimeSpan timeout)
|
|
{
|
|
if (!IsRunning) return false;
|
|
var task = InvokeAsync(() => { });
|
|
var completed = await Task.WhenAny(task, Task.Delay(timeout)).ConfigureAwait(false);
|
|
return completed == task;
|
|
}
|
|
|
|
private void PumpLoop()
|
|
{
|
|
try
|
|
{
|
|
_nativeThreadId = GetCurrentThreadId();
|
|
|
|
// Force the system to create the thread message queue before we signal Started.
|
|
// PeekMessage(PM_NOREMOVE) on an empty queue is the documented way to do this.
|
|
PeekMessage(out _, IntPtr.Zero, 0, 0, PM_NOREMOVE);
|
|
|
|
_started.TrySetResult(true);
|
|
|
|
// GetMessage returns 0 on WM_QUIT, -1 on error, otherwise a positive value.
|
|
while (GetMessage(out var msg, IntPtr.Zero, 0, 0) > 0)
|
|
{
|
|
if (msg.message == WM_APP)
|
|
{
|
|
DrainQueue();
|
|
}
|
|
else if (msg.message == WM_DRAIN_AND_QUIT)
|
|
{
|
|
DrainQueue();
|
|
PostQuitMessage(0);
|
|
}
|
|
else
|
|
{
|
|
// Pass through any window/dialog messages the COM proxy may inject.
|
|
TranslateMessage(ref msg);
|
|
DispatchMessage(ref msg);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_started.TrySetException(ex);
|
|
}
|
|
finally
|
|
{
|
|
_pumpExited = true;
|
|
DrainAndFaultQueue();
|
|
}
|
|
}
|
|
|
|
private void DrainQueue()
|
|
{
|
|
while (_workItems.TryDequeue(out var item))
|
|
{
|
|
item.Execute();
|
|
LastDispatchedUtc = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
private void DrainAndFaultQueue()
|
|
{
|
|
var ex = new InvalidOperationException("STA pump has exited");
|
|
while (_workItems.TryDequeue(out var item))
|
|
{
|
|
try { item.Fault(ex); }
|
|
catch { /* faulting a TCS shouldn't throw, but be defensive */ }
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (_disposed) return;
|
|
_disposed = true;
|
|
|
|
try
|
|
{
|
|
if (_nativeThreadId != 0 && !_pumpExited)
|
|
PostThreadMessage(_nativeThreadId, WM_DRAIN_AND_QUIT, IntPtr.Zero, IntPtr.Zero);
|
|
_thread.Join(TimeSpan.FromSeconds(5));
|
|
}
|
|
catch { /* swallow — best effort */ }
|
|
|
|
DrainAndFaultQueue();
|
|
}
|
|
|
|
private sealed record WorkItem(Action Execute, Action<Exception> Fault);
|
|
|
|
#region Win32 P/Invoke
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct MSG
|
|
{
|
|
public IntPtr hwnd;
|
|
public uint message;
|
|
public IntPtr wParam;
|
|
public IntPtr lParam;
|
|
public uint time;
|
|
public POINT pt;
|
|
}
|
|
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
private struct POINT { public int x; public int y; }
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern int GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
|
|
|
[DllImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool TranslateMessage(ref MSG lpMsg);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern IntPtr DispatchMessage(ref MSG lpMsg);
|
|
|
|
[DllImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool PostThreadMessage(uint idThread, uint Msg, IntPtr wParam, IntPtr lParam);
|
|
|
|
[DllImport("user32.dll")]
|
|
private static extern void PostQuitMessage(int nExitCode);
|
|
|
|
[DllImport("user32.dll")]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool PeekMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax,
|
|
uint wRemoveMsg);
|
|
|
|
[DllImport("kernel32.dll")]
|
|
private static extern uint GetCurrentThreadId();
|
|
|
|
#endregion
|
|
}
|