Skip to content

Bug: uithread-init-contract-residuals-784-787-788 #809

Description

@drmoisan
  • Work Mode: full-bug

Summary

Consolidates three findings on one file, UtilitiesCS/Threading/UiThread.cs, that were filed separately as #784, #787, and #788 after the #781 and #782 reviews. (1) Init() accepts a non-STA caller and installs that worker's non-pumping dispatcher and context into set-once process-global state (#787). (2) Init() consumes its single-shot latch before Initialize() runs, so a failed first attempt can never be retried, and the naive re-arm was measured to regress in #782 (#788). (3) SynchronizationContextAwaiter.IsCompleted compares contexts by reference, so any context captured inside a WPF dispatcher operation always posts instead of continuing inline on the UI thread (#784). All three touch the same initialization and awaiter code and should ship as one change with one test suite.

Environment

  • OS/version: Windows 11 Pro 10.0.26200
  • Runtime: .NET Framework 4.8 VSTO add-in hosted by Outlook desktop; main at 04a54e68
  • Command/flags used: vstest.console.exe <test assemblies> /InIsolation; runtime probe in the Bug: breadcrumb-ui-boundary-guard-rejects-dispatcher-built-viewers #781 feature folder (evidence/other/dispatcher-synccontext-probe.2026-09-05T10-40.md)
  • Data source or fixture: QuickFiler.Test/Controllers/QfcHomeControllerRunAsyncTests.cs:329 (MTA caller of UiThread.Init(false))

Steps to Reproduce

  1. Bug: uithread-init-accepts-non-sta-callers #787: call UiThread.Init(false) from an MTA thread (the in-repo instance is the test at QfcHomeControllerRunAsyncTests.cs:329). It returns normally and every later UiThread.Dispatcher / UiSyncContext / UiThreadId read marshals onto a thread with no message loop.
  2. Bug: uithread-init-latch-not-rearmed-after-failed-initialize #788: arrange for Initialize() to throw (headless or non-STA), call Init(), fix the condition, call Init() again. The second call is a no-op because _loaded.CheckAndSetFirstCall at UiThread.cs:36 was consumed before Initialize() ran.
  3. Bug: uithread-synccontext-awaiter-always-posts-for-dispatcher-built-viewers #784: construct an ItemViewer through ItemViewerQueue.Dequeue (inside UiThread.Dispatcher.Invoke, so UiSyncContext is a DispatcherSynchronizationContext), then on the UI thread evaluate viewer.UiSyncContext.GetAwaiter().IsCompleted. It is false, so the continuation posts instead of running inline.

Expected Behavior

  • Init() rejects a non-STA caller with a named InvalidOperationException before capturing anything.
  • A failed Initialize() leaves the latch re-armed so a later Init() retries, without reintroducing the regression Refactor: pr-778-post-merge-review-residuals #782 measured.
  • IsCompleted is true when the caller already runs on the owning UI thread, regardless of which SynchronizationContext instance is ambient.

Actual Behavior

See the three reproduction steps. #787 succeeds silently and poisons the globals for the process lifetime; #788 leaves UiThread.Dispatcher throwing an exception that names Init() as the remedy while Init() is a no-op; #784 adds one queued hop per await and changes ordering relative to already-queued UI work.

Logs / Screenshots

Impact / Severity

  • Blocker
  • High
  • Medium
  • Low

Medium, carried from #787: in production ThisAddIn.cs:35-40 is the only Init() caller and runs on the Outlook STA, so the hazards are reachable today only from test code, but a worker-thread read of the lazy accessors before startup completes would poison the process. #784 and #788 are Low individually.

Source

From: docs/features/potential/2026-09-07-uithread-init-contract-residuals-784-787-788.md

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions