Skip to content

[Vsintegration] Fix deadlock when a solution is opened during VS 2026… - #2046

Merged
RobertvanderHulst merged 1 commit into
X-Sharp:devfrom
hpetriffer:fix/vs2026-startup-deadlock
Aug 12, 2026
Merged

[Vsintegration] Fix deadlock when a solution is opened during VS 2026…#2046
RobertvanderHulst merged 1 commit into
X-Sharp:devfrom
hpetriffer:fix/vs2026-startup-deadlock

Conversation

@hpetriffer

Copy link
Copy Markdown
Contributor

… startup

Opening a solution while VS is starting - a solution pinned to the start window or jumplist, or passed on the commandline - deadlocked the IDE in VS 2026. VS 2022 is not affected.

The managed solution loader that is new in VS 2026 (Microsoft.VisualStudio.CommonIDE.Solutions) needs our project factory while it walks the projects of the solution, so it demands XSharpProjectPackage synchronously from Solution.OpenAsync -> LoadProjectsLoopAsync -> Project.CanOpenProject -> ProjectTypeManager.HrCanOpenProject and then blocks the UI thread on the package load task in VsTask.InternalGetResult -> UIThreadReentrancyScope.WaitOnTaskComplete -> Task.WaitAny. That is a plain task wait and not a JoinableTask join, so nothing can be inlined onto the blocked UI thread. In VS 2022 the solution is opened over the native path, which does not block this way.

Our InitializeAsync blocked on the UI thread from a background thread through JoinableTaskFactory.Run, which can never complete in that situation: the UI thread waits for the package load and the package load waits for the UI thread. The observed stack was XSharpProjectPackage.InitializeAsync -> Logger.Initialize -> Logger.Start -> JoinableTaskFactory.Run -> WaitSynchronously -> CompleteOnCurrentThread.

All blocking waits are removed from the package initialization path:

  • Logger.Initialize becomes InitializeAsync and awaits its work. Start() is split into ConfigureSerilog(), which is pure configuration without VS services, and LogEnvironmentAsync(), which holds everything that needs the UI thread. StartAsync() awaits the latter; the synchronous Start(), which is still needed for ILogger.Start(), fires it and forgets it. This removes four blocking hops: GetVsVersionAsync/ IsOpeningAsync, the synchronous VS.Solutions.GetCurrentSolution() wrapper, GetSolutionExplorerWindowAsync and the COM walk over the solution items.
  • XSharpShellLink and XSharpShellEvents subscribe to the shell events in an awaited InitializeAsync() instead of calling JoinableTaskFactory.Run in their constructors.
  • The static XSharpShellEvents constructor no longer shows a modal message box when XSharpMsBuildDir is missing, because that blocks for the UI thread as well. The warning is reported from InitializeAsync().
  • Both packages await Logger.InitializeAsync() in their own InitializeAsync.

Awaiting SwitchToMainThreadAsync stays as it is. The shell resolves those - the RegisterProjectFactory call in the same method needs the UI thread too, so MPF project packages could not load at all otherwise - only blocking waits cannot be resolved.

Two small behaviour changes fall out of the split: the logger is marked active as soon as Serilog is configured instead of after the solution has been enumerated, so messages from other threads reach the log earlier, and the walk over the solution items now runs on the UI thread, which it did not before.

Verified in the VS 2026 experimental instance: the build without this change deadlocks reproducibly when devenv is started with the solution as an argument, this build starts through, writes the complete environment block to the log and loads all three packages without an error in the ActivityLog.

… startup

Opening a solution while VS is starting - a solution pinned to the start window or
jumplist, or passed on the commandline - deadlocked the IDE in VS 2026. VS 2022 is
not affected.

The managed solution loader that is new in VS 2026
(Microsoft.VisualStudio.CommonIDE.Solutions) needs our project factory while it walks
the projects of the solution, so it demands XSharpProjectPackage synchronously from
Solution.OpenAsync -> LoadProjectsLoopAsync -> Project.CanOpenProject ->
ProjectTypeManager.HrCanOpenProject and then blocks the UI thread on the package load
task in VsTask.InternalGetResult -> UIThreadReentrancyScope.WaitOnTaskComplete ->
Task.WaitAny. That is a plain task wait and not a JoinableTask join, so nothing can be
inlined onto the blocked UI thread. In VS 2022 the solution is opened over the native
path, which does not block this way.

Our InitializeAsync blocked on the UI thread from a background thread through
JoinableTaskFactory.Run, which can never complete in that situation: the UI thread waits
for the package load and the package load waits for the UI thread. The observed stack was
XSharpProjectPackage.InitializeAsync -> Logger.Initialize -> Logger.Start ->
JoinableTaskFactory.Run -> WaitSynchronously -> CompleteOnCurrentThread.

All blocking waits are removed from the package initialization path:

- Logger.Initialize becomes InitializeAsync and awaits its work. Start() is split into
  ConfigureSerilog(), which is pure configuration without VS services, and
  LogEnvironmentAsync(), which holds everything that needs the UI thread. StartAsync()
  awaits the latter; the synchronous Start(), which is still needed for ILogger.Start(),
  fires it and forgets it. This removes four blocking hops: GetVsVersionAsync/
  IsOpeningAsync, the synchronous VS.Solutions.GetCurrentSolution() wrapper,
  GetSolutionExplorerWindowAsync and the COM walk over the solution items.
- XSharpShellLink and XSharpShellEvents subscribe to the shell events in an awaited
  InitializeAsync() instead of calling JoinableTaskFactory.Run in their constructors.
- The static XSharpShellEvents constructor no longer shows a modal message box when
  XSharpMsBuildDir is missing, because that blocks for the UI thread as well. The warning
  is reported from InitializeAsync().
- Both packages await Logger.InitializeAsync() in their own InitializeAsync.

Awaiting SwitchToMainThreadAsync stays as it is. The shell resolves those - the
RegisterProjectFactory call in the same method needs the UI thread too, so MPF project
packages could not load at all otherwise - only blocking waits cannot be resolved.

Two small behaviour changes fall out of the split: the logger is marked active as soon
as Serilog is configured instead of after the solution has been enumerated, so messages
from other threads reach the log earlier, and the walk over the solution items now runs
on the UI thread, which it did not before.

Verified in the VS 2026 experimental instance: the build without this change deadlocks
reproducibly when devenv is started with the solution as an argument, this build starts
through, writes the complete environment block to the log and loads all three packages
without an error in the ActivityLog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@RobertvanderHulst
RobertvanderHulst merged commit e186912 into X-Sharp:dev Aug 12, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants