Skip to content

Avoid crossgening dotnet-watch startup hooks#53501

Merged
tmat merged 1 commit intodotnet:mainfrom
tmat:SuppressStartupHookR2R
Mar 17, 2026
Merged

Avoid crossgening dotnet-watch startup hooks#53501
tmat merged 1 commit intodotnet:mainfrom
tmat:SuppressStartupHookR2R

Conversation

@tmat
Copy link
Copy Markdown
Member

@tmat tmat commented Mar 17, 2026

The architecture of the app process might be different then the architecture of SDK.

Copilot AI review requested due to automatic review settings March 17, 2026 19:02
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 updates the redist CrossGen inputs to exclude dotnet-watch “hot reload” runtime dependency assemblies that are injected into user apps at runtime, avoiding ReadyToRun compilation of those specific DLLs during layout generation.

Changes:

  • Exclude Microsoft.Extensions.DotNetDeltaApplier.dll from CrossGen when it appears under DotnetTools/dotnet-watch.
  • Exclude Microsoft.AspNetCore.Watch.BrowserRefresh.dll from CrossGen when it appears under DotnetTools/dotnet-watch.

Copy link
Copy Markdown
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

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

This looks right.

I found this file has an r2r image inside:

D:\src\xamarin-android\bin\Debug\dotnet\sdk\11.0.100-preview.3.26165.107\DotnetTools\dotnet-watch\11.0.100-preview.3.26165.107\tools\net11.0\any\hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll

And then if you try to dotnet-watch an Android app, the app crashes at startup with:

03-17 15:08:17.066 15581 15581 E DOTNET  : Unhandled exception. System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
03-17 15:08:17.066 15581 15581 E DOTNET  :  ---> System.ArgumentException: Startup hook assembly 'Microsoft.Extensions.DotNetDeltaApplier' failed to load. See inner exception for details.
03-17 15:08:17.066 15581 15581 E DOTNET  :  ---> System.BadImageFormatException: An attempt was made to load a program with an incorrect format.
03-17 15:08:17.066 15581 15581 E DOTNET  :  (0x8007000B)
03-17 15:08:17.066 15581 15581 E DOTNET  :    at System.Reflection.RuntimeAssembly.<InternalLoad>g____PInvoke|48_0(NativeAssemblyNameParts* __pAssemblyNameParts_native, ObjectHandleOnStack __requestingAssembly_native, StackCrawlMarkHandle __stackMark_native, Int32 __throwOnFileNotFound_native, ObjectHandleOnStack __assemblyLoadContext_native, ObjectHandleOnStack __retAssembly_native)
03-17 15:08:17.066 15581 15581 E DOTNET  :    at System.Reflection.RuntimeAssembly.<InternalLoad>g____PInvoke|48_0(NativeAssemblyNameParts* __pAssemblyNameParts_native, ObjectHandleOnStack __requestingAssembly_native, StackCrawlMarkHandle __stackMark_native, Int32 __throwOnFileNotFound_native, ObjectHandleOnStack __assemblyLoadContext_native, ObjectHandleOnStack __retAssembly_native)
03-17 15:08:17.066 15581 15581 E DOTNET  :    at System.Reflection.RuntimeAssembly.InternalLoad(NativeAssemblyNameParts* pAssemblyNameParts, ObjectHandleOnStack requestingAssembly, StackCrawlMarkHandle stackMark, Boolean throwOnFileNotFound, ObjectHandleOnStack assemblyLoadContext, ObjectHandleOnStack retAssembly)
03-17 15:08:17.066 15581 15581 E DOTNET  :    at System.Reflection.RuntimeAssembly.InternalLoad(AssemblyName assemblyName, StackCrawlMark& stackMark, AssemblyLoadContext assemblyLoadContext, RuntimeAssembly requestingAssembly, Boolean throwOnFileNotFound)
03-17 15:08:17.066 15581 15581 E DOTNET  :    at System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyName(AssemblyName assemblyName)
03-17 15:08:17.066 15581 15581 E DOTNET  :    at System.StartupHookProvider.CallStartupHook(StartupHookNameOrPath startupHook)
03-17 15:08:17.066 15581 15581 E DOTNET  :    --- End of inner exception stack trace ---
03-17 15:08:17.066 15581 15581 E DOTNET  :    at System.StartupHookProvider.CallStartupHook(StartupHookNameOrPath startupHook)
03-17 15:08:17.066 15581 15581 E DOTNET  :    at System.StartupHookProvider.ProcessStartupHooks(String diagnosticStartupHooks)

