using System; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Serilog; namespace ZB.MOM.WW.GRAccess.Cli.Session { /// /// Dedicated STA thread with a raw Win32 message pump for COM interop. /// All GRAccess COM objects must be created and called on this thread. /// 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(); private readonly Thread _thread; private readonly TaskCompletionSource _ready = new TaskCompletionSource(); private readonly ConcurrentQueue _workItems = new ConcurrentQueue(); private volatile uint _nativeThreadId; private bool _disposed; public StaComThread() { _thread = new Thread(ThreadEntry) { Name = "GRAccess-STA", IsBackground = true }; _thread.SetApartmentState(ApartmentState.STA); } /// /// Starts the STA thread and waits until the message pump is running. /// public void Start() { _thread.Start(); _ready.Task.GetAwaiter().GetResult(); Log.Information("STA COM thread started (ThreadId={ThreadId})", _thread.ManagedThreadId); } /// /// Marshals a synchronous action onto the STA thread and returns a Task /// that completes when the action finishes. /// public Task RunAsync(Action action) { if (_disposed) throw new ObjectDisposedException(nameof(StaComThread)); var tcs = new TaskCompletionSource(); _workItems.Enqueue(() => { try { action(); tcs.TrySetResult(true); } catch (Exception ex) { tcs.TrySetException(ex); } }); PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero); return tcs.Task; } /// /// Marshals a synchronous function onto the STA thread and returns /// a Task<T> with the result. /// public Task RunAsync(Func func) { if (_disposed) throw new ObjectDisposedException(nameof(StaComThread)); var tcs = new TaskCompletionSource(); _workItems.Enqueue(() => { try { tcs.TrySetResult(func()); } catch (Exception ex) { tcs.TrySetException(ex); } }); PostThreadMessage(_nativeThreadId, WM_APP, IntPtr.Zero, IntPtr.Zero); return tcs.Task; } public void Dispose() { if (_disposed) return; _disposed = true; try { if (_nativeThreadId != 0) PostThreadMessage(_nativeThreadId, WM_APP + 1, IntPtr.Zero, IntPtr.Zero); _thread.Join(TimeSpan.FromSeconds(5)); } catch (Exception ex) { Log.Warning(ex, "Error shutting down STA COM thread"); } Log.Information("STA COM thread stopped"); } private void ThreadEntry() { try { _nativeThreadId = GetCurrentThreadId(); // Force message queue creation by peeking MSG msg; PeekMessage(out msg, IntPtr.Zero, 0, 0, PM_NOREMOVE); _ready.TrySetResult(true); // Run the message loop — blocks until WM_QUIT 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) { Log.Error(ex, "STA COM thread crashed"); _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 } }