Skip to content

[release/11.0.1xx-preview4] [dotnet-watch] backport changes for mobile & .NET MAUI#54108

Merged
jonathanpeppers merged 4 commits intorelease/11.0.1xx-preview4from
dev/peppers/backport-watch-preview4
Apr 27, 2026
Merged

[release/11.0.1xx-preview4] [dotnet-watch] backport changes for mobile & .NET MAUI#54108
jonathanpeppers merged 4 commits intorelease/11.0.1xx-preview4from
dev/peppers/backport-watch-preview4

Conversation

jonathanpeppers and others added 4 commits April 27, 2026 13:21
Adds device selection to dotnet-watch for MAUI/mobile scenarios, mirroring the `dotnet-run` device selection flow from the spec (`documentation/specs/dotnet-run-for-maui.md`).

Merged `TargetFrameworkSelectionPrompt` and the new device prompt into a single `WatchSelectionPrompt` (library) / `SpectreWatchSelectionPrompt` (console app), keeping Spectre.Console isolated to the console app project.

After TFM selection, `HotReloadDotNetWatcher` calls the `ComputeAvailableDevices` MSBuild target via in-process MSBuild. A single device is auto-selected; multiple devices show an interactive Spectre prompt with search. The selected device and its `RuntimeIdentifier` are passed to `dotnet build` (`-p:Device`, `-p:RuntimeIdentifier`) and to the launched dotnet run subprocess (`--device`). A re-restore is performed when the device provides a `RuntimeIdentifier` not present in the original restore.

Adds `--device` CLI option to `dotnet-watch` for pre-specifying a device.

Tests:
- Unit tests for prompt selection, search, caching, and `FormatDevice`
- E2E tests using `DotnetRunDevices` test asset: interactive device selection, single-device auto-select

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Tomáš Matoušek <tmat@users.noreply.github.com>
Fixes: #53634

When a mobile/WebSocket app is shut down (Ctrl+C) or restarted (Ctrl+R), the child process is killed which closes the TCP connection. Kestrel tears down the HTTP context, and then `RunningProject.DisposeAsync` tries to dispose the WebSocket — hitting an `ObjectDisposedException` on the already-disposed `IFeatureCollection`. The `ListenForResponsesAsync` loop also logs a spurious `WebSocketException` error.

**Fixes:**
- `RequestHandler.Dispose`: catch `ObjectDisposedException` when the underlying Kestrel HTTP context is already torn down
- `IsExpectedConnectionTermination`: treat `WebSocketException` as expected when the socket state is `Aborted` (remote disconnected)

**Tests:**
- `CtrlC_ShutsDownCleanly`: verifies clean shutdown with WebSocket transport
- `CtrlR_RestartsCleanly`: verifies restart without `WebSocketException` or `ObjectDisposedException` errors

Both tests fail without the fix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Testing `dotnet-watch` end-to end on a `dotnet new maui` project...

I found the Spectre.Console input was just "stuck", neither up/down arrows or typing to search did anything!

`PhysicalConsole` runs `Console.ReadKey()` in a tight background loop, consuming all key presses and dispatching them via the `KeyPressed` event. When `SpectreBuildParametersSelectionPrompt` returned `AnsiConsole.Console` for non-redirected stdin, Spectre.Console would also call `Console.ReadKey()` internally, creating a race where `PhysicalConsole` steals every key press and the selection prompt appears completely stuck (arrow keys, typing, and search all do nothing).

Fix by always using `KeyPressedAnsiConsole`, which reads keys from `PhysicalConsole.KeyPressed` events instead of competing for `Console.ReadKey()`. This ensures a single key reader (`PhysicalConsole`) distributes keys to all consumers including Spectre.Console.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…#54023)

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:

<img width="480" height="588" alt="image" src="https://github.com/user-attachments/assets/9e5944ff-db11-4912-ae4d-48b53190b1da" />

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 27, 2026 18:25
@jonathanpeppers jonathanpeppers requested review from a team and tmat as code owners April 27, 2026 18:25
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

Backport of recent dotnet-watch improvements needed for mobile/.NET MAUI scenarios in the 11.0.1xx-preview4 release branch, including device selection, WebSocket shutdown/restart robustness, console prompt input fixes, and iOS deadlock avoidance.

Changes:

  • Add --device support and interactive device selection (via MSBuild ComputeAvailableDevices) and flow the selected device/RID through build and launch.
  • Fix WebSocket transport shutdown/restart behavior (expected termination handling; safe disposal) and add E2E coverage for Ctrl+C/Ctrl+R.
  • Replace the old TFM-only prompt with a combined build-parameters prompt, update localized resources, and add ConfigureAwait(false) in agent startup code paths to avoid iOS UI-thread deadlocks.

Reviewed changes

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