Because it's trying to load a win-x64 R2R image on android-arm64.

@jonathanpeppers
Copy link
Copy Markdown
Member

jonathanpeppers commented Mar 17, 2026

Local build:

> pwsh -NoProfile -Command "`$r = [System.Reflection.PortableExecutable.PEReader]::new([System.IO.File]::OpenRead('D:\src\dotnet\sdk\artifacts\bin\redist\Debug\dotnet\sdk\11.0.100-dev\DotnetTools\dotnet-watch\11.0.100-dev\tools\net11.0\any\hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll')); `$r.PEHeaders.CorHeader.Flags; `$r.Dispose()"
ILOnly, StrongNameSigned

Broken build:

> pwsh -NoProfile -Command "`$r = [System.Reflection.PortableExecutable.PEReader]::new([System.IO.File]::OpenRead('D:\src\xamarin-android\bin\Debug\dotnet\sdk\11.0.100-preview.3.26165.107\DotnetTools\dotnet-watch\11.0.100-preview.3.26165.107\tools\net11.0\any\hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll')); `$r.PEHeaders.CorHeader.Flags; `$r.Dispose()"
ILLibrary, StrongNameSigned

ILLibrary means it has R2R inside. Looks good! 👍

@tmat tmat merged commit 3f09f97 into dotnet:main Mar 17, 2026
32 checks passed
@tmat tmat deleted the SuppressStartupHookR2R branch March 17, 2026 21:36
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/sdk#52492
Context: dotnet/sdk#52581

`dotnet-watch` now runs Android applications via:

    dotnet watch 🚀 [helloandroid (net10.0-android)] Launched 'D:\src\xamarin-android\bin\Debug\dotnet\dotnet.exe' with arguments 'run --no-build -e DOTNET_WATCH=1 -e DOTNET_WATCH_ITERATION=1 -e DOTNET_MODIFIABLE_ASSEMBLIES=debug -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://localhost:9000 -e DOTNET_STARTUP_HOOKS=D:\src\xamarin-android\bin\Debug\dotnet\sdk\10.0.300-dev\DotnetTools\dotnet-watch\10.0.300-dev\tools\net10.0\any\hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll -bl': process id 3356

And so the pieces on Android for this to work are:

~~ Startup Hook Assembly ~~

Parse out the value:

    <_AndroidHotReloadAgentAssemblyPath>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_STARTUP_HOOKS')->'%(Value)'->Exists())</_AndroidHotReloadAgentAssemblyPath>

And verify this assembly is included in the app:

    <ResolvedFileToPublish Include="$(_AndroidHotReloadAgentAssemblyPath)" />

Then, for Android, we need to patch up `$DOTNET_STARTUP_HOOKS` to be
just the assembly name, not the full path:

    <_AndroidHotReloadAgentAssemblyName>$([System.IO.Path]::GetFileNameWithoutExtension('$(_AndroidHotReloadAgentAssemblyPath)'))</_AndroidHotReloadAgentAssemblyName>
    ...
    <RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(_AndroidHotReloadAgentAssemblyName)" />

~~ Port Forwarding ~~

A new `_AndroidConfigureAdbReverse` target runs after deploying apps,
that does:

    adb reverse tcp:9000 tcp:9000

I parsed the value out of:

    <_AndroidWebSocketEndpoint>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT')->'%(Value)')</_AndroidWebSocketEndpoint>
    <_AndroidWebSocketPort>$([System.Text.RegularExpressions.Regex]::Match('$(_AndroidWebSocketEndpoint)', ':(\d+)').Groups[1].Value)</_AndroidWebSocketPort>

~~ Prevent Startup Hooks in Microsoft.Android.Run ~~

When I was implementing this, I keep seeing *two* clients connect to
`dotnet-watch` and I was pulling my hair to figure out why!

