Fix emulator start false-positive when launcher forks and exits#131
Conversation
On Windows, `emulator.exe` is a thin launcher that spawns
`qemu-system-*.exe` and exits itself (typically with code 0) while the
VM keeps running. With `RedirectStandardOutput=true` and
`BeginOutputReadLine`/`BeginErrorReadLine` active on the launcher
process, the .NET runtime can drop the managed Process handle after the
launcher exits. The next call to `process.HasExited` then throws
`InvalidOperationException: No process is associated with this object`,
which `AvdManager.StartAvdAsync` was wrapping into a user-visible
error:
Error [E2106]: Failed to start AVD 'MAUI_Emulator_API_36.1':
No process is associated with this object.
— even though the AVD booted successfully.
The upstream `EmulatorRunner.BootEmulatorAsync` already handles this
case and logs `Emulator launcher process exited with code 0 (likely
forked). Continuing to poll adb devices.` Our fire-and-forget `start`
path did not.
This change:
* Wraps the post-`Task.Delay` `HasExited`/`ExitCode` check in a
try/catch for `InvalidOperationException` — treats it as "launcher
reaped, assume successful fork".
* Only propagates a failure when we can actually confirm a non-zero
exit code.
* Adds `EmulatorStartTimingTests` (two guardrail `[Fact]`s, opt-in
via `MAUI_TEST_AVD_NAME`) measuring the gap between `adb`
reporting `sys.boot_completed=1` and the `start` command
returning — baseline for #122.
Fixes the E2106 crash reported against `maui android emulator start`.
Related to #122 (emulator-start timing investigation).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes a Windows-specific false-positive failure in maui android emulator start where the emulator launcher (emulator.exe) forks and exits, causing .NET Process handle queries (HasExited/ExitCode) to throw and get misreported as an E2106 start failure.
Changes:
- Hardened the post-launch “immediate crash” check in
AvdManager.StartAvdAsyncto tolerateInvalidOperationExceptionwhen the launcher process handle is no longer queryable, while still failing on confirmed non-zero exit codes. - Added opt-in, live timing guardrail tests (
MAUI_TEST_AVD_NAME) to measure the gap between adb-confirmed boot andStartAvdAsynccompletion for both already-booted and cold-start scenarios.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/Cli/Microsoft.Maui.Cli/Providers/Android/AvdManager.cs |
Avoids misreporting successful Windows fork/exit behavior as an emulator start failure by guarding Process exit checks. |
src/Cli/Microsoft.Maui.Cli.UnitTests/EmulatorStartTimingTests.cs |
Adds opt-in live tests to detect regressions in emulator-start return timing relative to adb boot confirmation. |
| /// Methodology: | ||
| /// 1. Resolve adb from ANDROID_HOME / ANDROID_SDK_ROOT. | ||
| /// 2. Confirm via a direct <c>adb shell getprop sys.boot_completed</c> that | ||
| /// the emulator with the given AVD name is fully booted — record the | ||
| /// wall-clock time of that confirmation (T_adbBooted). | ||
| /// 3. Invoke the exact same code path the CLI uses | ||
| /// (<see cref="AvdManager.StartAvdAsync"/>) and record its completion | ||
| /// time (T_toolReturned). | ||
| /// 4. Compute the gap T_toolReturned - T_adbBooted. | ||
| /// | ||
| /// Today this gap is approximately 60 seconds (caused by | ||
| /// <c>AdbRunner.ListDevicesAsync</c> looping through emulator devices and | ||
| /// running <c>adb -s emulator-XXXX emu avd name</c>, which blocks on adb's | ||
| /// internal socket/auth timeout). After the fix the gap should be < 5s. | ||
| /// </summary> |
There was a problem hiding this comment.
The XML doc says this test records the wall-clock time when adb first confirms boot (T_adbBooted) and then computes T_toolReturned - T_adbBooted, but the implementation currently just measures how long IsBootCompletedAsync takes (adbConfirmAtMs) and then measures StartAvdAsync duration with a separate stopwatch. Either update the methodology comment to match what’s actually being measured, or switch to a single shared stopwatch (or timestamp) so the reported “gap” is truly time-from-adb-confirmation to tool return.
🔍 Expert Code Review — PR #131Methodology: 3 independent reviewers with adversarial consensus. Findings included only when ≥2/3 reviewers agree (disputed findings were re-evaluated by the non-flagging reviewers). 🟡 MODERATE —
|
| # | Severity | File | Consensus | Category |
|---|---|---|---|---|
| 1 | 🟡 MODERATE | AvdManager.cs:258 |
2/3 | Bug — broad exception catch masks failures |
| 2 | 🟡 MODERATE | EmulatorStartTimingTests.cs:267 |
2/3 | Bug — deadlock pattern in test helper |
| 3 | 🟡 MODERATE | EmulatorStartTimingTests.cs:131 |
3/3 | Race condition — unsynchronized shared state |
| 4 | 🟢 MINOR | EmulatorStartTimingTests.cs:174 |
3/3 | Test gap — one-sided assertion |
| 5 | 🟢 MINOR | EmulatorStartTimingTests.cs:52 |
3/3 | Test gap — silent pass vs skip |
Discarded (single reviewer only): error message null literal, pre-existing process disposal, Process.Start null-forgiving operator, cold-start test teardown — each flagged by only 1/3 and not confirmed by the other two.
🏗️ CI Status
- ✅ CLA: passed
- 🔄 Build (macOS): in progress
- 🔄 Build (Windows): in progress
Overall Assessment
The core production fix in AvdManager.cs is correct and well-targeted — it properly handles the Windows emulator launcher fork/exit pattern that caused false E2106 errors. Finding #1 (broad catch) is the only production-code concern and is low risk in practice (the LaunchEmulator code path makes it unlikely HasExited would throw for other reasons), but narrowing the filter is a quick defensive improvement.
Findings #2–5 are all in the new test file. The tests are well-designed opt-in integration guardrails, but have minor robustness issues that could cause silent misreports in edge cases.
Generated by Expert Code Review (auto) for issue #131 · ● 7.3M · ◷
Fixes the E2106 crash reported on
maui android emulator startwhen the emulator actually boots successfully.Repro
At this point
adb devicesshows nothing yet, but theemulator.exeandqemu-system-x86_64.exeprocesses ARE running — the emulator boots fine; the CLI just misreports the outcome.Root cause
On Windows,
emulator.exeis a launcher that forksqemu-system-*.exeand exits itself (typically with code 0) while the VM keeps running. WithRedirectStandardOutput=trueplusBeginOutputReadLine/BeginErrorReadLineactive, the .NET runtime drops the managedProcesshandle shortly after the launcher exits. The next access toprocess.HasExited/process.ExitCodethen throwsInvalidOperationException: No process is associated with this object.In
AvdManager.StartAvdAsync, the 3-second post-launch health check did exactly that unguarded access, and the generic catch below wrapped it into theE2106message.The upstream
EmulatorRunner.BootEmulatorAsyncalready handles this case:Our fire-and-forget
startpath did not.Fix
Wrap the
HasExited/ExitCodecheck in a try/catch forInvalidOperationException. If the handle has been reaped we assume the launcher forked cleanly (matching upstream behavior). Only a confirmed non-zero exit code now surfaces as an error.Validation
Error [E2106]: ... No process is associated with this objectevery time, even though the emulator was up.✓ Started AVD: MAUI_Emulator_API_36.1in 3.2s,adb devicesshowsemulator-5554 device.EmulatorStartTimingTests(2 tests) pass on Windows in ~8s.Also includes the
EmulatorStartTimingTestsguardrail from the #122 investigation (opt-in viaMAUI_TEST_AVD_NAME) — separately measures theadb says booted→start command returnsgap so any future regression shows up as a test diff.Related