Repository navigation
[AndroidSdk] Limit inherited handles for persistent emulators - #13523
Closed
simonrozsival wants to merge 2 commits into
Closed
simonrozsival wants to merge 2 commits into
simonrozsival wants to merge 2 commits into
Conversation
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Cleanup can kill an unrelated process group if the cached launcher PID is reused, and a throwing logger can cause disposal to terminate a successfully booted emulator.
3 open issues
What changed in this PR
This PR separates persistent emulator lifetimes from caller-owned streams in Android SDK tooling and bootstrap tasks, addressing part of #12981.
Changes:
- Introduces shared emulator lifetime handling with retained diagnostic files, null stdin, and platform-specific cleanup.
- Updates boot and bootstrap startup to join diagnostic forwarding, handle cancellation, and preserve successful emulators.
- Adds Unix lifecycle regression tests and documents the ownership contract.
| File | Description |
|---|---|
| tests/Xamarin.Android.Tools.AndroidSdk-Tests/EmulatorRunnerTests.cs | Adds persistence, cancellation, fork cleanup, and diagnostic tests. |
| tests/Microsoft.Android.Build.BaseTasks-Tests/StartAndroidEmulatorTests.cs | Adds bootstrap startup and lifetime regression tests. |
| tests/Microsoft.Android.Build.BaseTasks-Tests/Microsoft.Android.Build.BaseTasks-Tests.csproj | References bootstrap tasks for testing. |
| src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs | Integrates lifetime ownership and bounds discovery within the boot deadline. |
| src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorProcess.cs | Implements independent streams, diagnostic forwarding, and owned-process cleanup. |
| src/Xamarin.Android.Tools.AndroidSdk/README.md | Documents persistence, disposal, and cleanup behavior. |
| build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks/StartAndroidEmulator.cs | Adopts shared lifetime handling and adds cancellation support. |
| build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks.csproj | Links the shared emulator implementation. |
| .github/skills/tests/references/test-catalog.md | Adds the focused bootstrap emulator test command. |
Review effort: Balanced
| } else { | ||
| // StartDetached creates a new Unix session and process group. A forked | ||
| // QEMU keeps this group even after the launcher has exited/reparented. | ||
| if (KillProcessGroup (-processId, 9) != 0 && Marshal.GetLastPInvokeError () != 3) |
Comment on lines
+84
to
+87
| if (!AssignProcessToJobObject (job, process.SafeHandle)) | ||
| throw new Win32Exception (Marshal.GetLastPInvokeError ()); | ||
| process.assignedToJob = true; | ||
| process.SafeHandle.Resume (); |
Comment on lines
+178
to
+183
| try { | ||
| CompleteOutput (); | ||
| } catch { | ||
| Stop (); | ||
| throw; | ||
| } |
7 tasks
simonrozsival
force-pushed
the
simonrozsival-license-installer-process-pumps
branch
from
November 12, 2026 16:48
d4f9f1c to
a235b37
Compare
simonrozsival
force-pushed
the
simonrozsival-persistent-emulator-lifetimes
branch
2 times, most recently
from
November 13, 2026 09:24
af3d0b8 to
3b25c37
Compare
simonrozsival
force-pushed
the
simonrozsival-license-installer-process-pumps
branch
2 times, most recently
from
November 13, 2026 10:51
7d5da70 to
c545363
Compare
Set the .NET 11 `ProcessStartInfo.InheritedHandles = []` for the long-lived emulator processes started by `EmulatorRunner.LaunchEmulator` and the bootstrap `StartAndroidEmulator` task, so the emulator only inherits its standard handles instead of every inheritable handle in the parent (e.g. other concurrently started processes' pipes). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`ProcessStartInfo.InheritedHandles` is .NET 11+ API, and Xamarin.Android.Tools.AndroidSdk also targets `netstandard2.0`, so only set it under `NET11_0_OR_GREATER`; netstandard2.0 behavior is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
simonrozsival
force-pushed
the
simonrozsival-persistent-emulator-lifetimes
branch
from
November 13, 2026 11:08
3b25c37 to
2dcfcd1
Compare
Member
Author
|
Closing: per discussion, the emulator InheritedHandles change isn't worth carrying (the shipped boot path compiles against netstandard2.0, where it's excluded). |
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: #12981
Context: https://devblogs.microsoft.com/dotnet/process-api-improvements-in-dotnet-11/
EmulatorRunner.LaunchEmulator()and the bootstrap<StartAndroidEmulator/>task start emulator processes that intentionally outlive the code that launched them. Until now they used the defaultProcessStartInfohandle inheritance, so a long-lived emulator received every inheritable handle the parent had open at that moment, not just its own redirected stdout/stderr.That matters because pipe EOF is only observed once every copy of the write end is closed. If another process was being started concurrently with redirected output (for example
adboremulator -list-avds), the emulator could inherit that sibling's pipe handle and keep it open for the emulator's whole lifetime. The sibling's reader would then never see EOF and would hang or run into its timeout..NET 11 adds
ProcessStartInfo.InheritedHandles. An empty list means only the standard handles are inherited, so setInheritedHandles = []on both emulator launches.Xamarin.Android.Tools.AndroidSdkalso targetsnetstandard2.0, where the property does not exist, so theEmulatorRunnerassignment is guarded by#if NET11_0_OR_GREATER;netstandard2.0behavior is unchanged.Everything else stays as it was:
LaunchEmulator) or scanned for startup errors (<StartAndroidEmulator/>);/bin/sh -c "trap '' INT; exec …"launch and argument quoting are unchanged, so the emulator still survives Ctrl+C in the parent terminal;StartDetachedis deliberately not used, because it would replace the standard handles with the null device and discard emulator diagnostics.This PR replaces an earlier, much larger revision (a new
EmulatorProcesstype, file-backed logs, process groups and Windows job objects) with this minimal, targeted change.Note that
Xamarin.Android.Build.Taskstargetsnetstandard2.0, so the in-product<BootAndroidEmulator/>→EmulatorRunner.BootEmulatorAsync()path compiles against thenetstandard2.0build where the guarded assignment is excluded; onlynet11.0+consumers ofXamarin.Android.Tools.AndroidSdkand the bootstrap<StartAndroidEmulator/>task get the new behavior.Testing (macOS,
DOTNET_ROOT= locally built .NET runtime 12.0.0-alpha.1.26556.115):netstandard2.0:dotnet build src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj -nr:false -p:NuGetAudit=false -p:AndroidToolsDisableMultiTargeting=false -p:DotNetTargetFrameworkVersion=11.0 -f netstandard2.0succeeded, 0 warnings, 0 errors.net11.0: the same command with-f net11.0succeeded, 0 warnings, 0 errors.Default
net12.0(dotnet build src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj -nr:false -p:NuGetAudit=false -p:AndroidToolsDisableMultiTargeting=false, resolvingTargetFrameworks=netstandard2.0;net12.0) fails with exit code 1 on both this PR and its base07c58d96, with the same two errors:The project adds
PublicAPI/$(TargetFramework)/*unconditionally, and onlynet10.0,net11.0andnetstandard2.0baselines exist; there is no supported property to select or generate them, sonet12.0compilation was not validated.Bootstrap (
net11.0):dotnet build build-tools/Xamarin.Android.Tools.BootstrapTasks/Xamarin.Android.Tools.BootstrapTasks.csprojsucceeded.dotnet test tests/Xamarin.Android.Tools.AndroidSdk-Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj -nr:false -p:NuGetAudit=false --filter FullyQualifiedName~EmulatorRunnerTestspassed 27 of 27 (test assemblynet11.0, against thenet11.0Xamarin.Android.Tools.AndroidSdkbuild), at head2dcfcd169b.No new test asserts the non-inheritance itself. Windows, Linux and on-device runs were not done.