Then I realized that `Microsoft.Android.Run` was also getting
`$DOTNET_STARTUP_HOOKS`, and so we had a desktop process + mobile
process both trying to connect!

Easiest fix, is to disable startup hook support in
`Microsoft.Android.Run`. I reviewed the code in `dotnet run`, and it
doesn't seem correct to try to clear the env vars.

~~ Conclusion ~~

With these changes, everything is working!

    dotnet watch 🔥 C# and Razor changes applied in 23ms.

* Include $(AdbTarget) in adb reverse command

When multiple devices/emulators are connected, the adb reverse
command needs  (e.g. -s emulator-5554) to target
the correct device. Without it, the command fails with
'more than one device' or forwards on the wrong device.

NOTE: we run the test on MonoVM-only for now, until we get:
* dotnet/sdk#53501

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Context: dotnet/sdk#52492
Context: dotnet/sdk#52581

`dotnet-watch` now runs Android applications via:

    dotnet watch 🚀 [helloandroid (net10.0-android)] Launched 'D:\src\xamarin-android\bin\Debug\dotnet\dotnet.exe' with arguments 'run --no-build -e DOTNET_WATCH=1 -e DOTNET_WATCH_ITERATION=1 -e DOTNET_MODIFIABLE_ASSEMBLIES=debug -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://localhost:9000 -e DOTNET_STARTUP_HOOKS=D:\src\xamarin-android\bin\Debug\dotnet\sdk\10.0.300-dev\DotnetTools\dotnet-watch\10.0.300-dev\tools\net10.0\any\hotreload\net10.0\Microsoft.Extensions.DotNetDeltaApplier.dll -bl': process id 3356

And so the pieces on Android for this to work are:

~~ Startup Hook Assembly ~~

Parse out the value:

    <_AndroidHotReloadAgentAssemblyPath>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_STARTUP_HOOKS')->'%(Value)'->Exists())</_AndroidHotReloadAgentAssemblyPath>

And verify this assembly is included in the app:

    <ResolvedFileToPublish Include="$(_AndroidHotReloadAgentAssemblyPath)" />

Then, for Android, we need to patch up `$DOTNET_STARTUP_HOOKS` to be
just the assembly name, not the full path:

    <_AndroidHotReloadAgentAssemblyName>$([System.IO.Path]::GetFileNameWithoutExtension('$(_AndroidHotReloadAgentAssemblyPath)'))</_AndroidHotReloadAgentAssemblyName>
    ...
    <RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS" Value="$(_AndroidHotReloadAgentAssemblyName)" />

~~ Port Forwarding ~~

A new `_AndroidConfigureAdbReverse` target runs after deploying apps,
that does:

    adb reverse tcp:9000 tcp:9000

I parsed the value out of:

    <_AndroidWebSocketEndpoint>@(RuntimeEnvironmentVariable->WithMetadataValue('Identity', 'DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT')->'%(Value)')</_AndroidWebSocketEndpoint>
    <_AndroidWebSocketPort>$([System.Text.RegularExpressions.Regex]::Match('$(_AndroidWebSocketEndpoint)', ':(\d+)').Groups[1].Value)</_AndroidWebSocketPort>

~~ Prevent Startup Hooks in Microsoft.Android.Run ~~

When I was implementing this, I keep seeing *two* clients connect to
`dotnet-watch` and I was pulling my hair to figure out why!

Then I realized that `Microsoft.Android.Run` was also getting
`$DOTNET_STARTUP_HOOKS`, and so we had a desktop process + mobile
process both trying to connect!

Easiest fix, is to disable startup hook support in
`Microsoft.Android.Run`. I reviewed the code in `dotnet run`, and it
doesn't seem correct to try to clear the env vars.

~~ Conclusion ~~

With these changes, everything is working!

    dotnet watch 🔥 C# and Razor changes applied in 23ms.

* Include $(AdbTarget) in adb reverse command

When multiple devices/emulators are connected, the adb reverse
command needs  (e.g. -s emulator-5554) to target
the correct device. Without it, the command fails with
'more than one device' or forwards on the wrong device.

NOTE: we run the test on MonoVM-only for now, until we get:
* dotnet/sdk#53501

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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