Skip to content

[dotnet watch] fix deadlock on iOS with UIKitSynchronizationContext#54023

Open
jonathanpeppers wants to merge 1 commit intorelease/10.0.4xxfrom
dev/peppers/watch-websocket-configureawait
Open

[dotnet watch] fix deadlock on iOS with UIKitSynchronizationContext#54023
jonathanpeppers wants to merge 1 commit intorelease/10.0.4xxfrom
dev/peppers/watch-websocket-configureawait

Conversation

@jonathanpeppers
Copy link
Copy Markdown
Member

On iOS with CoreCLR, UIKitSynchronizationContext is installed before startup hooks run. Listener.Listen() calls GetAwaiter().GetResult() on the main thread, and await continuations try to post back to the blocked UI thread, causing a deadlock. Fix by adding ConfigureAwait(false) to awaits in the startup hook's call chain. On Android, just due to startup ordering the SynchronizationContext was not set.

I didn't think of a way we could test this easily -- I basically built the dotnet/macios repo and copied dotnet-watch files on top to manually test. An end-to-end test in the dotnet/macios repo might be the best place.

With these changes in place:

2026-04-21 14:41:01.681990-0500 heyo[27489:980549] [HotReload] DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://localhost:61875
2026-04-21 14:41:01.686900-0500 heyo[27489:980549] [HotReload] Connecting to Hot Reload server via WebSocket ws://localhost:61875.
2026-04-21 14:41:01.696114-0500 heyo[27489:980549] [HotReload] Connecting to ws://localhost:61875...
dotnet watch 🔥 [heyo (net11.0-ios)] WebSocket client connected
2026-04-21 14:41:01.754571-0500 heyo[27489:980834] [HotReload] Connected.
2026-04-21 14:41:01.755569-0500 heyo[27489:980834] [HotReload] Sending InitializationResponse (247 bytes)
dotnet watch 🔥 [heyo (net11.0-ios)] Capabilities: 'Baseline AddMethodToExistingType AddStaticFieldToExistingType AddInstanceFieldToExistingType NewTypeDefinition ChangeCustomAttributes UpdateParameters GenericUpdateMethod GenericAddMethodToExistingType GenericAddFieldToExistingType AddFieldRva AddExplicitInterfaceImplementation'.
2026-04-21 14:41:01.768519-0500 heyo[27489:980834] [HotReload] Received 1 bytes
dotnet watch ⌚ Waiting for changes
dotnet watch ⌚ File change: Update '/Users/jopeppers/src/heyo/SceneDelegate.cs'.
dotnet watch ⌚ File updated: ./SceneDelegate.cs
dotnet watch ⌚ Updating document text of '/Users/jopeppers/src/heyo/SceneDelegate.cs'.
dotnet watch ⌚ Solution after document update: v2
dotnet watch 🔥 Hot reload capabilities: AddExplicitInterfaceImplementation AddFieldRva AddInstanceFieldToExistingType AddMethodToExistingType AddStaticFieldToExistingType Baseline ChangeCustomAttributes GenericAddFieldToExistingType GenericAddMethodToExistingType GenericUpdateMethod NewTypeDefinition UpdateParameters.
dotnet watch 🔥 [heyo (net11.0-ios)] Sending update batch #0
2026-04-21 14:41:13.296225-0500 heyo[27489:980834] [HotReload] Received 5770 bytes
2026-04-21 14:41:13.338366-0500 heyo[27489:980834] [HotReload] Sending UpdateResponse (466 bytes)
dotnet watch 🕵️ [heyo (net11.0-ios)] Writing capabilities: Baseline AddMethodToExistingType AddStaticFieldToExistingType AddInstanceFieldToExistingType NewTypeDefinition ChangeCustomAttributes UpdateParameters GenericUpdateMethod GenericAddMethodToExistingType GenericAddFieldToExistingType AddFieldRva
dotnet watch 🕵️ [heyo (net11.0-ios)] Applying updates to module 24147f53-599d-4c51-872f-f2073583eddb.
dotnet watch 🕵️ [heyo (net11.0-ios)] Invoking metadata update handlers.
dotnet watch 🕵️ [heyo (net11.0-ios)] System.Reflection.Metadata.RuntimeTypeMetadataUpdateHandler.ClearCache
dotnet watch 🕵️ [heyo (net11.0-ios)] Updates applied.
dotnet watch 🔥 [heyo (net11.0-ios)] Update batch #0 completed.
dotnet watch 🔥 C# and Razor changes applied in 567ms.

I can see it working on a net11.0-ios project in an iOS simulator:

image

…text

On iOS with CoreCLR, UIKitSynchronizationContext is installed before
startup hooks run. Listener.Listen() calls GetAwaiter().GetResult()
on the main thread, and await continuations try to post back to the
blocked UI thread, causing a deadlock. Fix by adding
ConfigureAwait(false) to awaits in the startup hook's call chain.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers jonathanpeppers requested review from a team and tmat as code owners April 21, 2026 19:48
Copilot AI review requested due to automatic review settings April 21, 2026 19:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a deadlock in dotnet watch Hot Reload on iOS/CoreCLR caused by UIKitSynchronizationContext being installed before startup hooks run, combined with synchronous blocking (GetAwaiter().GetResult()) during startup-hook initialization.

Changes:

  • Add ConfigureAwait(false) to awaited operations in the startup-hook initialization call chain within Listener.
  • Add ConfigureAwait(false) to awaited operations in WebSocketTransport send/receive/connect paths to avoid resuming onto the UI SynchronizationContext.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/Dotnet.Watch/HotReloadAgent.Host/WebSocketTransport.cs Prevents WebSocket connect/send/receive continuations from attempting to marshal back to a potentially-blocked UI thread.
src/Dotnet.Watch/HotReloadAgent.Host/Listener.cs Prevents initialization/update-processing continuations from attempting to resume on the UI SynchronizationContext during the synchronous startup-hook initialization phase.

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