Show a summary per file
File Description
test/dotnet-watch.Tests/TestUtilities/TestOptions.cs Introduces shared GlobalOptions for tests (incl. binlog path).
test/dotnet-watch.Tests/TestUtilities/MockFileSetFactory.cs Updates test factory construction for new MSBuild factory signature.
test/dotnet-watch.Tests/TestUtilities/DotNetWatchTestBase.cs Updates watcher ctor usage for new selection prompt abstraction.
test/dotnet-watch.Tests/HotReload/TargetFrameworkSelectionPromptTests.cs Removes tests for the old TFM-only prompt.
test/dotnet-watch.Tests/HotReload/MobileHotReloadTests.cs Adds Ctrl+C/Ctrl+R regression tests for WebSocket transport shutdown/restart.
test/dotnet-watch.Tests/HotReload/MauiHotReloadTests.cs Adds device selection E2E tests using DotnetRunDevices test asset.
test/dotnet-watch.Tests/HotReload/CompilationHandlerTests.cs Updates ProjectGraphFactory construction signature in tests.
test/dotnet-watch.Tests/HotReload/BuildProjectsTests.cs Updates watcher ctor and build API calls for device selector parameter.
test/dotnet-watch.Tests/HotReload/BuildParametersSelectionPromptTests.cs Adds unit tests for combined TFM + device selection prompt behavior.
test/dotnet-watch.Tests/Build/ProjectGraphFactoryTests.cs Updates ProjectGraphFactory signature usage in tests.
test/dotnet-watch.Tests/Build/EvaluationTests.cs Updates MSBuildFileSetFactory signature usage in tests.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hant.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.zh-Hans.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.tr.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.ru.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.pt-BR.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.pl.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.ko.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.ja.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.it.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.fr.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.es.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.de.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/xlf/Resources.cs.xlf Adds new strings for device selection/help.
src/Dotnet.Watch/dotnet-watch/Watch/MsBuildFileSetFactory.cs Threads GlobalOptions into evaluation/project graph creation for build reporting/binlogs.
src/Dotnet.Watch/dotnet-watch/Watch/BuildEvaluator.cs Adds --device propagation for non-hot-reload watch loop process launches.
src/Dotnet.Watch/dotnet-watch/UI/SpectreBuildParametersSelectionPrompt.cs New combined Spectre prompt + key input bridge to avoid Console.ReadKey races.
src/Dotnet.Watch/dotnet-watch/Resources.resx Adds new device selection/help strings.
src/Dotnet.Watch/dotnet-watch/Program.cs Switches to combined selection prompt; updates MSBuild file listing evaluation call.
src/Dotnet.Watch/dotnet-watch/CommandLine/DotnetWatchCommandDefinition.cs Adds --device option as a watch-specific option.
src/Dotnet.Watch/dotnet-watch/CommandLine/CommandLineOptions.cs Parses/stores Device and flows it into ProjectOptions.
src/Dotnet.Watch/Watch/UI/TargetFrameworkSelectionPrompt.cs Removes old prompt abstraction (superseded).
src/Dotnet.Watch/Watch/UI/IReporter.cs Adds a new message descriptor for “no devices available”.
src/Dotnet.Watch/Watch/UI/DeviceInfo.cs Adds device model used for MSBuild-provided device metadata.
src/Dotnet.Watch/Watch/UI/BuildParametersSelectionPrompt.cs Adds new base prompt abstraction for TFM + device selection with caching.
src/Dotnet.Watch/Watch/Process/ProjectLauncher.cs Passes --device and --runtime to dotnet run when launching.
src/Dotnet.Watch/Watch/HotReload/HotReloadDotNetWatcher.cs Adds device selection orchestration via MSBuild target + re-restore for RID changes.
src/Dotnet.Watch/Watch/Context/ProjectOptions.cs Extends project options with Device and DeviceRuntimeIdentifier.
src/Dotnet.Watch/Watch/Build/ProjectGraphFactory.cs Threads GlobalOptions/EnvironmentOptions into LoadedProjectGraph for build manager/reporting.
src/Dotnet.Watch/Watch/Build/LoadedProjectGraph.cs Adds a shared ProjectBuildManager per loaded graph to support device computation/builds.
src/Dotnet.Watch/Watch/Build/EvaluationResult.cs Uses LoadedProjectGraph.BuildManager rather than owning a separate build manager.
src/Dotnet.Watch/Watch/Build/BuildNames.cs Adds ComputeAvailableDevices target name constant.
src/Dotnet.Watch/Watch.Aspire/Server/AspireWatcherLauncher.cs Updates watcher ctor usage for new selection prompt parameter name/type.
src/Dotnet.Watch/HotReloadClient/WebSocketClientTransport.cs Treats certain WebSocket failures/disposals as expected; guards disposal.
src/Dotnet.Watch/HotReloadAgent.Host/WebSocketTransport.cs Adds ConfigureAwait(false) to avoid iOS UI-thread deadlocks.
src/Dotnet.Watch/HotReloadAgent.Host/Listener.cs Adds ConfigureAwait(false) throughout receive/apply pipeline to avoid iOS UI-thread deadlocks.

Comment thread src/Dotnet.Watch/dotnet-watch/Watch/BuildEvaluator.cs
Comment thread test/dotnet-watch.Tests/TestUtilities/TestOptions.cs
@jonathanpeppers
Copy link
Copy Markdown
Member Author

Going to rerun some of the failures, copilot thinks this:

CI Failures on PR #54108 — Not related to the changes
❌ Failure 1: dotnet-watch.Tests.dll.5 (Linux x64)
Test: FileUpdateTests.TestCommand
Cause: Test hung — blame-hang timeout triggered and hang dumps were collected
Relation to PR: FileUpdateTests is not modified in this PR. This is a known flaky integration test that occasionally deadlocks waiting for dotnet watch subprocess output.
❌ Failure 2: Microsoft.NET.Publish.Tests.dll.5 (Windows x64)
Test: It_does_not_rewrite_the_single_file_unnecessarily
Cause: Timestamp flake — expected file write time 19:33:41.784 but got 19:33:43.774 (~2s drift)
Relation to PR: Microsoft.NET.Publish.Tests is completely unrelated to dotnet-watch changes. This is a known flaky timestamp-comparison test.

@jonathanpeppers jonathanpeppers merged commit 9a7cae8 into release/11.0.1xx-preview4 Apr 27, 2026
27 checks passed
@jonathanpeppers jonathanpeppers deleted the dev/peppers/backport-watch-preview4 branch April 27, 2026 23:53
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.

3 participants