169 lines
7.5 KiB
C#
169 lines
7.5 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Threading;
|
|
using Microsoft.Win32.SafeHandles;
|
|
|
|
namespace ZB.MOM.WW.MxGateway.Worker.Sta;
|
|
|
|
/// <summary>
|
|
/// Message-aware blocking wait shared by STA code that must sit idle without
|
|
/// starving the Windows message queue. <see cref="StaMessagePump"/> 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.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Deliberately separate from <see cref="StaMessagePump"/> 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
|
|
/// <see cref="StaNativeMethods"/>.
|
|
/// </remarks>
|
|
internal static class StaWaitHelper
|
|
{
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal const int MaxFallbackTickMilliseconds = 50;
|
|
|
|
private static int waitFailureReported;
|
|
|
|
/// <summary>
|
|
/// Blocks the calling thread until <paramref name="signal"/> is set, a
|
|
/// Windows message is available to pump, or the timeout elapses —
|
|
/// whichever comes first. Unlike <see cref="Thread.Sleep(int)"/> this
|
|
/// never blinds the thread to inbound COM event messages: the wait
|
|
/// returns the instant one arrives, so the caller can dispatch it.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <em>Caller contract: the pump step must drain the message queue
|
|
/// to empty on every call.</em> The wait uses
|
|
/// <see cref="StaNativeMethods.MwmoInputAvailable"/>, which reports
|
|
/// input that is merely <em>present</em> — including input an earlier
|
|
/// <c>PeekMessage</c> 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 <c>PeekMessage(..., PM_REMOVE)</c> in a loop
|
|
/// until it returns false, exactly as
|
|
/// <see cref="StaMessagePump.PumpPendingMessages"/> does.
|
|
/// </para>
|
|
/// <para>
|
|
/// The wait does not <c>DangerousAddRef</c> the handle. That is
|
|
/// sound only because the callers never dispose their wait handles —
|
|
/// see the coupled invariant documented on the caches' signal fields.
|
|
/// </para>
|
|
/// </remarks>
|
|
/// <param name="signal">Wait handle set by whoever produces the awaited result.</param>
|
|
/// <param name="timeoutMilliseconds">Maximum wait; values at or below zero return immediately.</param>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="MaxFallbackTickMilliseconds"/>.
|
|
/// </summary>
|
|
/// <param name="remaining">Time left before the caller's absolute deadline.</param>
|
|
/// <param name="pollIntervalMilliseconds">The caller's requested pump cadence.</param>
|
|
/// <returns>Milliseconds to wait; zero or less when the deadline has passed.</returns>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Traces the first wait failure of the process and stays silent after
|
|
/// that.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The degrade path is silent by design — a failed wait still honors the
|
|
/// caller's deadline — but a <em>permanently</em> 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.
|
|
/// </remarks>
|
|
/// <param name="lastError">The Win32 error captured immediately after the failed wait.</param>
|
|
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);
|
|
}
|
|
}
|