using System; using System.Diagnostics; using System.Runtime.InteropServices; using System.Threading; using Microsoft.Win32.SafeHandles; namespace ZB.MOM.WW.MxGateway.Worker.Sta; /// /// Message-aware blocking wait shared by STA code that must sit idle without /// starving the Windows message queue. owns the /// runtime's outer idle wait; this helper serves the inner waits performed /// by commands that already hold the STA — the write-completion wait and the /// ReadBulk first-value wait — which need the same "wake on a signal or on /// an inbound message" behavior without owning a pump instance. /// /// /// Deliberately separate from rather than a new /// method on it: the two caches that use this are plain per-session objects /// with no pump dependency (they take the pump step as an injected delegate /// so tests can drive them off the STA). The wait itself is not duplicated — /// both this and the pump call the single declaration in /// . /// internal static class StaWaitHelper { /// /// Ceiling on the fallback tick — the longest a wait may block when /// nothing wakes it. A caller's poll interval is clamped to this, so a /// caller's pump step keeps running at least this often no matter what /// interval it asked for, even on a thread that never receives a message /// and whose signal is never set. /// internal const int MaxFallbackTickMilliseconds = 50; private static int waitFailureReported; /// /// Blocks the calling thread until is set, a /// Windows message is available to pump, or the timeout elapses — /// whichever comes first. Unlike this /// never blinds the thread to inbound COM event messages: the wait /// returns the instant one arrives, so the caller can dispatch it. /// /// /// /// Caller contract: the pump step must drain the message queue /// to empty on every call. The wait uses /// , which reports /// input that is merely present — including input an earlier /// PeekMessage has already seen but not removed. A caller /// whose pump step drains only part of the queue, or does nothing at /// all, therefore leaves a message that satisfies the wake condition /// forever: every wait returns immediately, and the caller's poll /// loop degenerates into a busy loop that spins the thread at 100% /// until its deadline. That is a CPU fault, not a correctness one — /// the loop still honors its deadline and return shape — but it must /// not ship. Drain with PeekMessage(..., PM_REMOVE) in a loop /// until it returns false, exactly as /// does. /// /// /// The wait does not DangerousAddRef the handle. That is /// sound only because the callers never dispose their wait handles — /// see the coupled invariant documented on the caches' signal fields. /// /// /// Wait handle set by whoever produces the awaited result. /// Maximum wait; values at or below zero return immediately. internal static void WaitForSignalOrMessages( WaitHandle signal, int timeoutMilliseconds) { if (signal is null) { throw new ArgumentNullException(nameof(signal)); } if (timeoutMilliseconds <= 0) { return; } SafeWaitHandle safeHandle = signal.SafeWaitHandle; IntPtr[] handles = [safeHandle.DangerousGetHandle()]; uint result = StaNativeMethods.MsgWaitForMultipleObjectsEx( (uint)handles.Length, handles, (uint)timeoutMilliseconds, StaNativeMethods.QsAllInput, StaNativeMethods.MwmoInputAvailable); // Read before anything else can run another P/Invoke and overwrite the // thread's last-error slot. int lastError = result == StaNativeMethods.MsgWaitFailed ? Marshal.GetLastWin32Error() : 0; // The handle is a field of a live caller object, so it cannot be // collected mid-call; KeepAlive states that dependency explicitly // rather than relying on it. GC.KeepAlive(signal); if (result == StaNativeMethods.MsgWaitFailed) { ReportWaitFailureOnce(lastError); // Degrade to the pre-existing blind sleep instead of throwing or // hot-spinning: the caller is a bounded, deadline-driven poll loop, // and a failed wait must not turn a timed-out write into a fault. Thread.Sleep(timeoutMilliseconds); } } /// /// Computes the wait slice for one iteration of a deadline-driven poll /// loop: never past the deadline, never longer than the caller's poll /// interval, and never longer than . /// /// Time left before the caller's absolute deadline. /// The caller's requested pump cadence. /// Milliseconds to wait; zero or less when the deadline has passed. internal static int ClampWaitMilliseconds( TimeSpan remaining, int pollIntervalMilliseconds) { int tick = pollIntervalMilliseconds < 1 ? 1 : Math.Min(pollIntervalMilliseconds, MaxFallbackTickMilliseconds); double remainingMilliseconds = remaining.TotalMilliseconds; if (remainingMilliseconds <= 0d) { return 0; } return remainingMilliseconds < tick ? (int)Math.Ceiling(remainingMilliseconds) : tick; } /// /// Traces the first wait failure of the process and stays silent after /// that. /// /// /// The degrade path is silent by design — a failed wait still honors the /// caller's deadline — but a permanently failing wait means every /// completion wait has quietly reverted to the sleep-polled behavior this /// class exists to replace, and nothing else would ever say so. One trace /// makes that discoverable; latching it keeps a repeating failure from /// flooding the trace listener from inside a poll loop. /// /// The Win32 error captured immediately after the failed wait. private static void ReportWaitFailureOnce(int lastError) { if (Interlocked.Exchange(ref waitFailureReported, 1) != 0) { return; } Trace.TraceWarning( "MsgWaitForMultipleObjectsEx failed in the worker STA completion wait (Win32 error {0}). " + "The wait has degraded to blind sleeping: deadlines are still honored, but inbound " + "MXAccess COM events will only dispatch at poll-tick granularity. Reported once per process.", lastError); } }