fix(lmxproxy): use raw Win32 message pump instead of WinForms Application.Run

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-03-22 18:03:34 -04:00
parent b6408726bc
commit a59d4ad76c
2 changed files with 101 additions and 18 deletions

View File

@@ -1,24 +1,29 @@
using System; using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Forms;
using Serilog; using Serilog;
namespace ZB.MOM.WW.LmxProxy.Host.MxAccess namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
{ {
/// <summary> /// <summary>
/// Dedicated STA thread with a Windows message pump for COM interop. /// Dedicated STA thread with a raw Win32 message pump for COM interop.
/// All MxAccess COM objects must be created and called on this thread /// All MxAccess COM objects must be created and called on this thread
/// so that COM callbacks (OnDataChange, OnWriteComplete) are delivered /// so that COM callbacks (OnDataChange, OnWriteComplete) are delivered
/// via the message loop. /// via the message loop.
/// </summary> /// </summary>
public sealed class StaComThread : IDisposable public sealed class StaComThread : IDisposable
{ {
private const uint WM_APP = 0x8000;
private const uint PM_NOREMOVE = 0x0000;
private static readonly ILogger Log = Serilog.Log.ForContext<StaComThread>(); private static readonly ILogger Log = Serilog.Log.ForContext<StaComThread>();
private readonly Thread _thread; private readonly Thread _thread;
private readonly TaskCompletionSource<bool> _ready = new TaskCompletionSource<bool>(); private readonly TaskCompletionSource<bool> _ready = new TaskCompletionSource<bool>();
private SynchronizationContext _syncContext = null!; private readonly ConcurrentQueue<Action> _workItems = new ConcurrentQueue<Action>();
private volatile uint _nativeThreadId;
private bool _disposed; private bool _disposed;
public StaComThread() public StaComThread()
@@ -50,7 +55,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread)); if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
var tcs = new TaskCompletionSource<bool>(); var tcs = new TaskCompletionSource<bool>();
_syncContext.Post(_ => _workItems.Enqueue(() =>
{ {
try try
{ {
@@ -61,7 +66,8 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
{ {
tcs.TrySetException(ex); tcs.TrySetException(ex);
} }
}, null); });
PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero);
return tcs.Task; return tcs.Task;
} }
@@ -74,7 +80,7 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
if (_disposed) throw new ObjectDisposedException(nameof(StaComThread)); if (_disposed) throw new ObjectDisposedException(nameof(StaComThread));
var tcs = new TaskCompletionSource<T>(); var tcs = new TaskCompletionSource<T>();
_syncContext.Post(_ => _workItems.Enqueue(() =>
{ {
try try
{ {
@@ -84,7 +90,8 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
{ {
tcs.TrySetException(ex); tcs.TrySetException(ex);
} }
}, null); });
PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero);
return tcs.Task; return tcs.Task;
} }
@@ -95,8 +102,8 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
try try
{ {
// Post Application.ExitThread to break out of the message loop if (_nativeThreadId != 0)
_syncContext?.Post(_ => Application.ExitThread(), null); PostThreadMessage(_nativeThreadId, WM_APP + 1, IntPtr.Zero, IntPtr.Zero);
_thread.Join(TimeSpan.FromSeconds(5)); _thread.Join(TimeSpan.FromSeconds(5));
} }
catch (Exception ex) catch (Exception ex)
@@ -111,17 +118,33 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
{ {
try try
{ {
// Install a WindowsFormsSynchronizationContext so that _nativeThreadId = GetCurrentThreadId();
// Post/Send dispatches onto this thread's message loop
Application.OleRequired(); // Force message queue creation by peeking
var ctx = new WindowsFormsSynchronizationContext(); MSG msg;
SynchronizationContext.SetSynchronizationContext(ctx); PeekMessage(out msg, IntPtr.Zero, 0, 0, PM_NOREMOVE);
_syncContext = ctx;
_ready.TrySetResult(true); _ready.TrySetResult(true);
// Run the message loop — this blocks until Application.ExitThread() // Run the message loop — blocks until WM_QUIT
Application.Run(); while (GetMessage(out msg, IntPtr.Zero, 0, 0) > 0)
{
if (msg.message == WM_APP)
{
DrainQueue();
}
else if (msg.message == WM_APP + 1)
{
// Shutdown signal — drain remaining work then quit
DrainQueue();
PostQuitMessage(0);
}
else
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
}
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -129,5 +152,66 @@ namespace ZB.MOM.WW.LmxProxy.Host.MxAccess
_ready.TrySetException(ex); _ready.TrySetException(ex);
} }
} }
private void DrainQueue()
{
while (_workItems.TryDequeue(out var workItem))
{
try
{
workItem();
}
catch (Exception ex)
{
Log.Error(ex, "Unhandled exception in STA work item");
}
}
}
#region Win32 PInvoke
[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
} }
} }

View File

@@ -42,7 +42,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="System.Windows.Forms" />
<Reference Include="ArchestrA.MXAccess"> <Reference Include="ArchestrA.MXAccess">
<HintPath>..\..\lib\ArchestrA.MXAccess.dll</HintPath> <HintPath>..\..\lib\ArchestrA.MXAccess.dll</HintPath>
<Private>true</Private> <Private>true</Private>