[mono] read $DOTNET_STARTUP_HOOKS#123964
Merged
lewing merged 2 commits intodotnet:mainfrom Feb 5, 2026
Merged
Conversation
Context: https://github.com/dotnet/runtime/blob/741b7157472b9a5c83a78f781ccfa8cd39707763/docs/design/features/host-startup-hook.md Context: dotnet/android#10755 Mono does not appear to read the `$DOTNET_STARTUP_HOOKS` env var when calling `System.StartupHookProvider.ProcessStartupHooks()` and only passes in `""`. So, to get the same behavior as other runtimes, you would need to workaround with something like: <RuntimeHostConfigurationOption Include="STARTUP_HOOKS" Value="MyStartupHook" Condition=" '$(UseMonoRuntime)' == 'true' " /> Let's read the env var in Mono so that it works the same way as other runtimes.
51 tasks
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds support for reading the DOTNET_STARTUP_HOOKS environment variable in Mono runtime to match the behavior of CoreCLR and NativeAOT runtimes. Previously, Mono only passed an empty string to System.StartupHookProvider.ProcessStartupHooks(), ignoring the environment variable.
Changes:
- Mono now reads
DOTNET_STARTUP_HOOKSenvironment variable and passes its value to the startup hook processor - This aligns Mono's behavior with CoreCLR and NativeAOT implementations
- Eliminates the need for workarounds using
STARTUP_HOOKSruntime configuration option when using Mono
This was referenced Feb 4, 2026
steveisok
approved these changes
Feb 4, 2026
lewing
approved these changes
Feb 5, 2026
lewing
pushed a commit
to lewing/runtime
that referenced
this pull request
Feb 9, 2026
Context: https://github.com/dotnet/runtime/blob/741b7157472b9a5c83a78f781ccfa8cd39707763/docs/design/features/host-startup-hook.md Context: dotnet/android#10755 Mono does not appear to read the `$DOTNET_STARTUP_HOOKS` env var when calling `System.StartupHookProvider.ProcessStartupHooks()` and only passes in `""`. So, to get the same behavior as other runtimes, you would need to workaround with something like: <RuntimeHostConfigurationOption Include="STARTUP_HOOKS" Value="MyStartupHook" Condition=" '$(UseMonoRuntime)' == 'true' " /> Let's read the env var in Mono so that it works the same way as other runtimes.
jonathanpeppers
added a commit
to dotnet/sdk
that referenced
this pull request
Feb 20, 2026
Fixes: #52492 # `dotnet watch` for .NET MAUI Scenarios ## Overview This implements `dotnet watch` / Hot Reload for mobile platforms (Android, iOS), which cannot use the standard named pipe transport. Similar to how web applications already use websockets for reloading CSS and JavaScript, we use the same model for mobile applications. ## Transport Selection | Platform | Transport | Reason | |-----------------|------------|-------------------------------------------------------------------------------| | Desktop/Console | Named Pipe | Existing implementation, Fast, local IPC | | Android/iOS | WebSocket | Named pipes don't work over the network; `adb reverse` tunnels the connection | `dotnet-watch` detects WebSocket transport via the `HotReloadWebSockets` capability: ```xml <ProjectCapability Include="HotReloadWebSockets" /> ``` Mobile workloads (Android, iOS) add this capability to their SDK targets. This allows any workload to opt into WebSocket-based hot reload. ## SDK Changes ### Architecture Both named pipe and WebSocket transports now share a common `ClientTransport` abstraction consumed by `DefaultHotReloadClient`: - **`NamedPipeClientTransport`** — existing local IPC path - **`WebSocketClientTransport`** — new path for mobile, composes a sealed `KestrelWebSocketServer` The agent side mirrors this with a `Transport` base class (`NamedPipeTransport` / `WebSocketTransport`). ### WebSocket Details `dotnet-watch` already has a WebSocket server for web apps: `BrowserRefreshServer`. This server: - Hosts via Kestrel on a local HTTP or HTTPS endpoint (e.g., `http://127.0.0.1:<port>` or `https://localhost:<port>`) - Communicates with JavaScript (`aspnetcore-browser-refresh.js`) injected into web pages - Sends commands like "refresh CSS", "reload page", "apply Blazor delta" For mobile, we reuse the Kestrel infrastructure but with a different protocol: | Server | Client | Protocol | |----------------------------|------------------------|--------------------------------------------| | `BrowserRefreshServer` | JavaScript in browser | JSON messages for CSS/page refresh | | `WebSocketClientTransport` | Startup hook on device | Binary delta payloads (same as named pipe) | `WebSocketClientTransport` composes a sealed `KestrelWebSocketServer` (shared with `BrowserRefreshServer`) and speaks the same binary protocol as the named pipe transport, just over WebSocket instead. Static asset updates (CSS) are also supported. ### WebSocket Authentication To prevent unauthorized processes from connecting to the hot reload server, `WebSocketClientTransport` uses RSA-based authentication identical to `BrowserRefreshServer`: 1. **Server generates RSA key pair:** `SharedSecretProvider` creates a 2048-bit RSA key on startup 2. **Public key exported:** The public key (X.509 SubjectPublicKeyInfo, Base64-encoded) is passed to the app via `DOTNET_WATCH_HOTRELOAD_WEBSOCKET_KEY` 3. **Client encrypts secret:** The startup hook generates a random 32-byte secret, encrypts it with RSA-OAEP-SHA256 using the public key 4. **Secret sent as subprotocol:** The encrypted secret is URL-encoded (`WebUtility.UrlEncode`) and sent as the WebSocket subprotocol header — same encoding as `BrowserRefreshServer` 5. **Server validates:** `WebSocketClientTransport.HandleRequestAsync` decodes and decrypts the subprotocol value, accepting the connection only if decryption succeeds ### HTTPS Support `KestrelWebSocketServer` supports binding both HTTP and HTTPS ports simultaneously via the `securePort` parameter. When `AgentWebSocketSecurePort` is configured, the server binds a WSS endpoint alongside the WS endpoint. ### Environment Variables `dotnet-watch` launches the app via: ```dotnetcli dotnet run --no-build \ -e DOTNET_WATCH=1 \ -e DOTNET_MODIFIABLE_ASSEMBLIES=debug \ -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://127.0.0.1:<port> \ -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_KEY=<base64-encoded-rsa-public-key> \ -e DOTNET_STARTUP_HOOKS=<path to DeltaApplier.dll> ``` The port is dynamically assigned (defaults to 0, meaning the OS picks an available port) to avoid conflicts in CI and parallel test scenarios. The `DOTNET_WATCH_AGENT_WEBSOCKET_PORT` environment variable can override this if a specific port is needed. `DOTNET_WATCH_AGENT_WEBSOCKET_SECURE_PORT` optionally enables WSS. These environment variables are passed as `@(RuntimeEnvironmentVariable)` MSBuild items to the workload. See `dotnet-run-for-maui.md` for details on `dotnet run` and environment variables. ## Android Workload Changes (Example Integration) ### [dotnet/android#10770](dotnet/android#10770) — RuntimeEnvironmentVariable Support Enables the Android workload to receive env vars from `dotnet run -e`: - Adds `<ProjectCapability Include="RuntimeEnvironmentVariableSupport" />` - Adds `<ProjectCapability Include="HotReloadWebSockets" />` to opt into WebSocket-based hot reload - Configures `@(RuntimeEnvironmentVariable)` items, so they will apply to Android. ### [dotnet/android#10778](dotnet/android#10778) — dotnet-watch Integration 1. **Startup Hook:** Parses `DOTNET_STARTUP_HOOKS`, includes the assembly in the app package, rewrites the path to just the assembly name (since the full path doesn't exist on device) 2. **Port Forwarding:** Runs `adb reverse tcp:<port> tcp:<port>` so the device can reach the host's WebSocket server via `127.0.0.1:<port>` (port is parsed from the endpoint URL) 3. **Prevents Double Connection:** Disables startup hooks in `Microsoft.Android.Run` (the desktop launcher) so only the mobile app connects ## Data Flow 1. **Build:** `dotnet-watch` builds the project, detects `HotReloadWebSockets` capability 2. **Launch:** `dotnet run -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_ENDPOINT=ws://127.0.0.1:<port> -e DOTNET_WATCH_HOTRELOAD_WEBSOCKET_KEY=<key> -e DOTNET_STARTUP_HOOKS=...` 3. **Workload:** Android build tasks:a - Include the startup hook DLL in the APK - Set up ADB port forwarding for the dynamically assigned port - Rewrite env vars for on-device paths 4. **Device:** App starts → StartupHook loads → `Transport.TryCreate()` reads env vars → `WebSocketTransport` encrypts secret with RSA public key → connects to `ws://127.0.0.1:<port>` with encrypted secret as subprotocol 5. **Server:** `WebSocketClientTransport` validates the encrypted secret, accepts connection 6. **Hot Reload:** File change → delta compiled → sent over WebSocket → applied on device ## iOS Similar changes will be made in the iOS workload to opt into WebSocket-based hot reload: - Add `<ProjectCapability Include="HotReloadWebSockets" />` - Handle startup hooks and port forwarding similar to Android ## Dependencies - **[runtime#123964](dotnet/runtime#123964 [mono] read `$DOTNET_STARTUP_HOOKS` — needed for Mono runtime to honor startup hooks (temporary workaround via `RuntimeHostConfigurationOption`) Co-authored-by: Tomas Matousek <tomat@microsoft.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.
Context: https://github.com/dotnet/runtime/blob/741b7157472b9a5c83a78f781ccfa8cd39707763/docs/design/features/host-startup-hook.md
Context: dotnet/android#10755
Mono does not appear to read the
$DOTNET_STARTUP_HOOKSenv var when callingSystem.StartupHookProvider.ProcessStartupHooks()and only passes in"".So, to get the same behavior as other runtimes, you would need to workaround with something like:
Let's read the env var in Mono so that it works the same way as other runtimes.