[Vsintegration] Fix deadlock when a solution is opened during VS 2026… - #2046
Merged
RobertvanderHulst merged 1 commit intoAug 12, 2026
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
… 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:
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.