This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add EmulatorRunner for emulator CLI operations - #284

Merged
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner
Mar 19, 2026
Merged

Add EmulatorRunner for emulator CLI operations#284
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner

Conversation

@rmarinho

@rmarinhormarinho commented Feb 23, 2026

Copy link
Copy Markdown
Member

EmulatorRunner: High-level emulator lifecycle management

Adds EmulatorRunner — a managed wrapper over the Android SDK emulator CLI binary, following the same pattern as AdbRunner and AvdManagerRunner.

API Surface

MethodDescription
LaunchEmulator(avdName, options?)Fire-and-forget: starts an emulator process and returns the Process handle. Caller owns the process lifetime. Validates avdName is non-empty.
BootEmulatorAsync(avdName, adb, options?, token?)Full lifecycle: checks if device is already online → checks if emulator process is running → launches emulator → polls adb devices until boot completes or timeout. Returns EmulatorBootResult with status and serial. Disposes Process handle on success (emulator keeps running).
ListAvdNamesAsync(token?)Lists available AVD names via emulator -list-avds. Checks exit code for failures.

Key Design Decisions

  • Naming: LaunchEmulator (fire-and-forget) vs BootEmulatorAsync (full lifecycle) — clear verb distinction matching the emulator domain
  • Kept EmulatorRunner name (not AvdRunner) — follows convention of naming runners after their CLI binary (emulatorEmulatorRunner, adbAdbRunner)
  • Process handle management: LaunchEmulator returns Process (caller-owned); BootEmulatorAsync disposes handle on success (emulator keeps running as detached process), kills+disposes on failure/timeout
  • Pipe draining: LaunchEmulator calls BeginOutputReadLine()/BeginErrorReadLine() after Start() to prevent OS pipe buffer deadlock
  • TryKillProcess: Instance method, uses typed catch (Exception ex) with logger for diagnostics, uses Kill(entireProcessTree: true) on .NET 5+

AdbRunner Enhancements (in this PR)

  • Added optional Action? logger parameter to constructor
  • RunShellCommandAsync(serial, command, ct) — single-string shell command (⚠️ device shell interprets it — documented in XML doc)
  • RunShellCommandAsync(serial, command, args, ct)NEW: structured overload that passes args as separate tokens, bypassing device shell interpretation via exec(). Safer for dynamic input.
  • GetShellPropertyAsync returns first non-empty line (for getprop queries)
  • Shell methods log stderr via logger on non-zero exit codes
  • Fixed RS0026/RS0027: only the most-params overload has optional CancellationToken
  • AVD name detection fix: GetEmulatorAvdNameAsync now falls back to adb shell getprop ro.boot.qemu.avd_name when adb emu avd name returns empty (observed returning empty on some adb/emulator v36 combinations)

Models

  • EmulatorBootOptions — configurable timeout (default 120s), poll interval (default 2s), cold boot, extra args (IEnumerable?)
  • EmulatorBootResult — immutable record with init-only properties: Status (enum), Serial, Message. Statuses: Success, AlreadyRunning, Timeout, Error

Bug Fix: AVD Name Detection on Emulator v36+

The adb emu avd name console command can return empty output on some adb/emulator version combinations (observed with adb v36). This caused BootEmulatorAsync to never match the running emulator by AVD name, resulting in a perpetual polling loop and eventual timeout.

Root cause: GetEmulatorAvdNameAsync relied solely on adb -s <serial> emu avd name. On some adb/emulator version combinations this command silently returns empty output (exit code 0, no content). The exact cause is unclear but the getprop fallback provides reliable AVD name resolution regardless.

Fix: Added fallback to adb shell getprop ro.boot.qemu.avd_name, which reads the boot property set by the emulator kernel. This property is always available via the standard adb shell interface and does not depend on the emulator console protocol.

Verified: BootEmulatorAsync now completes in ~3s (was timing out at 120s) on emulator v36.4.9 with API 36 image.

Consumer PR

  • dotnet/android #10949 — replaces BootAndroidEmulator MSBuild task (~454 lines) with a ~180-line wrapper delegating to EmulatorRunner.BootEmulatorAsync()

Tests (24 EmulatorRunner + 9 AdbRunner = 33 total)

EmulatorRunner (24):

  • Parse emulator -list-avds output (empty, single, multiple, blank lines, Windows newlines) — 4 tests
  • Constructor validation (null/empty/whitespace tool path) — 3 tests
  • LaunchEmulator argument validation (null, empty, whitespace AVD name) — 3 tests
  • BootEmulatorAsync lifecycle: already online device, already running AVD, successful boot after polling, timeout, launch failure, cancellation token — 6 tests
  • BootEmulatorAsync validation: invalid timeout, invalid poll interval, null AdbRunner, empty device name — 4 tests
  • Ported from dotnet/android BootAndroidEmulatorTests: physical device passthrough, AdditionalArgs forwarding, ColdBoot flag, cancellation abort — 4 tests

AdbRunner (9):

  • FirstNonEmptyLine parsing (null, empty, whitespace, single value, multiline, mixed) — 9 tests

Review Feedback Addressed

  • LaunchEmulator validates avdName parameter (throws ArgumentException)
  • LaunchEmulator drains stdout/stderr pipes via BeginOutputReadLine()/BeginErrorReadLine()
  • RunShellCommandAsync returns full stdout (not just first line)
  • ✅ Added structured RunShellCommandAsync overload (no shell interpretation)
  • ✅ Added 12 new unit tests (LaunchEmulator validation + FirstNonEmptyLine parsing)
  • ✅ Shell methods log stderr via logger on failure
  • ✅ Removed TOCTOU HasExited guard from TryKillProcess
  • ✅ Process handle disposed on successful boot (no handle leak)
  • ListAvdNamesAsync checks exit code
  • TryKillProcess uses typed catch (Exception ex) with logging
  • RunShellCommandAsync XML doc warns about shell interpretation
  • ✅ Fixed RS0026/RS0027 PublicAPI analyzer warnings
  • EmulatorBootResult uses init-only properties (immutable record)
  • ✅ Ported 6 additional tests from dotnet/android BootAndroidEmulatorTests
  • ✅ Fixed AVD name detection for emulator v36+ (getprop fallback)

CopilotAI review requested due to automatic review settings February 23, 2026 17:39

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 adds a new EmulatorRunner to Xamarin.Android.Tools.AndroidSdk intended to wrap Android emulator CLI operations, alongside new shared infrastructure for running Android SDK command-line tools with environment setup and result modeling.

Changes:

  • Added EmulatorRunner to start an AVD, stop an emulator, and list available AVD names.
  • Added AndroidToolRunner utility to run SDK tools sync/async (with timeouts) and to start long-running background processes.
  • Added AndroidEnvironmentHelper and ToolRunnerResult / ToolRunnerResult<T> to standardize tool environment and execution results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

FileDescription
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.csIntroduces emulator wrapper methods (start/stop/list AVDs) built on the tool runner infrastructure.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.csAdds process execution helpers (sync/async + background) with timeout/output capture.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.csAdds env var setup and mapping helpers (ABI/API/tag display names).
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.csAdds a shared result model for tool execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
@rmarinhormarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho
rmarinho requested a review from RedthFebruary 23, 2026 17:51
@jonathanpeppers

Copy link
Copy Markdown
Member

I'd like to get the System.Diagnostics.Process code unified like mentioned here:

rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #284 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:
- Methods now throw InvalidOperationException on failure
- Uses ProcessUtils.RunToolAsync() and StartToolBackground()
- Removed complex ToolRunnerResult wrapper types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from f1aa44f to 826d4aaCompareFebruary 24, 2026 14:15
rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #283/#284 feedback to use existing ProcessUtils.
Simplifies API by throwing exceptions on failure instead of
returning result types with error states.
Changes:
- AdbRunner: Simplified using ProcessUtils.RunToolAsync()
- EmulatorRunner: Uses ProcessUtils.StartToolBackground()
- Removed duplicate AndroidDeviceInfo from Models directory
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 2 times, most recently from 39617c8 to 5268300CompareFebruary 24, 2026 19:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 5 times, most recently from 1b10889 to ee31e4bCompareMarch 3, 2026 14:36
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from ee31e4b to 3a788bbCompareMarch 3, 2026 18:23
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Review feedback addressed — commit references

FeedbackCommitDetails
Port BootAndroidEmulator logic from dotnet/android0088e39BootAndWaitAsync with 3-phase boot, GetShellPropertyAsync, RunShellCommandAsync, 6 new tests

New files:

  • Models/EmulatorBootResult.cs, Models/EmulatorBootOptions.cs
  • Tests: 6 async boot scenarios ported from BootAndroidEmulatorTests.cs

Modified:

  • Runners/EmulatorRunner.csBootAndWaitAsync, FindRunningAvdSerial, WaitForFullBootAsync
  • Runners/AdbRunner.csGetShellPropertyAsync, RunShellCommandAsync (+ ListDevicesAsync made virtual for testability)

Draft dotnet/android consumer PR to follow.

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 AI Review Summary

Found 7 issues: 1 correctness, 2 error handling, 1 API design, 1 code duplication, 1 code organization, 1 naming.

  • Correctness: StartAvd redirects stdout/stderr but never drains the pipes — OS buffer fill will deadlock the emulator process (EmulatorRunner.cs:74)
  • API design: AdditionalArgs is a single string — will be treated as one argument by ProcessUtils.ArgumentList, breaking multi-token args like -gpu swiftshader_indirect (EmulatorBootOptions.cs:14)
  • Error handling: ListDevicesAsync ignores the exit code from ProcessUtils.StartProcess while sibling methods in AvdManagerRunner check it consistently (AdbRunner.cs:72)
  • Code duplication: AvdManagerRunner.AvdManagerPath reimplements the cmdline-tools version scanning that ProcessUtils.FindCmdlineTool (added in this same PR) already provides (AvdManagerRunner.cs:33)
  • Error handling: Bare catch { } swallows all exceptions without capturing them (AdbRunner.cs:107)

👍 Solid three-phase boot logic ported faithfully from dotnet/android. Good use of virtual on AdbRunner methods to enable clean test mocking. Thorough test coverage with 13+ unit tests covering parsing, edge cases, and the full boot flow. Nice extraction of AndroidEnvironmentHelper for shared env var setup.


This review was generated by the android-tools-reviewer skill based on review guidelines established by @jonathanpeppers.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
jonathanpeppers added a commit that referenced this pull request Mar 4, 2026
This skill let's you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
rmarinho added a commit to dotnet/android that referenced this pull request Mar 16, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

@jonathanpeppers Here's the dotnet/android consumer PR you requested: dotnet/android#10948

It replaces the 454-line BootAndroidEmulator task with a ~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync(). Same MSBuild interface, same error codes (XA0143/XA0145), but all the process management and polling logic is now in the shared library.

The PR is in draft since it depends on this PR (#284) merging first — the submodule currently points to feature/emulator-runner.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Note: the consumer PR was recreated after a branch rename — the correct link is now dotnet/android#10949 (the previous #10948 was auto-closed).

Port additional test coverage from dotnet/android PR #10949:
- AlreadyOnlinePhysicalDevice: physical device serial passthrough
- AdditionalArgs_PassedToLaunchEmulator: verify extra args reach process
- CancellationToken_AbortsBoot: cancellation during polling phase
- ColdBoot_PassesNoSnapshotLoad: verify -no-snapshot-load flag
- BootEmulatorAsync_NullAdbRunner_Throws: null guard validation
- BootEmulatorAsync_EmptyDeviceName_Throws: empty string guard
Total EmulatorRunner test count: 24 (18 existing + 6 new)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried it locally, but it errors:

Image

Is anything different from the code <BootAndroidEmulator/> had before?

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
The 'adb emu avd name' console command returns empty output on emulator
v36+ due to gRPC authentication requirements. This causes
BootEmulatorAsync to never match the running emulator by AVD name,
resulting in a perpetual polling loop and eventual timeout.
Add a fallback to 'adb shell getprop ro.boot.qemu.avd_name' which reads
the boot property set by the emulator kernel. This property is always
available and doesn't require console authentication.
The fix benefits all consumers of ListDevicesAsync/GetEmulatorAvdNameAsync,
not just BootEmulatorAsync.
Verified locally: BootEmulatorAsync now completes in ~3s (was timing out
at 120s) on emulator v36.4.9 with API 36 image.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔬 Definitive Proof: adb emu avd name Bug on Emulator v36+

Following up on the AVD name detection fix — I did thorough live testing on a properly running emulator (v36.4.9, API 36, arm64) to confirm the behavior.

Test Environment

  • Emulator: v36.4.9.0 (build 14788078), AVD MAUI_Emulator_API_36
  • ADB: v37.0.0-14910828
  • macOS: Darwin 25.3.0 (arm64, Apple M3 Pro)
  • Emulator fully booted: sys.boot_completed=1, adb devices shows device state

Results

MethodResult
adb -s emulator-5554 emu avd nameEMPTY (exit code 0, no output)
adb shell getprop ro.boot.qemu.avd_nameMAUI_Emulator_API_36
echo "avd name" | nc localhost 5554 (raw telnet)MAUI_Emulator_API_36
Console port 5554OPEN (nc -z succeeds)

Analysis

  1. The console port IS accessible — raw telnet to 5554 returns the AVD name correctly
  2. adb emu returns emptyadb uses a different protocol path than raw telnet, and something changed in emulator v36 that breaks it
  3. The emulator warns: The emulator now requires a signed jwt token for gRPC access! — while gRPC (port 8554) differs from telnet console (port 5554), this may affect how adb authenticates to the console

Impact on dotnet/android

The original BootAndroidEmulator.GetRunningAvdName() on main uses the exact same command:

MonoAndroidHelper.RunProcess(adbPath,$"-s {serial} emu avd name", ...);

This means FindRunningEmulatorForAvd would fail to match the AVD → WaitForEmulatorOnline would poll indefinitely → timeout after 120s. This is exactly the bug @jonathanpeppers reported.

Fix Validation

Our getprop ro.boot.qemu.avd_name fallback in AdbRunner.GetEmulatorAvdNameAsync:

  • Completes in 13ms (vs infinite timeout)
  • BootEmulatorAsync end-to-end: 2.8 seconds (vs 120s timeout)
  • All 259 existing tests pass

@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔄 Correction: ADB v37 Regression (not emulator v36 issue)

After deeper investigation, the root cause is more specific:

The Real Issue: ADB v37.0.0 broke adb emu commands

Platform-tools 37.0.0 (ADB 37.0.0-14910828) returns empty output for ALL adb emu subcommands — not just avd name. This is a regression from ADB 36.x where these commands work fine.

I verified with a .NET test program using bothMonoAndroidHelper.RunProcess-style (event-based) and ProcessUtils.StartProcess-style (stream-based) approaches — both get identical empty results. It's not a process execution issue.

Why dotnet/android CI works today

dotnet/android's Configuration.props pins XAPlatformToolsVersion to 36.0.0, so CI uses ADB 36.x where adb emu avd name works correctly. Users who manually upgrade to platform-tools 37 will hit this bug.

The getprop fallback is forward-compatible

The getprop ro.boot.qemu.avd_name fallback works regardless of ADB version, making EmulatorRunner robust against both the current ADB 37 regression and any future changes to the console protocol.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

📋 Research: ADB v37.0.0 adb emu Regression — Evidence & References

Following up on the correction comment with formal evidence supporting the getprop fallback fix.

1. Platform-Tools 37.0.0 is a stable public release

  • Google's official download servers host it:
    • https://dl.google.com/android/repository/platform-tools_r37.0.0-{win,linux,darwin}.zip
  • GitHub Actions macOS 15 runner images ship with it (image version 20260303):
    • Android SDK Platform-Tools | 37.0.0 (source)
    • Same emulator version we tested: Android Emulator | 36.4.9
  • Listed as "Latest Stable Release" in ADB-Explorer's version catalog
  • Google's official release notes at developer.android.com haven't been updated past 36.0.2 yet — the version is released but undocumented

2. Known Google Bug: adb emu returns empty

  • Google Issue Tracker #251776353: "adb avd id returns empty"
  • Originally reported on Intel Macs (platform-tools 34.x), still open/unresolved
  • Our testing confirms it now affects Apple Silicon (M3 Pro) with ADB 37.0.0
  • ALL adb emu subcommands return empty (not just avd name/id) — the entire console-via-ADB pathway is broken
  • Raw telnet to the console port (5554) works perfectly — proving the emulator console itself is fine

3. Why dotnet/android CI is not affected (yet)

  • Configuration.props pins XAPlatformToolsVersion=36.0.0 → CI uses ADB 36.x where adb emu works
  • Any CI/CD using macos-15 GitHub Actions runners WILL be affected — they already have pt 37.0.0
  • Developers using Android Studio (which auto-updates SDK components) will also hit this

4. The getprop fallback is the correct fix

  • getprop ro.boot.qemu.avd_name uses adb shell (standard ADB transport), not the emulator console protocol
  • Works on all ADB versions (35.x, 36.x, 37.x) — we verified this
  • Avoids the broken console-via-ADB pathway entirely
  • Available since Android API 21+ (emulator sets ro.boot.qemu.avd_name at boot)
  • Completes in ~13ms vs 120s timeout with broken adb emu

Summary

EvidenceFinding
Platform-tools 37.0.0✅ Stable, public release on dl.google.com
GitHub Actions macOS 15✅ Ships with pt 37.0.0 + emulator 36.4.9
Google Issue Tracker#251776353 — known open bug
dotnet/android CIUses pt 36.0.0 (pinned) — not yet affected
getprop fallbackWorks on ALL ADB versions — forward-compatible fix

rmarinhoand others added 2 commits March 17, 2026 12:22
Changes:
- Convert EmulatorBootOptions from class to record with init properties
- Change AdditionalArgs from IEnumerable to List for collection initializers
- Remove REMOVED lines from PublicAPI.Unshipped.txt files
- Remove local Log function, inline logger calls
- Simplify while loop condition in WaitForFullBootAsync
- Remove entireProcessTree from process termination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace logger?.Invoke with logger.Invoke using a static
NullLogger no-op delegate in EmulatorRunner, AdbRunner, and
AvdManagerRunner. The constructor assigns logger ?? NullLogger
so the field is never null. Static methods use logger ??= NullLogger
at entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
rmarinhoand others added 4 commits March 18, 2026 09:09
Add structured error classification enum (None, LaunchFailed, Timeout,
Cancelled, Unknown) so consumers can switch on ErrorKind instead of
parsing ErrorMessage strings. Set ErrorKind on all BootEmulatorAsync
return paths.
Addresses review feedback from dotnet/android#10949.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract shared NullLogger to RunnerDefaults utility class
- Remove duplicate NullLogger from AdbRunner, EmulatorRunner, AvdManagerRunner
Addresses review feedback from @jonathanpeppers on PR #284.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment incorrectly claimed the getprop fallback was needed because
'emulator 36+ requires auth for console commands'. After reviewing the
actual adb source code (console.cpp), adb emu handles console auth
automatically — it reads ~/.emulator_console_auth_token and sends it
before any command. This has been the case since ~2016.
The real reason for the fallback is that 'adb emu avd name' can return
empty output on some adb/emulator version combinations (observed with
adb v36). Updated both the XML doc and inline comment to accurately
describe the issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add early process exit detection in BootEmulatorAsync boot polling loop.
Previously, if the emulator failed immediately (e.g., insufficient disk
space, missing AVD), the full 300s timeout was wasted before reporting.
On macOS, the emulator binary forks the real QEMU process and the parent
exits with code 0 immediately. Only non-zero exit codes are treated as
immediate failures; exit code 0 continues polling since the real emulator
runs as a separate process.
Context: dotnet/android#10965
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The build failures is an issue rerunning -- attempt number need to be on artifacts (I'm fixing separately).

Merging shortly.

@jonathanpeppers
jonathanpeppers merged commit 39995cf into mainMar 19, 2026
1 of 2 checks passed
@jonathanpeppers
jonathanpeppers deleted the feature/emulator-runner branch March 19, 2026 13:05
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 20, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Mar 23, 2026
* Use shared EmulatorRunner from android-tools for BootAndroidEmulator
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
### Restore XA0144 for unexpected emulator errors
Update XA0144 message format to accept the ErrorMessage from
EmulatorRunner directly. The default switch case (Unknown and
future error kinds) now uses XA0144 with the full error details
instead of the misleading timeout message XA0145.
Error code mapping:
- XA0143: Launch failed (couldn't start emulator)
- XA0144: Unexpected exit/error (process exited, unknown errors)
- XA0145: Boot timeout (didn't finish in time)
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jul 13, 2026
This skill lets you say:
review this PR: dotnet/android-tools#284
Some example code reviews:
* dotnet/android-tools#283 (review)
* dotnet/android-tools#284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@rmarinho@jonathanpeppers@mattleibow
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add EmulatorRunner for emulator CLI operations - #284

Merged
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner
Mar 19, 2026
Merged

Add EmulatorRunner for emulator CLI operations#284
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner

Conversation

@rmarinho

@rmarinhormarinho commented Feb 23, 2026

Copy link
Copy Markdown
Member

EmulatorRunner: High-level emulator lifecycle management

Adds EmulatorRunner — a managed wrapper over the Android SDK emulator CLI binary, following the same pattern as AdbRunner and AvdManagerRunner.

API Surface

MethodDescription
LaunchEmulator(avdName, options?)Fire-and-forget: starts an emulator process and returns the Process handle. Caller owns the process lifetime. Validates avdName is non-empty.
BootEmulatorAsync(avdName, adb, options?, token?)Full lifecycle: checks if device is already online → checks if emulator process is running → launches emulator → polls adb devices until boot completes or timeout. Returns EmulatorBootResult with status and serial. Disposes Process handle on success (emulator keeps running).
ListAvdNamesAsync(token?)Lists available AVD names via emulator -list-avds. Checks exit code for failures.

Key Design Decisions

  • Naming: LaunchEmulator (fire-and-forget) vs BootEmulatorAsync (full lifecycle) — clear verb distinction matching the emulator domain
  • Kept EmulatorRunner name (not AvdRunner) — follows convention of naming runners after their CLI binary (emulatorEmulatorRunner, adbAdbRunner)
  • Process handle management: LaunchEmulator returns Process (caller-owned); BootEmulatorAsync disposes handle on success (emulator keeps running as detached process), kills+disposes on failure/timeout
  • Pipe draining: LaunchEmulator calls BeginOutputReadLine()/BeginErrorReadLine() after Start() to prevent OS pipe buffer deadlock
  • TryKillProcess: Instance method, uses typed catch (Exception ex) with logger for diagnostics, uses Kill(entireProcessTree: true) on .NET 5+

AdbRunner Enhancements (in this PR)

  • Added optional Action? logger parameter to constructor
  • RunShellCommandAsync(serial, command, ct) — single-string shell command (⚠️ device shell interprets it — documented in XML doc)
  • RunShellCommandAsync(serial, command, args, ct)NEW: structured overload that passes args as separate tokens, bypassing device shell interpretation via exec(). Safer for dynamic input.
  • GetShellPropertyAsync returns first non-empty line (for getprop queries)
  • Shell methods log stderr via logger on non-zero exit codes
  • Fixed RS0026/RS0027: only the most-params overload has optional CancellationToken
  • AVD name detection fix: GetEmulatorAvdNameAsync now falls back to adb shell getprop ro.boot.qemu.avd_name when adb emu avd name returns empty (observed returning empty on some adb/emulator v36 combinations)

Models

  • EmulatorBootOptions — configurable timeout (default 120s), poll interval (default 2s), cold boot, extra args (IEnumerable?)
  • EmulatorBootResult — immutable record with init-only properties: Status (enum), Serial, Message. Statuses: Success, AlreadyRunning, Timeout, Error

Bug Fix: AVD Name Detection on Emulator v36+

The adb emu avd name console command can return empty output on some adb/emulator version combinations (observed with adb v36). This caused BootEmulatorAsync to never match the running emulator by AVD name, resulting in a perpetual polling loop and eventual timeout.

Root cause: GetEmulatorAvdNameAsync relied solely on adb -s <serial> emu avd name. On some adb/emulator version combinations this command silently returns empty output (exit code 0, no content). The exact cause is unclear but the getprop fallback provides reliable AVD name resolution regardless.

Fix: Added fallback to adb shell getprop ro.boot.qemu.avd_name, which reads the boot property set by the emulator kernel. This property is always available via the standard adb shell interface and does not depend on the emulator console protocol.

Verified: BootEmulatorAsync now completes in ~3s (was timing out at 120s) on emulator v36.4.9 with API 36 image.

Consumer PR

  • dotnet/android #10949 — replaces BootAndroidEmulator MSBuild task (~454 lines) with a ~180-line wrapper delegating to EmulatorRunner.BootEmulatorAsync()

Tests (24 EmulatorRunner + 9 AdbRunner = 33 total)

EmulatorRunner (24):

  • Parse emulator -list-avds output (empty, single, multiple, blank lines, Windows newlines) — 4 tests
  • Constructor validation (null/empty/whitespace tool path) — 3 tests
  • LaunchEmulator argument validation (null, empty, whitespace AVD name) — 3 tests
  • BootEmulatorAsync lifecycle: already online device, already running AVD, successful boot after polling, timeout, launch failure, cancellation token — 6 tests
  • BootEmulatorAsync validation: invalid timeout, invalid poll interval, null AdbRunner, empty device name — 4 tests
  • Ported from dotnet/android BootAndroidEmulatorTests: physical device passthrough, AdditionalArgs forwarding, ColdBoot flag, cancellation abort — 4 tests

AdbRunner (9):

  • FirstNonEmptyLine parsing (null, empty, whitespace, single value, multiline, mixed) — 9 tests

Review Feedback Addressed

  • LaunchEmulator validates avdName parameter (throws ArgumentException)
  • LaunchEmulator drains stdout/stderr pipes via BeginOutputReadLine()/BeginErrorReadLine()
  • RunShellCommandAsync returns full stdout (not just first line)
  • ✅ Added structured RunShellCommandAsync overload (no shell interpretation)
  • ✅ Added 12 new unit tests (LaunchEmulator validation + FirstNonEmptyLine parsing)
  • ✅ Shell methods log stderr via logger on failure
  • ✅ Removed TOCTOU HasExited guard from TryKillProcess
  • ✅ Process handle disposed on successful boot (no handle leak)
  • ListAvdNamesAsync checks exit code
  • TryKillProcess uses typed catch (Exception ex) with logging
  • RunShellCommandAsync XML doc warns about shell interpretation
  • ✅ Fixed RS0026/RS0027 PublicAPI analyzer warnings
  • EmulatorBootResult uses init-only properties (immutable record)
  • ✅ Ported 6 additional tests from dotnet/android BootAndroidEmulatorTests
  • ✅ Fixed AVD name detection for emulator v36+ (getprop fallback)

CopilotAI review requested due to automatic review settings February 23, 2026 17:39

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 adds a new EmulatorRunner to Xamarin.Android.Tools.AndroidSdk intended to wrap Android emulator CLI operations, alongside new shared infrastructure for running Android SDK command-line tools with environment setup and result modeling.

Changes:

  • Added EmulatorRunner to start an AVD, stop an emulator, and list available AVD names.
  • Added AndroidToolRunner utility to run SDK tools sync/async (with timeouts) and to start long-running background processes.
  • Added AndroidEnvironmentHelper and ToolRunnerResult / ToolRunnerResult<T> to standardize tool environment and execution results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

FileDescription
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.csIntroduces emulator wrapper methods (start/stop/list AVDs) built on the tool runner infrastructure.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.csAdds process execution helpers (sync/async + background) with timeout/output capture.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.csAdds env var setup and mapping helpers (ABI/API/tag display names).
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.csAdds a shared result model for tool execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
@rmarinhormarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho
rmarinho requested a review from RedthFebruary 23, 2026 17:51
@jonathanpeppers

Copy link
Copy Markdown
Member

I'd like to get the System.Diagnostics.Process code unified like mentioned here:

rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #284 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:
- Methods now throw InvalidOperationException on failure
- Uses ProcessUtils.RunToolAsync() and StartToolBackground()
- Removed complex ToolRunnerResult wrapper types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from f1aa44f to 826d4aaCompareFebruary 24, 2026 14:15
rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #283/#284 feedback to use existing ProcessUtils.
Simplifies API by throwing exceptions on failure instead of
returning result types with error states.
Changes:
- AdbRunner: Simplified using ProcessUtils.RunToolAsync()
- EmulatorRunner: Uses ProcessUtils.StartToolBackground()
- Removed duplicate AndroidDeviceInfo from Models directory
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 2 times, most recently from 39617c8 to 5268300CompareFebruary 24, 2026 19:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 5 times, most recently from 1b10889 to ee31e4bCompareMarch 3, 2026 14:36
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from ee31e4b to 3a788bbCompareMarch 3, 2026 18:23
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Review feedback addressed — commit references

FeedbackCommitDetails
Port BootAndroidEmulator logic from dotnet/android0088e39BootAndWaitAsync with 3-phase boot, GetShellPropertyAsync, RunShellCommandAsync, 6 new tests

New files:

  • Models/EmulatorBootResult.cs, Models/EmulatorBootOptions.cs
  • Tests: 6 async boot scenarios ported from BootAndroidEmulatorTests.cs

Modified:

  • Runners/EmulatorRunner.csBootAndWaitAsync, FindRunningAvdSerial, WaitForFullBootAsync
  • Runners/AdbRunner.csGetShellPropertyAsync, RunShellCommandAsync (+ ListDevicesAsync made virtual for testability)

Draft dotnet/android consumer PR to follow.

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 AI Review Summary

Found 7 issues: 1 correctness, 2 error handling, 1 API design, 1 code duplication, 1 code organization, 1 naming.

  • Correctness: StartAvd redirects stdout/stderr but never drains the pipes — OS buffer fill will deadlock the emulator process (EmulatorRunner.cs:74)
  • API design: AdditionalArgs is a single string — will be treated as one argument by ProcessUtils.ArgumentList, breaking multi-token args like -gpu swiftshader_indirect (EmulatorBootOptions.cs:14)
  • Error handling: ListDevicesAsync ignores the exit code from ProcessUtils.StartProcess while sibling methods in AvdManagerRunner check it consistently (AdbRunner.cs:72)
  • Code duplication: AvdManagerRunner.AvdManagerPath reimplements the cmdline-tools version scanning that ProcessUtils.FindCmdlineTool (added in this same PR) already provides (AvdManagerRunner.cs:33)
  • Error handling: Bare catch { } swallows all exceptions without capturing them (AdbRunner.cs:107)

👍 Solid three-phase boot logic ported faithfully from dotnet/android. Good use of virtual on AdbRunner methods to enable clean test mocking. Thorough test coverage with 13+ unit tests covering parsing, edge cases, and the full boot flow. Nice extraction of AndroidEnvironmentHelper for shared env var setup.


This review was generated by the android-tools-reviewer skill based on review guidelines established by @jonathanpeppers.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
jonathanpeppers added a commit that referenced this pull request Mar 4, 2026
This skill let's you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
rmarinho added a commit to dotnet/android that referenced this pull request Mar 16, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

@jonathanpeppers Here's the dotnet/android consumer PR you requested: dotnet/android#10948

It replaces the 454-line BootAndroidEmulator task with a ~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync(). Same MSBuild interface, same error codes (XA0143/XA0145), but all the process management and polling logic is now in the shared library.

The PR is in draft since it depends on this PR (#284) merging first — the submodule currently points to feature/emulator-runner.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Note: the consumer PR was recreated after a branch rename — the correct link is now dotnet/android#10949 (the previous #10948 was auto-closed).

Port additional test coverage from dotnet/android PR #10949:
- AlreadyOnlinePhysicalDevice: physical device serial passthrough
- AdditionalArgs_PassedToLaunchEmulator: verify extra args reach process
- CancellationToken_AbortsBoot: cancellation during polling phase
- ColdBoot_PassesNoSnapshotLoad: verify -no-snapshot-load flag
- BootEmulatorAsync_NullAdbRunner_Throws: null guard validation
- BootEmulatorAsync_EmptyDeviceName_Throws: empty string guard
Total EmulatorRunner test count: 24 (18 existing + 6 new)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried it locally, but it errors:

Image

Is anything different from the code <BootAndroidEmulator/> had before?

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
The 'adb emu avd name' console command returns empty output on emulator
v36+ due to gRPC authentication requirements. This causes
BootEmulatorAsync to never match the running emulator by AVD name,
resulting in a perpetual polling loop and eventual timeout.
Add a fallback to 'adb shell getprop ro.boot.qemu.avd_name' which reads
the boot property set by the emulator kernel. This property is always
available and doesn't require console authentication.
The fix benefits all consumers of ListDevicesAsync/GetEmulatorAvdNameAsync,
not just BootEmulatorAsync.
Verified locally: BootEmulatorAsync now completes in ~3s (was timing out
at 120s) on emulator v36.4.9 with API 36 image.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔬 Definitive Proof: adb emu avd name Bug on Emulator v36+

Following up on the AVD name detection fix — I did thorough live testing on a properly running emulator (v36.4.9, API 36, arm64) to confirm the behavior.

Test Environment

  • Emulator: v36.4.9.0 (build 14788078), AVD MAUI_Emulator_API_36
  • ADB: v37.0.0-14910828
  • macOS: Darwin 25.3.0 (arm64, Apple M3 Pro)
  • Emulator fully booted: sys.boot_completed=1, adb devices shows device state

Results

MethodResult
adb -s emulator-5554 emu avd nameEMPTY (exit code 0, no output)
adb shell getprop ro.boot.qemu.avd_nameMAUI_Emulator_API_36
echo "avd name" | nc localhost 5554 (raw telnet)MAUI_Emulator_API_36
Console port 5554OPEN (nc -z succeeds)

Analysis

  1. The console port IS accessible — raw telnet to 5554 returns the AVD name correctly
  2. adb emu returns emptyadb uses a different protocol path than raw telnet, and something changed in emulator v36 that breaks it
  3. The emulator warns: The emulator now requires a signed jwt token for gRPC access! — while gRPC (port 8554) differs from telnet console (port 5554), this may affect how adb authenticates to the console

Impact on dotnet/android

The original BootAndroidEmulator.GetRunningAvdName() on main uses the exact same command:

MonoAndroidHelper.RunProcess(adbPath,$"-s {serial} emu avd name", ...);

This means FindRunningEmulatorForAvd would fail to match the AVD → WaitForEmulatorOnline would poll indefinitely → timeout after 120s. This is exactly the bug @jonathanpeppers reported.

Fix Validation

Our getprop ro.boot.qemu.avd_name fallback in AdbRunner.GetEmulatorAvdNameAsync:

  • Completes in 13ms (vs infinite timeout)
  • BootEmulatorAsync end-to-end: 2.8 seconds (vs 120s timeout)
  • All 259 existing tests pass

@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔄 Correction: ADB v37 Regression (not emulator v36 issue)

After deeper investigation, the root cause is more specific:

The Real Issue: ADB v37.0.0 broke adb emu commands

Platform-tools 37.0.0 (ADB 37.0.0-14910828) returns empty output for ALL adb emu subcommands — not just avd name. This is a regression from ADB 36.x where these commands work fine.

I verified with a .NET test program using bothMonoAndroidHelper.RunProcess-style (event-based) and ProcessUtils.StartProcess-style (stream-based) approaches — both get identical empty results. It's not a process execution issue.

Why dotnet/android CI works today

dotnet/android's Configuration.props pins XAPlatformToolsVersion to 36.0.0, so CI uses ADB 36.x where adb emu avd name works correctly. Users who manually upgrade to platform-tools 37 will hit this bug.

The getprop fallback is forward-compatible

The getprop ro.boot.qemu.avd_name fallback works regardless of ADB version, making EmulatorRunner robust against both the current ADB 37 regression and any future changes to the console protocol.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

📋 Research: ADB v37.0.0 adb emu Regression — Evidence & References

Following up on the correction comment with formal evidence supporting the getprop fallback fix.

1. Platform-Tools 37.0.0 is a stable public release

  • Google's official download servers host it:
    • https://dl.google.com/android/repository/platform-tools_r37.0.0-{win,linux,darwin}.zip
  • GitHub Actions macOS 15 runner images ship with it (image version 20260303):
    • Android SDK Platform-Tools | 37.0.0 (source)
    • Same emulator version we tested: Android Emulator | 36.4.9
  • Listed as "Latest Stable Release" in ADB-Explorer's version catalog
  • Google's official release notes at developer.android.com haven't been updated past 36.0.2 yet — the version is released but undocumented

2. Known Google Bug: adb emu returns empty

  • Google Issue Tracker #251776353: "adb avd id returns empty"
  • Originally reported on Intel Macs (platform-tools 34.x), still open/unresolved
  • Our testing confirms it now affects Apple Silicon (M3 Pro) with ADB 37.0.0
  • ALL adb emu subcommands return empty (not just avd name/id) — the entire console-via-ADB pathway is broken
  • Raw telnet to the console port (5554) works perfectly — proving the emulator console itself is fine

3. Why dotnet/android CI is not affected (yet)

  • Configuration.props pins XAPlatformToolsVersion=36.0.0 → CI uses ADB 36.x where adb emu works
  • Any CI/CD using macos-15 GitHub Actions runners WILL be affected — they already have pt 37.0.0
  • Developers using Android Studio (which auto-updates SDK components) will also hit this

4. The getprop fallback is the correct fix

  • getprop ro.boot.qemu.avd_name uses adb shell (standard ADB transport), not the emulator console protocol
  • Works on all ADB versions (35.x, 36.x, 37.x) — we verified this
  • Avoids the broken console-via-ADB pathway entirely
  • Available since Android API 21+ (emulator sets ro.boot.qemu.avd_name at boot)
  • Completes in ~13ms vs 120s timeout with broken adb emu

Summary

EvidenceFinding
Platform-tools 37.0.0✅ Stable, public release on dl.google.com
GitHub Actions macOS 15✅ Ships with pt 37.0.0 + emulator 36.4.9
Google Issue Tracker#251776353 — known open bug
dotnet/android CIUses pt 36.0.0 (pinned) — not yet affected
getprop fallbackWorks on ALL ADB versions — forward-compatible fix

rmarinhoand others added 2 commits March 17, 2026 12:22
Changes:
- Convert EmulatorBootOptions from class to record with init properties
- Change AdditionalArgs from IEnumerable to List for collection initializers
- Remove REMOVED lines from PublicAPI.Unshipped.txt files
- Remove local Log function, inline logger calls
- Simplify while loop condition in WaitForFullBootAsync
- Remove entireProcessTree from process termination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace logger?.Invoke with logger.Invoke using a static
NullLogger no-op delegate in EmulatorRunner, AdbRunner, and
AvdManagerRunner. The constructor assigns logger ?? NullLogger
so the field is never null. Static methods use logger ??= NullLogger
at entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
rmarinhoand others added 4 commits March 18, 2026 09:09
Add structured error classification enum (None, LaunchFailed, Timeout,
Cancelled, Unknown) so consumers can switch on ErrorKind instead of
parsing ErrorMessage strings. Set ErrorKind on all BootEmulatorAsync
return paths.
Addresses review feedback from dotnet/android#10949.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract shared NullLogger to RunnerDefaults utility class
- Remove duplicate NullLogger from AdbRunner, EmulatorRunner, AvdManagerRunner
Addresses review feedback from @jonathanpeppers on PR #284.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment incorrectly claimed the getprop fallback was needed because
'emulator 36+ requires auth for console commands'. After reviewing the
actual adb source code (console.cpp), adb emu handles console auth
automatically — it reads ~/.emulator_console_auth_token and sends it
before any command. This has been the case since ~2016.
The real reason for the fallback is that 'adb emu avd name' can return
empty output on some adb/emulator version combinations (observed with
adb v36). Updated both the XML doc and inline comment to accurately
describe the issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add early process exit detection in BootEmulatorAsync boot polling loop.
Previously, if the emulator failed immediately (e.g., insufficient disk
space, missing AVD), the full 300s timeout was wasted before reporting.
On macOS, the emulator binary forks the real QEMU process and the parent
exits with code 0 immediately. Only non-zero exit codes are treated as
immediate failures; exit code 0 continues polling since the real emulator
runs as a separate process.
Context: dotnet/android#10965
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The build failures is an issue rerunning -- attempt number need to be on artifacts (I'm fixing separately).

Merging shortly.

@jonathanpeppers
jonathanpeppers merged commit 39995cf into mainMar 19, 2026
1 of 2 checks passed
@jonathanpeppers
jonathanpeppers deleted the feature/emulator-runner branch March 19, 2026 13:05
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 20, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Mar 23, 2026
* Use shared EmulatorRunner from android-tools for BootAndroidEmulator
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
### Restore XA0144 for unexpected emulator errors
Update XA0144 message format to accept the ErrorMessage from
EmulatorRunner directly. The default switch case (Unknown and
future error kinds) now uses XA0144 with the full error details
instead of the misleading timeout message XA0145.
Error code mapping:
- XA0143: Launch failed (couldn't start emulator)
- XA0144: Unexpected exit/error (process exited, unknown errors)
- XA0145: Boot timeout (didn't finish in time)
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jul 13, 2026
This skill lets you say:
review this PR: dotnet/android-tools#284
Some example code reviews:
* dotnet/android-tools#283 (review)
* dotnet/android-tools#284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@rmarinho@jonathanpeppers@mattleibow
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add EmulatorRunner for emulator CLI operations - #284

Merged
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner
Mar 19, 2026
Merged

Add EmulatorRunner for emulator CLI operations#284
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner

Conversation

@rmarinho

@rmarinhormarinho commented Feb 23, 2026

Copy link
Copy Markdown
Member

EmulatorRunner: High-level emulator lifecycle management

Adds EmulatorRunner — a managed wrapper over the Android SDK emulator CLI binary, following the same pattern as AdbRunner and AvdManagerRunner.

API Surface

MethodDescription
LaunchEmulator(avdName, options?)Fire-and-forget: starts an emulator process and returns the Process handle. Caller owns the process lifetime. Validates avdName is non-empty.
BootEmulatorAsync(avdName, adb, options?, token?)Full lifecycle: checks if device is already online → checks if emulator process is running → launches emulator → polls adb devices until boot completes or timeout. Returns EmulatorBootResult with status and serial. Disposes Process handle on success (emulator keeps running).
ListAvdNamesAsync(token?)Lists available AVD names via emulator -list-avds. Checks exit code for failures.

Key Design Decisions

  • Naming: LaunchEmulator (fire-and-forget) vs BootEmulatorAsync (full lifecycle) — clear verb distinction matching the emulator domain
  • Kept EmulatorRunner name (not AvdRunner) — follows convention of naming runners after their CLI binary (emulatorEmulatorRunner, adbAdbRunner)
  • Process handle management: LaunchEmulator returns Process (caller-owned); BootEmulatorAsync disposes handle on success (emulator keeps running as detached process), kills+disposes on failure/timeout
  • Pipe draining: LaunchEmulator calls BeginOutputReadLine()/BeginErrorReadLine() after Start() to prevent OS pipe buffer deadlock
  • TryKillProcess: Instance method, uses typed catch (Exception ex) with logger for diagnostics, uses Kill(entireProcessTree: true) on .NET 5+

AdbRunner Enhancements (in this PR)

  • Added optional Action? logger parameter to constructor
  • RunShellCommandAsync(serial, command, ct) — single-string shell command (⚠️ device shell interprets it — documented in XML doc)
  • RunShellCommandAsync(serial, command, args, ct)NEW: structured overload that passes args as separate tokens, bypassing device shell interpretation via exec(). Safer for dynamic input.
  • GetShellPropertyAsync returns first non-empty line (for getprop queries)
  • Shell methods log stderr via logger on non-zero exit codes
  • Fixed RS0026/RS0027: only the most-params overload has optional CancellationToken
  • AVD name detection fix: GetEmulatorAvdNameAsync now falls back to adb shell getprop ro.boot.qemu.avd_name when adb emu avd name returns empty (observed returning empty on some adb/emulator v36 combinations)

Models

  • EmulatorBootOptions — configurable timeout (default 120s), poll interval (default 2s), cold boot, extra args (IEnumerable?)
  • EmulatorBootResult — immutable record with init-only properties: Status (enum), Serial, Message. Statuses: Success, AlreadyRunning, Timeout, Error

Bug Fix: AVD Name Detection on Emulator v36+

The adb emu avd name console command can return empty output on some adb/emulator version combinations (observed with adb v36). This caused BootEmulatorAsync to never match the running emulator by AVD name, resulting in a perpetual polling loop and eventual timeout.

Root cause: GetEmulatorAvdNameAsync relied solely on adb -s <serial> emu avd name. On some adb/emulator version combinations this command silently returns empty output (exit code 0, no content). The exact cause is unclear but the getprop fallback provides reliable AVD name resolution regardless.

Fix: Added fallback to adb shell getprop ro.boot.qemu.avd_name, which reads the boot property set by the emulator kernel. This property is always available via the standard adb shell interface and does not depend on the emulator console protocol.

Verified: BootEmulatorAsync now completes in ~3s (was timing out at 120s) on emulator v36.4.9 with API 36 image.

Consumer PR

  • dotnet/android #10949 — replaces BootAndroidEmulator MSBuild task (~454 lines) with a ~180-line wrapper delegating to EmulatorRunner.BootEmulatorAsync()

Tests (24 EmulatorRunner + 9 AdbRunner = 33 total)

EmulatorRunner (24):

  • Parse emulator -list-avds output (empty, single, multiple, blank lines, Windows newlines) — 4 tests
  • Constructor validation (null/empty/whitespace tool path) — 3 tests
  • LaunchEmulator argument validation (null, empty, whitespace AVD name) — 3 tests
  • BootEmulatorAsync lifecycle: already online device, already running AVD, successful boot after polling, timeout, launch failure, cancellation token — 6 tests
  • BootEmulatorAsync validation: invalid timeout, invalid poll interval, null AdbRunner, empty device name — 4 tests
  • Ported from dotnet/android BootAndroidEmulatorTests: physical device passthrough, AdditionalArgs forwarding, ColdBoot flag, cancellation abort — 4 tests

AdbRunner (9):

  • FirstNonEmptyLine parsing (null, empty, whitespace, single value, multiline, mixed) — 9 tests

Review Feedback Addressed

  • LaunchEmulator validates avdName parameter (throws ArgumentException)
  • LaunchEmulator drains stdout/stderr pipes via BeginOutputReadLine()/BeginErrorReadLine()
  • RunShellCommandAsync returns full stdout (not just first line)
  • ✅ Added structured RunShellCommandAsync overload (no shell interpretation)
  • ✅ Added 12 new unit tests (LaunchEmulator validation + FirstNonEmptyLine parsing)
  • ✅ Shell methods log stderr via logger on failure
  • ✅ Removed TOCTOU HasExited guard from TryKillProcess
  • ✅ Process handle disposed on successful boot (no handle leak)
  • ListAvdNamesAsync checks exit code
  • TryKillProcess uses typed catch (Exception ex) with logging
  • RunShellCommandAsync XML doc warns about shell interpretation
  • ✅ Fixed RS0026/RS0027 PublicAPI analyzer warnings
  • EmulatorBootResult uses init-only properties (immutable record)
  • ✅ Ported 6 additional tests from dotnet/android BootAndroidEmulatorTests
  • ✅ Fixed AVD name detection for emulator v36+ (getprop fallback)

CopilotAI review requested due to automatic review settings February 23, 2026 17:39

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 adds a new EmulatorRunner to Xamarin.Android.Tools.AndroidSdk intended to wrap Android emulator CLI operations, alongside new shared infrastructure for running Android SDK command-line tools with environment setup and result modeling.

Changes:

  • Added EmulatorRunner to start an AVD, stop an emulator, and list available AVD names.
  • Added AndroidToolRunner utility to run SDK tools sync/async (with timeouts) and to start long-running background processes.
  • Added AndroidEnvironmentHelper and ToolRunnerResult / ToolRunnerResult<T> to standardize tool environment and execution results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

FileDescription
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.csIntroduces emulator wrapper methods (start/stop/list AVDs) built on the tool runner infrastructure.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.csAdds process execution helpers (sync/async + background) with timeout/output capture.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.csAdds env var setup and mapping helpers (ABI/API/tag display names).
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.csAdds a shared result model for tool execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
@rmarinhormarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho
rmarinho requested a review from RedthFebruary 23, 2026 17:51
@jonathanpeppers

Copy link
Copy Markdown
Member

I'd like to get the System.Diagnostics.Process code unified like mentioned here:

rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #284 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:
- Methods now throw InvalidOperationException on failure
- Uses ProcessUtils.RunToolAsync() and StartToolBackground()
- Removed complex ToolRunnerResult wrapper types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from f1aa44f to 826d4aaCompareFebruary 24, 2026 14:15
rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #283/#284 feedback to use existing ProcessUtils.
Simplifies API by throwing exceptions on failure instead of
returning result types with error states.
Changes:
- AdbRunner: Simplified using ProcessUtils.RunToolAsync()
- EmulatorRunner: Uses ProcessUtils.StartToolBackground()
- Removed duplicate AndroidDeviceInfo from Models directory
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 2 times, most recently from 39617c8 to 5268300CompareFebruary 24, 2026 19:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 5 times, most recently from 1b10889 to ee31e4bCompareMarch 3, 2026 14:36
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from ee31e4b to 3a788bbCompareMarch 3, 2026 18:23
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Review feedback addressed — commit references

FeedbackCommitDetails
Port BootAndroidEmulator logic from dotnet/android0088e39BootAndWaitAsync with 3-phase boot, GetShellPropertyAsync, RunShellCommandAsync, 6 new tests

New files:

  • Models/EmulatorBootResult.cs, Models/EmulatorBootOptions.cs
  • Tests: 6 async boot scenarios ported from BootAndroidEmulatorTests.cs

Modified:

  • Runners/EmulatorRunner.csBootAndWaitAsync, FindRunningAvdSerial, WaitForFullBootAsync
  • Runners/AdbRunner.csGetShellPropertyAsync, RunShellCommandAsync (+ ListDevicesAsync made virtual for testability)

Draft dotnet/android consumer PR to follow.

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 AI Review Summary

Found 7 issues: 1 correctness, 2 error handling, 1 API design, 1 code duplication, 1 code organization, 1 naming.

  • Correctness: StartAvd redirects stdout/stderr but never drains the pipes — OS buffer fill will deadlock the emulator process (EmulatorRunner.cs:74)
  • API design: AdditionalArgs is a single string — will be treated as one argument by ProcessUtils.ArgumentList, breaking multi-token args like -gpu swiftshader_indirect (EmulatorBootOptions.cs:14)
  • Error handling: ListDevicesAsync ignores the exit code from ProcessUtils.StartProcess while sibling methods in AvdManagerRunner check it consistently (AdbRunner.cs:72)
  • Code duplication: AvdManagerRunner.AvdManagerPath reimplements the cmdline-tools version scanning that ProcessUtils.FindCmdlineTool (added in this same PR) already provides (AvdManagerRunner.cs:33)
  • Error handling: Bare catch { } swallows all exceptions without capturing them (AdbRunner.cs:107)

👍 Solid three-phase boot logic ported faithfully from dotnet/android. Good use of virtual on AdbRunner methods to enable clean test mocking. Thorough test coverage with 13+ unit tests covering parsing, edge cases, and the full boot flow. Nice extraction of AndroidEnvironmentHelper for shared env var setup.


This review was generated by the android-tools-reviewer skill based on review guidelines established by @jonathanpeppers.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
jonathanpeppers added a commit that referenced this pull request Mar 4, 2026
This skill let's you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
rmarinho added a commit to dotnet/android that referenced this pull request Mar 16, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

@jonathanpeppers Here's the dotnet/android consumer PR you requested: dotnet/android#10948

It replaces the 454-line BootAndroidEmulator task with a ~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync(). Same MSBuild interface, same error codes (XA0143/XA0145), but all the process management and polling logic is now in the shared library.

The PR is in draft since it depends on this PR (#284) merging first — the submodule currently points to feature/emulator-runner.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Note: the consumer PR was recreated after a branch rename — the correct link is now dotnet/android#10949 (the previous #10948 was auto-closed).

Port additional test coverage from dotnet/android PR #10949:
- AlreadyOnlinePhysicalDevice: physical device serial passthrough
- AdditionalArgs_PassedToLaunchEmulator: verify extra args reach process
- CancellationToken_AbortsBoot: cancellation during polling phase
- ColdBoot_PassesNoSnapshotLoad: verify -no-snapshot-load flag
- BootEmulatorAsync_NullAdbRunner_Throws: null guard validation
- BootEmulatorAsync_EmptyDeviceName_Throws: empty string guard
Total EmulatorRunner test count: 24 (18 existing + 6 new)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried it locally, but it errors:

Image

Is anything different from the code <BootAndroidEmulator/> had before?

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
The 'adb emu avd name' console command returns empty output on emulator
v36+ due to gRPC authentication requirements. This causes
BootEmulatorAsync to never match the running emulator by AVD name,
resulting in a perpetual polling loop and eventual timeout.
Add a fallback to 'adb shell getprop ro.boot.qemu.avd_name' which reads
the boot property set by the emulator kernel. This property is always
available and doesn't require console authentication.
The fix benefits all consumers of ListDevicesAsync/GetEmulatorAvdNameAsync,
not just BootEmulatorAsync.
Verified locally: BootEmulatorAsync now completes in ~3s (was timing out
at 120s) on emulator v36.4.9 with API 36 image.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔬 Definitive Proof: adb emu avd name Bug on Emulator v36+

Following up on the AVD name detection fix — I did thorough live testing on a properly running emulator (v36.4.9, API 36, arm64) to confirm the behavior.

Test Environment

  • Emulator: v36.4.9.0 (build 14788078), AVD MAUI_Emulator_API_36
  • ADB: v37.0.0-14910828
  • macOS: Darwin 25.3.0 (arm64, Apple M3 Pro)
  • Emulator fully booted: sys.boot_completed=1, adb devices shows device state

Results

MethodResult
adb -s emulator-5554 emu avd nameEMPTY (exit code 0, no output)
adb shell getprop ro.boot.qemu.avd_nameMAUI_Emulator_API_36
echo "avd name" | nc localhost 5554 (raw telnet)MAUI_Emulator_API_36
Console port 5554OPEN (nc -z succeeds)

Analysis

  1. The console port IS accessible — raw telnet to 5554 returns the AVD name correctly
  2. adb emu returns emptyadb uses a different protocol path than raw telnet, and something changed in emulator v36 that breaks it
  3. The emulator warns: The emulator now requires a signed jwt token for gRPC access! — while gRPC (port 8554) differs from telnet console (port 5554), this may affect how adb authenticates to the console

Impact on dotnet/android

The original BootAndroidEmulator.GetRunningAvdName() on main uses the exact same command:

MonoAndroidHelper.RunProcess(adbPath,$"-s {serial} emu avd name", ...);

This means FindRunningEmulatorForAvd would fail to match the AVD → WaitForEmulatorOnline would poll indefinitely → timeout after 120s. This is exactly the bug @jonathanpeppers reported.

Fix Validation

Our getprop ro.boot.qemu.avd_name fallback in AdbRunner.GetEmulatorAvdNameAsync:

  • Completes in 13ms (vs infinite timeout)
  • BootEmulatorAsync end-to-end: 2.8 seconds (vs 120s timeout)
  • All 259 existing tests pass

@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔄 Correction: ADB v37 Regression (not emulator v36 issue)

After deeper investigation, the root cause is more specific:

The Real Issue: ADB v37.0.0 broke adb emu commands

Platform-tools 37.0.0 (ADB 37.0.0-14910828) returns empty output for ALL adb emu subcommands — not just avd name. This is a regression from ADB 36.x where these commands work fine.

I verified with a .NET test program using bothMonoAndroidHelper.RunProcess-style (event-based) and ProcessUtils.StartProcess-style (stream-based) approaches — both get identical empty results. It's not a process execution issue.

Why dotnet/android CI works today

dotnet/android's Configuration.props pins XAPlatformToolsVersion to 36.0.0, so CI uses ADB 36.x where adb emu avd name works correctly. Users who manually upgrade to platform-tools 37 will hit this bug.

The getprop fallback is forward-compatible

The getprop ro.boot.qemu.avd_name fallback works regardless of ADB version, making EmulatorRunner robust against both the current ADB 37 regression and any future changes to the console protocol.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

📋 Research: ADB v37.0.0 adb emu Regression — Evidence & References

Following up on the correction comment with formal evidence supporting the getprop fallback fix.

1. Platform-Tools 37.0.0 is a stable public release

  • Google's official download servers host it:
    • https://dl.google.com/android/repository/platform-tools_r37.0.0-{win,linux,darwin}.zip
  • GitHub Actions macOS 15 runner images ship with it (image version 20260303):
    • Android SDK Platform-Tools | 37.0.0 (source)
    • Same emulator version we tested: Android Emulator | 36.4.9
  • Listed as "Latest Stable Release" in ADB-Explorer's version catalog
  • Google's official release notes at developer.android.com haven't been updated past 36.0.2 yet — the version is released but undocumented

2. Known Google Bug: adb emu returns empty

  • Google Issue Tracker #251776353: "adb avd id returns empty"
  • Originally reported on Intel Macs (platform-tools 34.x), still open/unresolved
  • Our testing confirms it now affects Apple Silicon (M3 Pro) with ADB 37.0.0
  • ALL adb emu subcommands return empty (not just avd name/id) — the entire console-via-ADB pathway is broken
  • Raw telnet to the console port (5554) works perfectly — proving the emulator console itself is fine

3. Why dotnet/android CI is not affected (yet)

  • Configuration.props pins XAPlatformToolsVersion=36.0.0 → CI uses ADB 36.x where adb emu works
  • Any CI/CD using macos-15 GitHub Actions runners WILL be affected — they already have pt 37.0.0
  • Developers using Android Studio (which auto-updates SDK components) will also hit this

4. The getprop fallback is the correct fix

  • getprop ro.boot.qemu.avd_name uses adb shell (standard ADB transport), not the emulator console protocol
  • Works on all ADB versions (35.x, 36.x, 37.x) — we verified this
  • Avoids the broken console-via-ADB pathway entirely
  • Available since Android API 21+ (emulator sets ro.boot.qemu.avd_name at boot)
  • Completes in ~13ms vs 120s timeout with broken adb emu

Summary

EvidenceFinding
Platform-tools 37.0.0✅ Stable, public release on dl.google.com
GitHub Actions macOS 15✅ Ships with pt 37.0.0 + emulator 36.4.9
Google Issue Tracker#251776353 — known open bug
dotnet/android CIUses pt 36.0.0 (pinned) — not yet affected
getprop fallbackWorks on ALL ADB versions — forward-compatible fix

rmarinhoand others added 2 commits March 17, 2026 12:22
Changes:
- Convert EmulatorBootOptions from class to record with init properties
- Change AdditionalArgs from IEnumerable to List for collection initializers
- Remove REMOVED lines from PublicAPI.Unshipped.txt files
- Remove local Log function, inline logger calls
- Simplify while loop condition in WaitForFullBootAsync
- Remove entireProcessTree from process termination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace logger?.Invoke with logger.Invoke using a static
NullLogger no-op delegate in EmulatorRunner, AdbRunner, and
AvdManagerRunner. The constructor assigns logger ?? NullLogger
so the field is never null. Static methods use logger ??= NullLogger
at entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
rmarinhoand others added 4 commits March 18, 2026 09:09
Add structured error classification enum (None, LaunchFailed, Timeout,
Cancelled, Unknown) so consumers can switch on ErrorKind instead of
parsing ErrorMessage strings. Set ErrorKind on all BootEmulatorAsync
return paths.
Addresses review feedback from dotnet/android#10949.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract shared NullLogger to RunnerDefaults utility class
- Remove duplicate NullLogger from AdbRunner, EmulatorRunner, AvdManagerRunner
Addresses review feedback from @jonathanpeppers on PR #284.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment incorrectly claimed the getprop fallback was needed because
'emulator 36+ requires auth for console commands'. After reviewing the
actual adb source code (console.cpp), adb emu handles console auth
automatically — it reads ~/.emulator_console_auth_token and sends it
before any command. This has been the case since ~2016.
The real reason for the fallback is that 'adb emu avd name' can return
empty output on some adb/emulator version combinations (observed with
adb v36). Updated both the XML doc and inline comment to accurately
describe the issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add early process exit detection in BootEmulatorAsync boot polling loop.
Previously, if the emulator failed immediately (e.g., insufficient disk
space, missing AVD), the full 300s timeout was wasted before reporting.
On macOS, the emulator binary forks the real QEMU process and the parent
exits with code 0 immediately. Only non-zero exit codes are treated as
immediate failures; exit code 0 continues polling since the real emulator
runs as a separate process.
Context: dotnet/android#10965
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The build failures is an issue rerunning -- attempt number need to be on artifacts (I'm fixing separately).

Merging shortly.

@jonathanpeppers
jonathanpeppers merged commit 39995cf into mainMar 19, 2026
1 of 2 checks passed
@jonathanpeppers
jonathanpeppers deleted the feature/emulator-runner branch March 19, 2026 13:05
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 20, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Mar 23, 2026
* Use shared EmulatorRunner from android-tools for BootAndroidEmulator
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
### Restore XA0144 for unexpected emulator errors
Update XA0144 message format to accept the ErrorMessage from
EmulatorRunner directly. The default switch case (Unknown and
future error kinds) now uses XA0144 with the full error details
instead of the misleading timeout message XA0145.
Error code mapping:
- XA0143: Launch failed (couldn't start emulator)
- XA0144: Unexpected exit/error (process exited, unknown errors)
- XA0145: Boot timeout (didn't finish in time)
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jul 13, 2026
This skill lets you say:
review this PR: dotnet/android-tools#284
Some example code reviews:
* dotnet/android-tools#283 (review)
* dotnet/android-tools#284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@rmarinho@jonathanpeppers@mattleibow
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add EmulatorRunner for emulator CLI operations - #284

Merged
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner
Mar 19, 2026
Merged

Add EmulatorRunner for emulator CLI operations#284
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner

Conversation

@rmarinho

@rmarinhormarinho commented Feb 23, 2026

Copy link
Copy Markdown
Member

EmulatorRunner: High-level emulator lifecycle management

Adds EmulatorRunner — a managed wrapper over the Android SDK emulator CLI binary, following the same pattern as AdbRunner and AvdManagerRunner.

API Surface

MethodDescription
LaunchEmulator(avdName, options?)Fire-and-forget: starts an emulator process and returns the Process handle. Caller owns the process lifetime. Validates avdName is non-empty.
BootEmulatorAsync(avdName, adb, options?, token?)Full lifecycle: checks if device is already online → checks if emulator process is running → launches emulator → polls adb devices until boot completes or timeout. Returns EmulatorBootResult with status and serial. Disposes Process handle on success (emulator keeps running).
ListAvdNamesAsync(token?)Lists available AVD names via emulator -list-avds. Checks exit code for failures.

Key Design Decisions

  • Naming: LaunchEmulator (fire-and-forget) vs BootEmulatorAsync (full lifecycle) — clear verb distinction matching the emulator domain
  • Kept EmulatorRunner name (not AvdRunner) — follows convention of naming runners after their CLI binary (emulatorEmulatorRunner, adbAdbRunner)
  • Process handle management: LaunchEmulator returns Process (caller-owned); BootEmulatorAsync disposes handle on success (emulator keeps running as detached process), kills+disposes on failure/timeout
  • Pipe draining: LaunchEmulator calls BeginOutputReadLine()/BeginErrorReadLine() after Start() to prevent OS pipe buffer deadlock
  • TryKillProcess: Instance method, uses typed catch (Exception ex) with logger for diagnostics, uses Kill(entireProcessTree: true) on .NET 5+

AdbRunner Enhancements (in this PR)

  • Added optional Action? logger parameter to constructor
  • RunShellCommandAsync(serial, command, ct) — single-string shell command (⚠️ device shell interprets it — documented in XML doc)
  • RunShellCommandAsync(serial, command, args, ct)NEW: structured overload that passes args as separate tokens, bypassing device shell interpretation via exec(). Safer for dynamic input.
  • GetShellPropertyAsync returns first non-empty line (for getprop queries)
  • Shell methods log stderr via logger on non-zero exit codes
  • Fixed RS0026/RS0027: only the most-params overload has optional CancellationToken
  • AVD name detection fix: GetEmulatorAvdNameAsync now falls back to adb shell getprop ro.boot.qemu.avd_name when adb emu avd name returns empty (observed returning empty on some adb/emulator v36 combinations)

Models

  • EmulatorBootOptions — configurable timeout (default 120s), poll interval (default 2s), cold boot, extra args (IEnumerable?)
  • EmulatorBootResult — immutable record with init-only properties: Status (enum), Serial, Message. Statuses: Success, AlreadyRunning, Timeout, Error

Bug Fix: AVD Name Detection on Emulator v36+

The adb emu avd name console command can return empty output on some adb/emulator version combinations (observed with adb v36). This caused BootEmulatorAsync to never match the running emulator by AVD name, resulting in a perpetual polling loop and eventual timeout.

Root cause: GetEmulatorAvdNameAsync relied solely on adb -s <serial> emu avd name. On some adb/emulator version combinations this command silently returns empty output (exit code 0, no content). The exact cause is unclear but the getprop fallback provides reliable AVD name resolution regardless.

Fix: Added fallback to adb shell getprop ro.boot.qemu.avd_name, which reads the boot property set by the emulator kernel. This property is always available via the standard adb shell interface and does not depend on the emulator console protocol.

Verified: BootEmulatorAsync now completes in ~3s (was timing out at 120s) on emulator v36.4.9 with API 36 image.

Consumer PR

  • dotnet/android #10949 — replaces BootAndroidEmulator MSBuild task (~454 lines) with a ~180-line wrapper delegating to EmulatorRunner.BootEmulatorAsync()

Tests (24 EmulatorRunner + 9 AdbRunner = 33 total)

EmulatorRunner (24):

  • Parse emulator -list-avds output (empty, single, multiple, blank lines, Windows newlines) — 4 tests
  • Constructor validation (null/empty/whitespace tool path) — 3 tests
  • LaunchEmulator argument validation (null, empty, whitespace AVD name) — 3 tests
  • BootEmulatorAsync lifecycle: already online device, already running AVD, successful boot after polling, timeout, launch failure, cancellation token — 6 tests
  • BootEmulatorAsync validation: invalid timeout, invalid poll interval, null AdbRunner, empty device name — 4 tests
  • Ported from dotnet/android BootAndroidEmulatorTests: physical device passthrough, AdditionalArgs forwarding, ColdBoot flag, cancellation abort — 4 tests

AdbRunner (9):

  • FirstNonEmptyLine parsing (null, empty, whitespace, single value, multiline, mixed) — 9 tests

Review Feedback Addressed

  • LaunchEmulator validates avdName parameter (throws ArgumentException)
  • LaunchEmulator drains stdout/stderr pipes via BeginOutputReadLine()/BeginErrorReadLine()
  • RunShellCommandAsync returns full stdout (not just first line)
  • ✅ Added structured RunShellCommandAsync overload (no shell interpretation)
  • ✅ Added 12 new unit tests (LaunchEmulator validation + FirstNonEmptyLine parsing)
  • ✅ Shell methods log stderr via logger on failure
  • ✅ Removed TOCTOU HasExited guard from TryKillProcess
  • ✅ Process handle disposed on successful boot (no handle leak)
  • ListAvdNamesAsync checks exit code
  • TryKillProcess uses typed catch (Exception ex) with logging
  • RunShellCommandAsync XML doc warns about shell interpretation
  • ✅ Fixed RS0026/RS0027 PublicAPI analyzer warnings
  • EmulatorBootResult uses init-only properties (immutable record)
  • ✅ Ported 6 additional tests from dotnet/android BootAndroidEmulatorTests
  • ✅ Fixed AVD name detection for emulator v36+ (getprop fallback)

CopilotAI review requested due to automatic review settings February 23, 2026 17:39

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 adds a new EmulatorRunner to Xamarin.Android.Tools.AndroidSdk intended to wrap Android emulator CLI operations, alongside new shared infrastructure for running Android SDK command-line tools with environment setup and result modeling.

Changes:

  • Added EmulatorRunner to start an AVD, stop an emulator, and list available AVD names.
  • Added AndroidToolRunner utility to run SDK tools sync/async (with timeouts) and to start long-running background processes.
  • Added AndroidEnvironmentHelper and ToolRunnerResult / ToolRunnerResult<T> to standardize tool environment and execution results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

FileDescription
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.csIntroduces emulator wrapper methods (start/stop/list AVDs) built on the tool runner infrastructure.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.csAdds process execution helpers (sync/async + background) with timeout/output capture.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.csAdds env var setup and mapping helpers (ABI/API/tag display names).
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.csAdds a shared result model for tool execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
@rmarinhormarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho
rmarinho requested a review from RedthFebruary 23, 2026 17:51
@jonathanpeppers

Copy link
Copy Markdown
Member

I'd like to get the System.Diagnostics.Process code unified like mentioned here:

rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #284 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:
- Methods now throw InvalidOperationException on failure
- Uses ProcessUtils.RunToolAsync() and StartToolBackground()
- Removed complex ToolRunnerResult wrapper types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from f1aa44f to 826d4aaCompareFebruary 24, 2026 14:15
rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #283/#284 feedback to use existing ProcessUtils.
Simplifies API by throwing exceptions on failure instead of
returning result types with error states.
Changes:
- AdbRunner: Simplified using ProcessUtils.RunToolAsync()
- EmulatorRunner: Uses ProcessUtils.StartToolBackground()
- Removed duplicate AndroidDeviceInfo from Models directory
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 2 times, most recently from 39617c8 to 5268300CompareFebruary 24, 2026 19:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 5 times, most recently from 1b10889 to ee31e4bCompareMarch 3, 2026 14:36
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from ee31e4b to 3a788bbCompareMarch 3, 2026 18:23
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Review feedback addressed — commit references

FeedbackCommitDetails
Port BootAndroidEmulator logic from dotnet/android0088e39BootAndWaitAsync with 3-phase boot, GetShellPropertyAsync, RunShellCommandAsync, 6 new tests

New files:

  • Models/EmulatorBootResult.cs, Models/EmulatorBootOptions.cs
  • Tests: 6 async boot scenarios ported from BootAndroidEmulatorTests.cs

Modified:

  • Runners/EmulatorRunner.csBootAndWaitAsync, FindRunningAvdSerial, WaitForFullBootAsync
  • Runners/AdbRunner.csGetShellPropertyAsync, RunShellCommandAsync (+ ListDevicesAsync made virtual for testability)

Draft dotnet/android consumer PR to follow.

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 AI Review Summary

Found 7 issues: 1 correctness, 2 error handling, 1 API design, 1 code duplication, 1 code organization, 1 naming.

  • Correctness: StartAvd redirects stdout/stderr but never drains the pipes — OS buffer fill will deadlock the emulator process (EmulatorRunner.cs:74)
  • API design: AdditionalArgs is a single string — will be treated as one argument by ProcessUtils.ArgumentList, breaking multi-token args like -gpu swiftshader_indirect (EmulatorBootOptions.cs:14)
  • Error handling: ListDevicesAsync ignores the exit code from ProcessUtils.StartProcess while sibling methods in AvdManagerRunner check it consistently (AdbRunner.cs:72)
  • Code duplication: AvdManagerRunner.AvdManagerPath reimplements the cmdline-tools version scanning that ProcessUtils.FindCmdlineTool (added in this same PR) already provides (AvdManagerRunner.cs:33)
  • Error handling: Bare catch { } swallows all exceptions without capturing them (AdbRunner.cs:107)

👍 Solid three-phase boot logic ported faithfully from dotnet/android. Good use of virtual on AdbRunner methods to enable clean test mocking. Thorough test coverage with 13+ unit tests covering parsing, edge cases, and the full boot flow. Nice extraction of AndroidEnvironmentHelper for shared env var setup.


This review was generated by the android-tools-reviewer skill based on review guidelines established by @jonathanpeppers.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
jonathanpeppers added a commit that referenced this pull request Mar 4, 2026
This skill let's you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
rmarinho added a commit to dotnet/android that referenced this pull request Mar 16, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

@jonathanpeppers Here's the dotnet/android consumer PR you requested: dotnet/android#10948

It replaces the 454-line BootAndroidEmulator task with a ~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync(). Same MSBuild interface, same error codes (XA0143/XA0145), but all the process management and polling logic is now in the shared library.

The PR is in draft since it depends on this PR (#284) merging first — the submodule currently points to feature/emulator-runner.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Note: the consumer PR was recreated after a branch rename — the correct link is now dotnet/android#10949 (the previous #10948 was auto-closed).

Port additional test coverage from dotnet/android PR #10949:
- AlreadyOnlinePhysicalDevice: physical device serial passthrough
- AdditionalArgs_PassedToLaunchEmulator: verify extra args reach process
- CancellationToken_AbortsBoot: cancellation during polling phase
- ColdBoot_PassesNoSnapshotLoad: verify -no-snapshot-load flag
- BootEmulatorAsync_NullAdbRunner_Throws: null guard validation
- BootEmulatorAsync_EmptyDeviceName_Throws: empty string guard
Total EmulatorRunner test count: 24 (18 existing + 6 new)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried it locally, but it errors:

Image

Is anything different from the code <BootAndroidEmulator/> had before?

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
The 'adb emu avd name' console command returns empty output on emulator
v36+ due to gRPC authentication requirements. This causes
BootEmulatorAsync to never match the running emulator by AVD name,
resulting in a perpetual polling loop and eventual timeout.
Add a fallback to 'adb shell getprop ro.boot.qemu.avd_name' which reads
the boot property set by the emulator kernel. This property is always
available and doesn't require console authentication.
The fix benefits all consumers of ListDevicesAsync/GetEmulatorAvdNameAsync,
not just BootEmulatorAsync.
Verified locally: BootEmulatorAsync now completes in ~3s (was timing out
at 120s) on emulator v36.4.9 with API 36 image.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔬 Definitive Proof: adb emu avd name Bug on Emulator v36+

Following up on the AVD name detection fix — I did thorough live testing on a properly running emulator (v36.4.9, API 36, arm64) to confirm the behavior.

Test Environment

  • Emulator: v36.4.9.0 (build 14788078), AVD MAUI_Emulator_API_36
  • ADB: v37.0.0-14910828
  • macOS: Darwin 25.3.0 (arm64, Apple M3 Pro)
  • Emulator fully booted: sys.boot_completed=1, adb devices shows device state

Results

MethodResult
adb -s emulator-5554 emu avd nameEMPTY (exit code 0, no output)
adb shell getprop ro.boot.qemu.avd_nameMAUI_Emulator_API_36
echo "avd name" | nc localhost 5554 (raw telnet)MAUI_Emulator_API_36
Console port 5554OPEN (nc -z succeeds)

Analysis

  1. The console port IS accessible — raw telnet to 5554 returns the AVD name correctly
  2. adb emu returns emptyadb uses a different protocol path than raw telnet, and something changed in emulator v36 that breaks it
  3. The emulator warns: The emulator now requires a signed jwt token for gRPC access! — while gRPC (port 8554) differs from telnet console (port 5554), this may affect how adb authenticates to the console

Impact on dotnet/android

The original BootAndroidEmulator.GetRunningAvdName() on main uses the exact same command:

MonoAndroidHelper.RunProcess(adbPath,$"-s {serial} emu avd name", ...);

This means FindRunningEmulatorForAvd would fail to match the AVD → WaitForEmulatorOnline would poll indefinitely → timeout after 120s. This is exactly the bug @jonathanpeppers reported.

Fix Validation

Our getprop ro.boot.qemu.avd_name fallback in AdbRunner.GetEmulatorAvdNameAsync:

  • Completes in 13ms (vs infinite timeout)
  • BootEmulatorAsync end-to-end: 2.8 seconds (vs 120s timeout)
  • All 259 existing tests pass

@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔄 Correction: ADB v37 Regression (not emulator v36 issue)

After deeper investigation, the root cause is more specific:

The Real Issue: ADB v37.0.0 broke adb emu commands

Platform-tools 37.0.0 (ADB 37.0.0-14910828) returns empty output for ALL adb emu subcommands — not just avd name. This is a regression from ADB 36.x where these commands work fine.

I verified with a .NET test program using bothMonoAndroidHelper.RunProcess-style (event-based) and ProcessUtils.StartProcess-style (stream-based) approaches — both get identical empty results. It's not a process execution issue.

Why dotnet/android CI works today

dotnet/android's Configuration.props pins XAPlatformToolsVersion to 36.0.0, so CI uses ADB 36.x where adb emu avd name works correctly. Users who manually upgrade to platform-tools 37 will hit this bug.

The getprop fallback is forward-compatible

The getprop ro.boot.qemu.avd_name fallback works regardless of ADB version, making EmulatorRunner robust against both the current ADB 37 regression and any future changes to the console protocol.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

📋 Research: ADB v37.0.0 adb emu Regression — Evidence & References

Following up on the correction comment with formal evidence supporting the getprop fallback fix.

1. Platform-Tools 37.0.0 is a stable public release

  • Google's official download servers host it:
    • https://dl.google.com/android/repository/platform-tools_r37.0.0-{win,linux,darwin}.zip
  • GitHub Actions macOS 15 runner images ship with it (image version 20260303):
    • Android SDK Platform-Tools | 37.0.0 (source)
    • Same emulator version we tested: Android Emulator | 36.4.9
  • Listed as "Latest Stable Release" in ADB-Explorer's version catalog
  • Google's official release notes at developer.android.com haven't been updated past 36.0.2 yet — the version is released but undocumented

2. Known Google Bug: adb emu returns empty

  • Google Issue Tracker #251776353: "adb avd id returns empty"
  • Originally reported on Intel Macs (platform-tools 34.x), still open/unresolved
  • Our testing confirms it now affects Apple Silicon (M3 Pro) with ADB 37.0.0
  • ALL adb emu subcommands return empty (not just avd name/id) — the entire console-via-ADB pathway is broken
  • Raw telnet to the console port (5554) works perfectly — proving the emulator console itself is fine

3. Why dotnet/android CI is not affected (yet)

  • Configuration.props pins XAPlatformToolsVersion=36.0.0 → CI uses ADB 36.x where adb emu works
  • Any CI/CD using macos-15 GitHub Actions runners WILL be affected — they already have pt 37.0.0
  • Developers using Android Studio (which auto-updates SDK components) will also hit this

4. The getprop fallback is the correct fix

  • getprop ro.boot.qemu.avd_name uses adb shell (standard ADB transport), not the emulator console protocol
  • Works on all ADB versions (35.x, 36.x, 37.x) — we verified this
  • Avoids the broken console-via-ADB pathway entirely
  • Available since Android API 21+ (emulator sets ro.boot.qemu.avd_name at boot)
  • Completes in ~13ms vs 120s timeout with broken adb emu

Summary

EvidenceFinding
Platform-tools 37.0.0✅ Stable, public release on dl.google.com
GitHub Actions macOS 15✅ Ships with pt 37.0.0 + emulator 36.4.9
Google Issue Tracker#251776353 — known open bug
dotnet/android CIUses pt 36.0.0 (pinned) — not yet affected
getprop fallbackWorks on ALL ADB versions — forward-compatible fix

rmarinhoand others added 2 commits March 17, 2026 12:22
Changes:
- Convert EmulatorBootOptions from class to record with init properties
- Change AdditionalArgs from IEnumerable to List for collection initializers
- Remove REMOVED lines from PublicAPI.Unshipped.txt files
- Remove local Log function, inline logger calls
- Simplify while loop condition in WaitForFullBootAsync
- Remove entireProcessTree from process termination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace logger?.Invoke with logger.Invoke using a static
NullLogger no-op delegate in EmulatorRunner, AdbRunner, and
AvdManagerRunner. The constructor assigns logger ?? NullLogger
so the field is never null. Static methods use logger ??= NullLogger
at entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
rmarinhoand others added 4 commits March 18, 2026 09:09
Add structured error classification enum (None, LaunchFailed, Timeout,
Cancelled, Unknown) so consumers can switch on ErrorKind instead of
parsing ErrorMessage strings. Set ErrorKind on all BootEmulatorAsync
return paths.
Addresses review feedback from dotnet/android#10949.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract shared NullLogger to RunnerDefaults utility class
- Remove duplicate NullLogger from AdbRunner, EmulatorRunner, AvdManagerRunner
Addresses review feedback from @jonathanpeppers on PR #284.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment incorrectly claimed the getprop fallback was needed because
'emulator 36+ requires auth for console commands'. After reviewing the
actual adb source code (console.cpp), adb emu handles console auth
automatically — it reads ~/.emulator_console_auth_token and sends it
before any command. This has been the case since ~2016.
The real reason for the fallback is that 'adb emu avd name' can return
empty output on some adb/emulator version combinations (observed with
adb v36). Updated both the XML doc and inline comment to accurately
describe the issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add early process exit detection in BootEmulatorAsync boot polling loop.
Previously, if the emulator failed immediately (e.g., insufficient disk
space, missing AVD), the full 300s timeout was wasted before reporting.
On macOS, the emulator binary forks the real QEMU process and the parent
exits with code 0 immediately. Only non-zero exit codes are treated as
immediate failures; exit code 0 continues polling since the real emulator
runs as a separate process.
Context: dotnet/android#10965
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The build failures is an issue rerunning -- attempt number need to be on artifacts (I'm fixing separately).

Merging shortly.

@jonathanpeppers
jonathanpeppers merged commit 39995cf into mainMar 19, 2026
1 of 2 checks passed
@jonathanpeppers
jonathanpeppers deleted the feature/emulator-runner branch March 19, 2026 13:05
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 20, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Mar 23, 2026
* Use shared EmulatorRunner from android-tools for BootAndroidEmulator
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
### Restore XA0144 for unexpected emulator errors
Update XA0144 message format to accept the ErrorMessage from
EmulatorRunner directly. The default switch case (Unknown and
future error kinds) now uses XA0144 with the full error details
instead of the misleading timeout message XA0145.
Error code mapping:
- XA0143: Launch failed (couldn't start emulator)
- XA0144: Unexpected exit/error (process exited, unknown errors)
- XA0145: Boot timeout (didn't finish in time)
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jul 13, 2026
This skill lets you say:
review this PR: dotnet/android-tools#284
Some example code reviews:
* dotnet/android-tools#283 (review)
* dotnet/android-tools#284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@rmarinho@jonathanpeppers@mattleibow
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add EmulatorRunner for emulator CLI operations - #284

Merged
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner
Mar 19, 2026
Merged

Add EmulatorRunner for emulator CLI operations#284
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner

Conversation

@rmarinho

@rmarinhormarinho commented Feb 23, 2026

Copy link
Copy Markdown
Member

EmulatorRunner: High-level emulator lifecycle management

Adds EmulatorRunner — a managed wrapper over the Android SDK emulator CLI binary, following the same pattern as AdbRunner and AvdManagerRunner.

API Surface

MethodDescription
LaunchEmulator(avdName, options?)Fire-and-forget: starts an emulator process and returns the Process handle. Caller owns the process lifetime. Validates avdName is non-empty.
BootEmulatorAsync(avdName, adb, options?, token?)Full lifecycle: checks if device is already online → checks if emulator process is running → launches emulator → polls adb devices until boot completes or timeout. Returns EmulatorBootResult with status and serial. Disposes Process handle on success (emulator keeps running).
ListAvdNamesAsync(token?)Lists available AVD names via emulator -list-avds. Checks exit code for failures.

Key Design Decisions

  • Naming: LaunchEmulator (fire-and-forget) vs BootEmulatorAsync (full lifecycle) — clear verb distinction matching the emulator domain
  • Kept EmulatorRunner name (not AvdRunner) — follows convention of naming runners after their CLI binary (emulatorEmulatorRunner, adbAdbRunner)
  • Process handle management: LaunchEmulator returns Process (caller-owned); BootEmulatorAsync disposes handle on success (emulator keeps running as detached process), kills+disposes on failure/timeout
  • Pipe draining: LaunchEmulator calls BeginOutputReadLine()/BeginErrorReadLine() after Start() to prevent OS pipe buffer deadlock
  • TryKillProcess: Instance method, uses typed catch (Exception ex) with logger for diagnostics, uses Kill(entireProcessTree: true) on .NET 5+

AdbRunner Enhancements (in this PR)

  • Added optional Action? logger parameter to constructor
  • RunShellCommandAsync(serial, command, ct) — single-string shell command (⚠️ device shell interprets it — documented in XML doc)
  • RunShellCommandAsync(serial, command, args, ct)NEW: structured overload that passes args as separate tokens, bypassing device shell interpretation via exec(). Safer for dynamic input.
  • GetShellPropertyAsync returns first non-empty line (for getprop queries)
  • Shell methods log stderr via logger on non-zero exit codes
  • Fixed RS0026/RS0027: only the most-params overload has optional CancellationToken
  • AVD name detection fix: GetEmulatorAvdNameAsync now falls back to adb shell getprop ro.boot.qemu.avd_name when adb emu avd name returns empty (observed returning empty on some adb/emulator v36 combinations)

Models

  • EmulatorBootOptions — configurable timeout (default 120s), poll interval (default 2s), cold boot, extra args (IEnumerable?)
  • EmulatorBootResult — immutable record with init-only properties: Status (enum), Serial, Message. Statuses: Success, AlreadyRunning, Timeout, Error

Bug Fix: AVD Name Detection on Emulator v36+

The adb emu avd name console command can return empty output on some adb/emulator version combinations (observed with adb v36). This caused BootEmulatorAsync to never match the running emulator by AVD name, resulting in a perpetual polling loop and eventual timeout.

Root cause: GetEmulatorAvdNameAsync relied solely on adb -s <serial> emu avd name. On some adb/emulator version combinations this command silently returns empty output (exit code 0, no content). The exact cause is unclear but the getprop fallback provides reliable AVD name resolution regardless.

Fix: Added fallback to adb shell getprop ro.boot.qemu.avd_name, which reads the boot property set by the emulator kernel. This property is always available via the standard adb shell interface and does not depend on the emulator console protocol.

Verified: BootEmulatorAsync now completes in ~3s (was timing out at 120s) on emulator v36.4.9 with API 36 image.

Consumer PR

  • dotnet/android #10949 — replaces BootAndroidEmulator MSBuild task (~454 lines) with a ~180-line wrapper delegating to EmulatorRunner.BootEmulatorAsync()

Tests (24 EmulatorRunner + 9 AdbRunner = 33 total)

EmulatorRunner (24):

  • Parse emulator -list-avds output (empty, single, multiple, blank lines, Windows newlines) — 4 tests
  • Constructor validation (null/empty/whitespace tool path) — 3 tests
  • LaunchEmulator argument validation (null, empty, whitespace AVD name) — 3 tests
  • BootEmulatorAsync lifecycle: already online device, already running AVD, successful boot after polling, timeout, launch failure, cancellation token — 6 tests
  • BootEmulatorAsync validation: invalid timeout, invalid poll interval, null AdbRunner, empty device name — 4 tests
  • Ported from dotnet/android BootAndroidEmulatorTests: physical device passthrough, AdditionalArgs forwarding, ColdBoot flag, cancellation abort — 4 tests

AdbRunner (9):

  • FirstNonEmptyLine parsing (null, empty, whitespace, single value, multiline, mixed) — 9 tests

Review Feedback Addressed

  • LaunchEmulator validates avdName parameter (throws ArgumentException)
  • LaunchEmulator drains stdout/stderr pipes via BeginOutputReadLine()/BeginErrorReadLine()
  • RunShellCommandAsync returns full stdout (not just first line)
  • ✅ Added structured RunShellCommandAsync overload (no shell interpretation)
  • ✅ Added 12 new unit tests (LaunchEmulator validation + FirstNonEmptyLine parsing)
  • ✅ Shell methods log stderr via logger on failure
  • ✅ Removed TOCTOU HasExited guard from TryKillProcess
  • ✅ Process handle disposed on successful boot (no handle leak)
  • ListAvdNamesAsync checks exit code
  • TryKillProcess uses typed catch (Exception ex) with logging
  • RunShellCommandAsync XML doc warns about shell interpretation
  • ✅ Fixed RS0026/RS0027 PublicAPI analyzer warnings
  • EmulatorBootResult uses init-only properties (immutable record)
  • ✅ Ported 6 additional tests from dotnet/android BootAndroidEmulatorTests
  • ✅ Fixed AVD name detection for emulator v36+ (getprop fallback)

CopilotAI review requested due to automatic review settings February 23, 2026 17:39

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 adds a new EmulatorRunner to Xamarin.Android.Tools.AndroidSdk intended to wrap Android emulator CLI operations, alongside new shared infrastructure for running Android SDK command-line tools with environment setup and result modeling.

Changes:

  • Added EmulatorRunner to start an AVD, stop an emulator, and list available AVD names.
  • Added AndroidToolRunner utility to run SDK tools sync/async (with timeouts) and to start long-running background processes.
  • Added AndroidEnvironmentHelper and ToolRunnerResult / ToolRunnerResult<T> to standardize tool environment and execution results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

FileDescription
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.csIntroduces emulator wrapper methods (start/stop/list AVDs) built on the tool runner infrastructure.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.csAdds process execution helpers (sync/async + background) with timeout/output capture.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.csAdds env var setup and mapping helpers (ABI/API/tag display names).
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.csAdds a shared result model for tool execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
@rmarinhormarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho
rmarinho requested a review from RedthFebruary 23, 2026 17:51
@jonathanpeppers

Copy link
Copy Markdown
Member

I'd like to get the System.Diagnostics.Process code unified like mentioned here:

rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #284 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:
- Methods now throw InvalidOperationException on failure
- Uses ProcessUtils.RunToolAsync() and StartToolBackground()
- Removed complex ToolRunnerResult wrapper types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from f1aa44f to 826d4aaCompareFebruary 24, 2026 14:15
rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #283/#284 feedback to use existing ProcessUtils.
Simplifies API by throwing exceptions on failure instead of
returning result types with error states.
Changes:
- AdbRunner: Simplified using ProcessUtils.RunToolAsync()
- EmulatorRunner: Uses ProcessUtils.StartToolBackground()
- Removed duplicate AndroidDeviceInfo from Models directory
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 2 times, most recently from 39617c8 to 5268300CompareFebruary 24, 2026 19:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 5 times, most recently from 1b10889 to ee31e4bCompareMarch 3, 2026 14:36
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from ee31e4b to 3a788bbCompareMarch 3, 2026 18:23
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Review feedback addressed — commit references

FeedbackCommitDetails
Port BootAndroidEmulator logic from dotnet/android0088e39BootAndWaitAsync with 3-phase boot, GetShellPropertyAsync, RunShellCommandAsync, 6 new tests

New files:

  • Models/EmulatorBootResult.cs, Models/EmulatorBootOptions.cs
  • Tests: 6 async boot scenarios ported from BootAndroidEmulatorTests.cs

Modified:

  • Runners/EmulatorRunner.csBootAndWaitAsync, FindRunningAvdSerial, WaitForFullBootAsync
  • Runners/AdbRunner.csGetShellPropertyAsync, RunShellCommandAsync (+ ListDevicesAsync made virtual for testability)

Draft dotnet/android consumer PR to follow.

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 AI Review Summary

Found 7 issues: 1 correctness, 2 error handling, 1 API design, 1 code duplication, 1 code organization, 1 naming.

  • Correctness: StartAvd redirects stdout/stderr but never drains the pipes — OS buffer fill will deadlock the emulator process (EmulatorRunner.cs:74)
  • API design: AdditionalArgs is a single string — will be treated as one argument by ProcessUtils.ArgumentList, breaking multi-token args like -gpu swiftshader_indirect (EmulatorBootOptions.cs:14)
  • Error handling: ListDevicesAsync ignores the exit code from ProcessUtils.StartProcess while sibling methods in AvdManagerRunner check it consistently (AdbRunner.cs:72)
  • Code duplication: AvdManagerRunner.AvdManagerPath reimplements the cmdline-tools version scanning that ProcessUtils.FindCmdlineTool (added in this same PR) already provides (AvdManagerRunner.cs:33)
  • Error handling: Bare catch { } swallows all exceptions without capturing them (AdbRunner.cs:107)

👍 Solid three-phase boot logic ported faithfully from dotnet/android. Good use of virtual on AdbRunner methods to enable clean test mocking. Thorough test coverage with 13+ unit tests covering parsing, edge cases, and the full boot flow. Nice extraction of AndroidEnvironmentHelper for shared env var setup.


This review was generated by the android-tools-reviewer skill based on review guidelines established by @jonathanpeppers.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
jonathanpeppers added a commit that referenced this pull request Mar 4, 2026
This skill let's you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
rmarinho added a commit to dotnet/android that referenced this pull request Mar 16, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

@jonathanpeppers Here's the dotnet/android consumer PR you requested: dotnet/android#10948

It replaces the 454-line BootAndroidEmulator task with a ~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync(). Same MSBuild interface, same error codes (XA0143/XA0145), but all the process management and polling logic is now in the shared library.

The PR is in draft since it depends on this PR (#284) merging first — the submodule currently points to feature/emulator-runner.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Note: the consumer PR was recreated after a branch rename — the correct link is now dotnet/android#10949 (the previous #10948 was auto-closed).

Port additional test coverage from dotnet/android PR #10949:
- AlreadyOnlinePhysicalDevice: physical device serial passthrough
- AdditionalArgs_PassedToLaunchEmulator: verify extra args reach process
- CancellationToken_AbortsBoot: cancellation during polling phase
- ColdBoot_PassesNoSnapshotLoad: verify -no-snapshot-load flag
- BootEmulatorAsync_NullAdbRunner_Throws: null guard validation
- BootEmulatorAsync_EmptyDeviceName_Throws: empty string guard
Total EmulatorRunner test count: 24 (18 existing + 6 new)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried it locally, but it errors:

Image

Is anything different from the code <BootAndroidEmulator/> had before?

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
The 'adb emu avd name' console command returns empty output on emulator
v36+ due to gRPC authentication requirements. This causes
BootEmulatorAsync to never match the running emulator by AVD name,
resulting in a perpetual polling loop and eventual timeout.
Add a fallback to 'adb shell getprop ro.boot.qemu.avd_name' which reads
the boot property set by the emulator kernel. This property is always
available and doesn't require console authentication.
The fix benefits all consumers of ListDevicesAsync/GetEmulatorAvdNameAsync,
not just BootEmulatorAsync.
Verified locally: BootEmulatorAsync now completes in ~3s (was timing out
at 120s) on emulator v36.4.9 with API 36 image.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔬 Definitive Proof: adb emu avd name Bug on Emulator v36+

Following up on the AVD name detection fix — I did thorough live testing on a properly running emulator (v36.4.9, API 36, arm64) to confirm the behavior.

Test Environment

  • Emulator: v36.4.9.0 (build 14788078), AVD MAUI_Emulator_API_36
  • ADB: v37.0.0-14910828
  • macOS: Darwin 25.3.0 (arm64, Apple M3 Pro)
  • Emulator fully booted: sys.boot_completed=1, adb devices shows device state

Results

MethodResult
adb -s emulator-5554 emu avd nameEMPTY (exit code 0, no output)
adb shell getprop ro.boot.qemu.avd_nameMAUI_Emulator_API_36
echo "avd name" | nc localhost 5554 (raw telnet)MAUI_Emulator_API_36
Console port 5554OPEN (nc -z succeeds)

Analysis

  1. The console port IS accessible — raw telnet to 5554 returns the AVD name correctly
  2. adb emu returns emptyadb uses a different protocol path than raw telnet, and something changed in emulator v36 that breaks it
  3. The emulator warns: The emulator now requires a signed jwt token for gRPC access! — while gRPC (port 8554) differs from telnet console (port 5554), this may affect how adb authenticates to the console

Impact on dotnet/android

The original BootAndroidEmulator.GetRunningAvdName() on main uses the exact same command:

MonoAndroidHelper.RunProcess(adbPath,$"-s {serial} emu avd name", ...);

This means FindRunningEmulatorForAvd would fail to match the AVD → WaitForEmulatorOnline would poll indefinitely → timeout after 120s. This is exactly the bug @jonathanpeppers reported.

Fix Validation

Our getprop ro.boot.qemu.avd_name fallback in AdbRunner.GetEmulatorAvdNameAsync:

  • Completes in 13ms (vs infinite timeout)
  • BootEmulatorAsync end-to-end: 2.8 seconds (vs 120s timeout)
  • All 259 existing tests pass

@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔄 Correction: ADB v37 Regression (not emulator v36 issue)

After deeper investigation, the root cause is more specific:

The Real Issue: ADB v37.0.0 broke adb emu commands

Platform-tools 37.0.0 (ADB 37.0.0-14910828) returns empty output for ALL adb emu subcommands — not just avd name. This is a regression from ADB 36.x where these commands work fine.

I verified with a .NET test program using bothMonoAndroidHelper.RunProcess-style (event-based) and ProcessUtils.StartProcess-style (stream-based) approaches — both get identical empty results. It's not a process execution issue.

Why dotnet/android CI works today

dotnet/android's Configuration.props pins XAPlatformToolsVersion to 36.0.0, so CI uses ADB 36.x where adb emu avd name works correctly. Users who manually upgrade to platform-tools 37 will hit this bug.

The getprop fallback is forward-compatible

The getprop ro.boot.qemu.avd_name fallback works regardless of ADB version, making EmulatorRunner robust against both the current ADB 37 regression and any future changes to the console protocol.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

📋 Research: ADB v37.0.0 adb emu Regression — Evidence & References

Following up on the correction comment with formal evidence supporting the getprop fallback fix.

1. Platform-Tools 37.0.0 is a stable public release

  • Google's official download servers host it:
    • https://dl.google.com/android/repository/platform-tools_r37.0.0-{win,linux,darwin}.zip
  • GitHub Actions macOS 15 runner images ship with it (image version 20260303):
    • Android SDK Platform-Tools | 37.0.0 (source)
    • Same emulator version we tested: Android Emulator | 36.4.9
  • Listed as "Latest Stable Release" in ADB-Explorer's version catalog
  • Google's official release notes at developer.android.com haven't been updated past 36.0.2 yet — the version is released but undocumented

2. Known Google Bug: adb emu returns empty

  • Google Issue Tracker #251776353: "adb avd id returns empty"
  • Originally reported on Intel Macs (platform-tools 34.x), still open/unresolved
  • Our testing confirms it now affects Apple Silicon (M3 Pro) with ADB 37.0.0
  • ALL adb emu subcommands return empty (not just avd name/id) — the entire console-via-ADB pathway is broken
  • Raw telnet to the console port (5554) works perfectly — proving the emulator console itself is fine

3. Why dotnet/android CI is not affected (yet)

  • Configuration.props pins XAPlatformToolsVersion=36.0.0 → CI uses ADB 36.x where adb emu works
  • Any CI/CD using macos-15 GitHub Actions runners WILL be affected — they already have pt 37.0.0
  • Developers using Android Studio (which auto-updates SDK components) will also hit this

4. The getprop fallback is the correct fix

  • getprop ro.boot.qemu.avd_name uses adb shell (standard ADB transport), not the emulator console protocol
  • Works on all ADB versions (35.x, 36.x, 37.x) — we verified this
  • Avoids the broken console-via-ADB pathway entirely
  • Available since Android API 21+ (emulator sets ro.boot.qemu.avd_name at boot)
  • Completes in ~13ms vs 120s timeout with broken adb emu

Summary

EvidenceFinding
Platform-tools 37.0.0✅ Stable, public release on dl.google.com
GitHub Actions macOS 15✅ Ships with pt 37.0.0 + emulator 36.4.9
Google Issue Tracker#251776353 — known open bug
dotnet/android CIUses pt 36.0.0 (pinned) — not yet affected
getprop fallbackWorks on ALL ADB versions — forward-compatible fix

rmarinhoand others added 2 commits March 17, 2026 12:22
Changes:
- Convert EmulatorBootOptions from class to record with init properties
- Change AdditionalArgs from IEnumerable to List for collection initializers
- Remove REMOVED lines from PublicAPI.Unshipped.txt files
- Remove local Log function, inline logger calls
- Simplify while loop condition in WaitForFullBootAsync
- Remove entireProcessTree from process termination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace logger?.Invoke with logger.Invoke using a static
NullLogger no-op delegate in EmulatorRunner, AdbRunner, and
AvdManagerRunner. The constructor assigns logger ?? NullLogger
so the field is never null. Static methods use logger ??= NullLogger
at entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
rmarinhoand others added 4 commits March 18, 2026 09:09
Add structured error classification enum (None, LaunchFailed, Timeout,
Cancelled, Unknown) so consumers can switch on ErrorKind instead of
parsing ErrorMessage strings. Set ErrorKind on all BootEmulatorAsync
return paths.
Addresses review feedback from dotnet/android#10949.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract shared NullLogger to RunnerDefaults utility class
- Remove duplicate NullLogger from AdbRunner, EmulatorRunner, AvdManagerRunner
Addresses review feedback from @jonathanpeppers on PR #284.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment incorrectly claimed the getprop fallback was needed because
'emulator 36+ requires auth for console commands'. After reviewing the
actual adb source code (console.cpp), adb emu handles console auth
automatically — it reads ~/.emulator_console_auth_token and sends it
before any command. This has been the case since ~2016.
The real reason for the fallback is that 'adb emu avd name' can return
empty output on some adb/emulator version combinations (observed with
adb v36). Updated both the XML doc and inline comment to accurately
describe the issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add early process exit detection in BootEmulatorAsync boot polling loop.
Previously, if the emulator failed immediately (e.g., insufficient disk
space, missing AVD), the full 300s timeout was wasted before reporting.
On macOS, the emulator binary forks the real QEMU process and the parent
exits with code 0 immediately. Only non-zero exit codes are treated as
immediate failures; exit code 0 continues polling since the real emulator
runs as a separate process.
Context: dotnet/android#10965
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The build failures is an issue rerunning -- attempt number need to be on artifacts (I'm fixing separately).

Merging shortly.

@jonathanpeppers
jonathanpeppers merged commit 39995cf into mainMar 19, 2026
1 of 2 checks passed
@jonathanpeppers
jonathanpeppers deleted the feature/emulator-runner branch March 19, 2026 13:05
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 20, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Mar 23, 2026
* Use shared EmulatorRunner from android-tools for BootAndroidEmulator
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
### Restore XA0144 for unexpected emulator errors
Update XA0144 message format to accept the ErrorMessage from
EmulatorRunner directly. The default switch case (Unknown and
future error kinds) now uses XA0144 with the full error details
instead of the misleading timeout message XA0145.
Error code mapping:
- XA0143: Launch failed (couldn't start emulator)
- XA0144: Unexpected exit/error (process exited, unknown errors)
- XA0145: Boot timeout (didn't finish in time)
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jul 13, 2026
This skill lets you say:
review this PR: dotnet/android-tools#284
Some example code reviews:
* dotnet/android-tools#283 (review)
* dotnet/android-tools#284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@rmarinho@jonathanpeppers@mattleibow
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add EmulatorRunner for emulator CLI operations - #284

Merged
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner
Mar 19, 2026
Merged

Add EmulatorRunner for emulator CLI operations#284
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner

Conversation

@rmarinho

@rmarinhormarinho commented Feb 23, 2026

Copy link
Copy Markdown
Member

EmulatorRunner: High-level emulator lifecycle management

Adds EmulatorRunner — a managed wrapper over the Android SDK emulator CLI binary, following the same pattern as AdbRunner and AvdManagerRunner.

API Surface

MethodDescription
LaunchEmulator(avdName, options?)Fire-and-forget: starts an emulator process and returns the Process handle. Caller owns the process lifetime. Validates avdName is non-empty.
BootEmulatorAsync(avdName, adb, options?, token?)Full lifecycle: checks if device is already online → checks if emulator process is running → launches emulator → polls adb devices until boot completes or timeout. Returns EmulatorBootResult with status and serial. Disposes Process handle on success (emulator keeps running).
ListAvdNamesAsync(token?)Lists available AVD names via emulator -list-avds. Checks exit code for failures.

Key Design Decisions

  • Naming: LaunchEmulator (fire-and-forget) vs BootEmulatorAsync (full lifecycle) — clear verb distinction matching the emulator domain
  • Kept EmulatorRunner name (not AvdRunner) — follows convention of naming runners after their CLI binary (emulatorEmulatorRunner, adbAdbRunner)
  • Process handle management: LaunchEmulator returns Process (caller-owned); BootEmulatorAsync disposes handle on success (emulator keeps running as detached process), kills+disposes on failure/timeout
  • Pipe draining: LaunchEmulator calls BeginOutputReadLine()/BeginErrorReadLine() after Start() to prevent OS pipe buffer deadlock
  • TryKillProcess: Instance method, uses typed catch (Exception ex) with logger for diagnostics, uses Kill(entireProcessTree: true) on .NET 5+

AdbRunner Enhancements (in this PR)

  • Added optional Action? logger parameter to constructor
  • RunShellCommandAsync(serial, command, ct) — single-string shell command (⚠️ device shell interprets it — documented in XML doc)
  • RunShellCommandAsync(serial, command, args, ct)NEW: structured overload that passes args as separate tokens, bypassing device shell interpretation via exec(). Safer for dynamic input.
  • GetShellPropertyAsync returns first non-empty line (for getprop queries)
  • Shell methods log stderr via logger on non-zero exit codes
  • Fixed RS0026/RS0027: only the most-params overload has optional CancellationToken
  • AVD name detection fix: GetEmulatorAvdNameAsync now falls back to adb shell getprop ro.boot.qemu.avd_name when adb emu avd name returns empty (observed returning empty on some adb/emulator v36 combinations)

Models

  • EmulatorBootOptions — configurable timeout (default 120s), poll interval (default 2s), cold boot, extra args (IEnumerable?)
  • EmulatorBootResult — immutable record with init-only properties: Status (enum), Serial, Message. Statuses: Success, AlreadyRunning, Timeout, Error

Bug Fix: AVD Name Detection on Emulator v36+

The adb emu avd name console command can return empty output on some adb/emulator version combinations (observed with adb v36). This caused BootEmulatorAsync to never match the running emulator by AVD name, resulting in a perpetual polling loop and eventual timeout.

Root cause: GetEmulatorAvdNameAsync relied solely on adb -s <serial> emu avd name. On some adb/emulator version combinations this command silently returns empty output (exit code 0, no content). The exact cause is unclear but the getprop fallback provides reliable AVD name resolution regardless.

Fix: Added fallback to adb shell getprop ro.boot.qemu.avd_name, which reads the boot property set by the emulator kernel. This property is always available via the standard adb shell interface and does not depend on the emulator console protocol.

Verified: BootEmulatorAsync now completes in ~3s (was timing out at 120s) on emulator v36.4.9 with API 36 image.

Consumer PR

  • dotnet/android #10949 — replaces BootAndroidEmulator MSBuild task (~454 lines) with a ~180-line wrapper delegating to EmulatorRunner.BootEmulatorAsync()

Tests (24 EmulatorRunner + 9 AdbRunner = 33 total)

EmulatorRunner (24):

  • Parse emulator -list-avds output (empty, single, multiple, blank lines, Windows newlines) — 4 tests
  • Constructor validation (null/empty/whitespace tool path) — 3 tests
  • LaunchEmulator argument validation (null, empty, whitespace AVD name) — 3 tests
  • BootEmulatorAsync lifecycle: already online device, already running AVD, successful boot after polling, timeout, launch failure, cancellation token — 6 tests
  • BootEmulatorAsync validation: invalid timeout, invalid poll interval, null AdbRunner, empty device name — 4 tests
  • Ported from dotnet/android BootAndroidEmulatorTests: physical device passthrough, AdditionalArgs forwarding, ColdBoot flag, cancellation abort — 4 tests

AdbRunner (9):

  • FirstNonEmptyLine parsing (null, empty, whitespace, single value, multiline, mixed) — 9 tests

Review Feedback Addressed

  • LaunchEmulator validates avdName parameter (throws ArgumentException)
  • LaunchEmulator drains stdout/stderr pipes via BeginOutputReadLine()/BeginErrorReadLine()
  • RunShellCommandAsync returns full stdout (not just first line)
  • ✅ Added structured RunShellCommandAsync overload (no shell interpretation)
  • ✅ Added 12 new unit tests (LaunchEmulator validation + FirstNonEmptyLine parsing)
  • ✅ Shell methods log stderr via logger on failure
  • ✅ Removed TOCTOU HasExited guard from TryKillProcess
  • ✅ Process handle disposed on successful boot (no handle leak)
  • ListAvdNamesAsync checks exit code
  • TryKillProcess uses typed catch (Exception ex) with logging
  • RunShellCommandAsync XML doc warns about shell interpretation
  • ✅ Fixed RS0026/RS0027 PublicAPI analyzer warnings
  • EmulatorBootResult uses init-only properties (immutable record)
  • ✅ Ported 6 additional tests from dotnet/android BootAndroidEmulatorTests
  • ✅ Fixed AVD name detection for emulator v36+ (getprop fallback)

CopilotAI review requested due to automatic review settings February 23, 2026 17:39

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 adds a new EmulatorRunner to Xamarin.Android.Tools.AndroidSdk intended to wrap Android emulator CLI operations, alongside new shared infrastructure for running Android SDK command-line tools with environment setup and result modeling.

Changes:

  • Added EmulatorRunner to start an AVD, stop an emulator, and list available AVD names.
  • Added AndroidToolRunner utility to run SDK tools sync/async (with timeouts) and to start long-running background processes.
  • Added AndroidEnvironmentHelper and ToolRunnerResult / ToolRunnerResult<T> to standardize tool environment and execution results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

FileDescription
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.csIntroduces emulator wrapper methods (start/stop/list AVDs) built on the tool runner infrastructure.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.csAdds process execution helpers (sync/async + background) with timeout/output capture.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.csAdds env var setup and mapping helpers (ABI/API/tag display names).
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.csAdds a shared result model for tool execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
@rmarinhormarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho
rmarinho requested a review from RedthFebruary 23, 2026 17:51
@jonathanpeppers

Copy link
Copy Markdown
Member

I'd like to get the System.Diagnostics.Process code unified like mentioned here:

rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #284 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:
- Methods now throw InvalidOperationException on failure
- Uses ProcessUtils.RunToolAsync() and StartToolBackground()
- Removed complex ToolRunnerResult wrapper types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from f1aa44f to 826d4aaCompareFebruary 24, 2026 14:15
rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #283/#284 feedback to use existing ProcessUtils.
Simplifies API by throwing exceptions on failure instead of
returning result types with error states.
Changes:
- AdbRunner: Simplified using ProcessUtils.RunToolAsync()
- EmulatorRunner: Uses ProcessUtils.StartToolBackground()
- Removed duplicate AndroidDeviceInfo from Models directory
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 2 times, most recently from 39617c8 to 5268300CompareFebruary 24, 2026 19:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 5 times, most recently from 1b10889 to ee31e4bCompareMarch 3, 2026 14:36
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from ee31e4b to 3a788bbCompareMarch 3, 2026 18:23
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Review feedback addressed — commit references

FeedbackCommitDetails
Port BootAndroidEmulator logic from dotnet/android0088e39BootAndWaitAsync with 3-phase boot, GetShellPropertyAsync, RunShellCommandAsync, 6 new tests

New files:

  • Models/EmulatorBootResult.cs, Models/EmulatorBootOptions.cs
  • Tests: 6 async boot scenarios ported from BootAndroidEmulatorTests.cs

Modified:

  • Runners/EmulatorRunner.csBootAndWaitAsync, FindRunningAvdSerial, WaitForFullBootAsync
  • Runners/AdbRunner.csGetShellPropertyAsync, RunShellCommandAsync (+ ListDevicesAsync made virtual for testability)

Draft dotnet/android consumer PR to follow.

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 AI Review Summary

Found 7 issues: 1 correctness, 2 error handling, 1 API design, 1 code duplication, 1 code organization, 1 naming.

  • Correctness: StartAvd redirects stdout/stderr but never drains the pipes — OS buffer fill will deadlock the emulator process (EmulatorRunner.cs:74)
  • API design: AdditionalArgs is a single string — will be treated as one argument by ProcessUtils.ArgumentList, breaking multi-token args like -gpu swiftshader_indirect (EmulatorBootOptions.cs:14)
  • Error handling: ListDevicesAsync ignores the exit code from ProcessUtils.StartProcess while sibling methods in AvdManagerRunner check it consistently (AdbRunner.cs:72)
  • Code duplication: AvdManagerRunner.AvdManagerPath reimplements the cmdline-tools version scanning that ProcessUtils.FindCmdlineTool (added in this same PR) already provides (AvdManagerRunner.cs:33)
  • Error handling: Bare catch { } swallows all exceptions without capturing them (AdbRunner.cs:107)

👍 Solid three-phase boot logic ported faithfully from dotnet/android. Good use of virtual on AdbRunner methods to enable clean test mocking. Thorough test coverage with 13+ unit tests covering parsing, edge cases, and the full boot flow. Nice extraction of AndroidEnvironmentHelper for shared env var setup.


This review was generated by the android-tools-reviewer skill based on review guidelines established by @jonathanpeppers.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
jonathanpeppers added a commit that referenced this pull request Mar 4, 2026
This skill let's you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
rmarinho added a commit to dotnet/android that referenced this pull request Mar 16, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

@jonathanpeppers Here's the dotnet/android consumer PR you requested: dotnet/android#10948

It replaces the 454-line BootAndroidEmulator task with a ~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync(). Same MSBuild interface, same error codes (XA0143/XA0145), but all the process management and polling logic is now in the shared library.

The PR is in draft since it depends on this PR (#284) merging first — the submodule currently points to feature/emulator-runner.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Note: the consumer PR was recreated after a branch rename — the correct link is now dotnet/android#10949 (the previous #10948 was auto-closed).

Port additional test coverage from dotnet/android PR #10949:
- AlreadyOnlinePhysicalDevice: physical device serial passthrough
- AdditionalArgs_PassedToLaunchEmulator: verify extra args reach process
- CancellationToken_AbortsBoot: cancellation during polling phase
- ColdBoot_PassesNoSnapshotLoad: verify -no-snapshot-load flag
- BootEmulatorAsync_NullAdbRunner_Throws: null guard validation
- BootEmulatorAsync_EmptyDeviceName_Throws: empty string guard
Total EmulatorRunner test count: 24 (18 existing + 6 new)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried it locally, but it errors:

Image

Is anything different from the code <BootAndroidEmulator/> had before?

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
The 'adb emu avd name' console command returns empty output on emulator
v36+ due to gRPC authentication requirements. This causes
BootEmulatorAsync to never match the running emulator by AVD name,
resulting in a perpetual polling loop and eventual timeout.
Add a fallback to 'adb shell getprop ro.boot.qemu.avd_name' which reads
the boot property set by the emulator kernel. This property is always
available and doesn't require console authentication.
The fix benefits all consumers of ListDevicesAsync/GetEmulatorAvdNameAsync,
not just BootEmulatorAsync.
Verified locally: BootEmulatorAsync now completes in ~3s (was timing out
at 120s) on emulator v36.4.9 with API 36 image.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔬 Definitive Proof: adb emu avd name Bug on Emulator v36+

Following up on the AVD name detection fix — I did thorough live testing on a properly running emulator (v36.4.9, API 36, arm64) to confirm the behavior.

Test Environment

  • Emulator: v36.4.9.0 (build 14788078), AVD MAUI_Emulator_API_36
  • ADB: v37.0.0-14910828
  • macOS: Darwin 25.3.0 (arm64, Apple M3 Pro)
  • Emulator fully booted: sys.boot_completed=1, adb devices shows device state

Results

MethodResult
adb -s emulator-5554 emu avd nameEMPTY (exit code 0, no output)
adb shell getprop ro.boot.qemu.avd_nameMAUI_Emulator_API_36
echo "avd name" | nc localhost 5554 (raw telnet)MAUI_Emulator_API_36
Console port 5554OPEN (nc -z succeeds)

Analysis

  1. The console port IS accessible — raw telnet to 5554 returns the AVD name correctly
  2. adb emu returns emptyadb uses a different protocol path than raw telnet, and something changed in emulator v36 that breaks it
  3. The emulator warns: The emulator now requires a signed jwt token for gRPC access! — while gRPC (port 8554) differs from telnet console (port 5554), this may affect how adb authenticates to the console

Impact on dotnet/android

The original BootAndroidEmulator.GetRunningAvdName() on main uses the exact same command:

MonoAndroidHelper.RunProcess(adbPath,$"-s {serial} emu avd name", ...);

This means FindRunningEmulatorForAvd would fail to match the AVD → WaitForEmulatorOnline would poll indefinitely → timeout after 120s. This is exactly the bug @jonathanpeppers reported.

Fix Validation

Our getprop ro.boot.qemu.avd_name fallback in AdbRunner.GetEmulatorAvdNameAsync:

  • Completes in 13ms (vs infinite timeout)
  • BootEmulatorAsync end-to-end: 2.8 seconds (vs 120s timeout)
  • All 259 existing tests pass

@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔄 Correction: ADB v37 Regression (not emulator v36 issue)

After deeper investigation, the root cause is more specific:

The Real Issue: ADB v37.0.0 broke adb emu commands

Platform-tools 37.0.0 (ADB 37.0.0-14910828) returns empty output for ALL adb emu subcommands — not just avd name. This is a regression from ADB 36.x where these commands work fine.

I verified with a .NET test program using bothMonoAndroidHelper.RunProcess-style (event-based) and ProcessUtils.StartProcess-style (stream-based) approaches — both get identical empty results. It's not a process execution issue.

Why dotnet/android CI works today

dotnet/android's Configuration.props pins XAPlatformToolsVersion to 36.0.0, so CI uses ADB 36.x where adb emu avd name works correctly. Users who manually upgrade to platform-tools 37 will hit this bug.

The getprop fallback is forward-compatible

The getprop ro.boot.qemu.avd_name fallback works regardless of ADB version, making EmulatorRunner robust against both the current ADB 37 regression and any future changes to the console protocol.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

📋 Research: ADB v37.0.0 adb emu Regression — Evidence & References

Following up on the correction comment with formal evidence supporting the getprop fallback fix.

1. Platform-Tools 37.0.0 is a stable public release

  • Google's official download servers host it:
    • https://dl.google.com/android/repository/platform-tools_r37.0.0-{win,linux,darwin}.zip
  • GitHub Actions macOS 15 runner images ship with it (image version 20260303):
    • Android SDK Platform-Tools | 37.0.0 (source)
    • Same emulator version we tested: Android Emulator | 36.4.9
  • Listed as "Latest Stable Release" in ADB-Explorer's version catalog
  • Google's official release notes at developer.android.com haven't been updated past 36.0.2 yet — the version is released but undocumented

2. Known Google Bug: adb emu returns empty

  • Google Issue Tracker #251776353: "adb avd id returns empty"
  • Originally reported on Intel Macs (platform-tools 34.x), still open/unresolved
  • Our testing confirms it now affects Apple Silicon (M3 Pro) with ADB 37.0.0
  • ALL adb emu subcommands return empty (not just avd name/id) — the entire console-via-ADB pathway is broken
  • Raw telnet to the console port (5554) works perfectly — proving the emulator console itself is fine

3. Why dotnet/android CI is not affected (yet)

  • Configuration.props pins XAPlatformToolsVersion=36.0.0 → CI uses ADB 36.x where adb emu works
  • Any CI/CD using macos-15 GitHub Actions runners WILL be affected — they already have pt 37.0.0
  • Developers using Android Studio (which auto-updates SDK components) will also hit this

4. The getprop fallback is the correct fix

  • getprop ro.boot.qemu.avd_name uses adb shell (standard ADB transport), not the emulator console protocol
  • Works on all ADB versions (35.x, 36.x, 37.x) — we verified this
  • Avoids the broken console-via-ADB pathway entirely
  • Available since Android API 21+ (emulator sets ro.boot.qemu.avd_name at boot)
  • Completes in ~13ms vs 120s timeout with broken adb emu

Summary

EvidenceFinding
Platform-tools 37.0.0✅ Stable, public release on dl.google.com
GitHub Actions macOS 15✅ Ships with pt 37.0.0 + emulator 36.4.9
Google Issue Tracker#251776353 — known open bug
dotnet/android CIUses pt 36.0.0 (pinned) — not yet affected
getprop fallbackWorks on ALL ADB versions — forward-compatible fix

rmarinhoand others added 2 commits March 17, 2026 12:22
Changes:
- Convert EmulatorBootOptions from class to record with init properties
- Change AdditionalArgs from IEnumerable to List for collection initializers
- Remove REMOVED lines from PublicAPI.Unshipped.txt files
- Remove local Log function, inline logger calls
- Simplify while loop condition in WaitForFullBootAsync
- Remove entireProcessTree from process termination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace logger?.Invoke with logger.Invoke using a static
NullLogger no-op delegate in EmulatorRunner, AdbRunner, and
AvdManagerRunner. The constructor assigns logger ?? NullLogger
so the field is never null. Static methods use logger ??= NullLogger
at entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
rmarinhoand others added 4 commits March 18, 2026 09:09
Add structured error classification enum (None, LaunchFailed, Timeout,
Cancelled, Unknown) so consumers can switch on ErrorKind instead of
parsing ErrorMessage strings. Set ErrorKind on all BootEmulatorAsync
return paths.
Addresses review feedback from dotnet/android#10949.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract shared NullLogger to RunnerDefaults utility class
- Remove duplicate NullLogger from AdbRunner, EmulatorRunner, AvdManagerRunner
Addresses review feedback from @jonathanpeppers on PR #284.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment incorrectly claimed the getprop fallback was needed because
'emulator 36+ requires auth for console commands'. After reviewing the
actual adb source code (console.cpp), adb emu handles console auth
automatically — it reads ~/.emulator_console_auth_token and sends it
before any command. This has been the case since ~2016.
The real reason for the fallback is that 'adb emu avd name' can return
empty output on some adb/emulator version combinations (observed with
adb v36). Updated both the XML doc and inline comment to accurately
describe the issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add early process exit detection in BootEmulatorAsync boot polling loop.
Previously, if the emulator failed immediately (e.g., insufficient disk
space, missing AVD), the full 300s timeout was wasted before reporting.
On macOS, the emulator binary forks the real QEMU process and the parent
exits with code 0 immediately. Only non-zero exit codes are treated as
immediate failures; exit code 0 continues polling since the real emulator
runs as a separate process.
Context: dotnet/android#10965
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The build failures is an issue rerunning -- attempt number need to be on artifacts (I'm fixing separately).

Merging shortly.

@jonathanpeppers
jonathanpeppers merged commit 39995cf into mainMar 19, 2026
1 of 2 checks passed
@jonathanpeppers
jonathanpeppers deleted the feature/emulator-runner branch March 19, 2026 13:05
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 20, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Mar 23, 2026
* Use shared EmulatorRunner from android-tools for BootAndroidEmulator
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
### Restore XA0144 for unexpected emulator errors
Update XA0144 message format to accept the ErrorMessage from
EmulatorRunner directly. The default switch case (Unknown and
future error kinds) now uses XA0144 with the full error details
instead of the misleading timeout message XA0145.
Error code mapping:
- XA0143: Launch failed (couldn't start emulator)
- XA0144: Unexpected exit/error (process exited, unknown errors)
- XA0145: Boot timeout (didn't finish in time)
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jul 13, 2026
This skill lets you say:
review this PR: dotnet/android-tools#284
Some example code reviews:
* dotnet/android-tools#283 (review)
* dotnet/android-tools#284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@rmarinho@jonathanpeppers@mattleibow
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add EmulatorRunner for emulator CLI operations - #284

Merged
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner
Mar 19, 2026
Merged

Add EmulatorRunner for emulator CLI operations#284
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner

Conversation

@rmarinho

@rmarinhormarinho commented Feb 23, 2026

Copy link
Copy Markdown
Member

EmulatorRunner: High-level emulator lifecycle management

Adds EmulatorRunner — a managed wrapper over the Android SDK emulator CLI binary, following the same pattern as AdbRunner and AvdManagerRunner.

API Surface

MethodDescription
LaunchEmulator(avdName, options?)Fire-and-forget: starts an emulator process and returns the Process handle. Caller owns the process lifetime. Validates avdName is non-empty.
BootEmulatorAsync(avdName, adb, options?, token?)Full lifecycle: checks if device is already online → checks if emulator process is running → launches emulator → polls adb devices until boot completes or timeout. Returns EmulatorBootResult with status and serial. Disposes Process handle on success (emulator keeps running).
ListAvdNamesAsync(token?)Lists available AVD names via emulator -list-avds. Checks exit code for failures.

Key Design Decisions

  • Naming: LaunchEmulator (fire-and-forget) vs BootEmulatorAsync (full lifecycle) — clear verb distinction matching the emulator domain
  • Kept EmulatorRunner name (not AvdRunner) — follows convention of naming runners after their CLI binary (emulatorEmulatorRunner, adbAdbRunner)
  • Process handle management: LaunchEmulator returns Process (caller-owned); BootEmulatorAsync disposes handle on success (emulator keeps running as detached process), kills+disposes on failure/timeout
  • Pipe draining: LaunchEmulator calls BeginOutputReadLine()/BeginErrorReadLine() after Start() to prevent OS pipe buffer deadlock
  • TryKillProcess: Instance method, uses typed catch (Exception ex) with logger for diagnostics, uses Kill(entireProcessTree: true) on .NET 5+

AdbRunner Enhancements (in this PR)

  • Added optional Action? logger parameter to constructor
  • RunShellCommandAsync(serial, command, ct) — single-string shell command (⚠️ device shell interprets it — documented in XML doc)
  • RunShellCommandAsync(serial, command, args, ct)NEW: structured overload that passes args as separate tokens, bypassing device shell interpretation via exec(). Safer for dynamic input.
  • GetShellPropertyAsync returns first non-empty line (for getprop queries)
  • Shell methods log stderr via logger on non-zero exit codes
  • Fixed RS0026/RS0027: only the most-params overload has optional CancellationToken
  • AVD name detection fix: GetEmulatorAvdNameAsync now falls back to adb shell getprop ro.boot.qemu.avd_name when adb emu avd name returns empty (observed returning empty on some adb/emulator v36 combinations)

Models

  • EmulatorBootOptions — configurable timeout (default 120s), poll interval (default 2s), cold boot, extra args (IEnumerable?)
  • EmulatorBootResult — immutable record with init-only properties: Status (enum), Serial, Message. Statuses: Success, AlreadyRunning, Timeout, Error

Bug Fix: AVD Name Detection on Emulator v36+

The adb emu avd name console command can return empty output on some adb/emulator version combinations (observed with adb v36). This caused BootEmulatorAsync to never match the running emulator by AVD name, resulting in a perpetual polling loop and eventual timeout.

Root cause: GetEmulatorAvdNameAsync relied solely on adb -s <serial> emu avd name. On some adb/emulator version combinations this command silently returns empty output (exit code 0, no content). The exact cause is unclear but the getprop fallback provides reliable AVD name resolution regardless.

Fix: Added fallback to adb shell getprop ro.boot.qemu.avd_name, which reads the boot property set by the emulator kernel. This property is always available via the standard adb shell interface and does not depend on the emulator console protocol.

Verified: BootEmulatorAsync now completes in ~3s (was timing out at 120s) on emulator v36.4.9 with API 36 image.

Consumer PR

  • dotnet/android #10949 — replaces BootAndroidEmulator MSBuild task (~454 lines) with a ~180-line wrapper delegating to EmulatorRunner.BootEmulatorAsync()

Tests (24 EmulatorRunner + 9 AdbRunner = 33 total)

EmulatorRunner (24):

  • Parse emulator -list-avds output (empty, single, multiple, blank lines, Windows newlines) — 4 tests
  • Constructor validation (null/empty/whitespace tool path) — 3 tests
  • LaunchEmulator argument validation (null, empty, whitespace AVD name) — 3 tests
  • BootEmulatorAsync lifecycle: already online device, already running AVD, successful boot after polling, timeout, launch failure, cancellation token — 6 tests
  • BootEmulatorAsync validation: invalid timeout, invalid poll interval, null AdbRunner, empty device name — 4 tests
  • Ported from dotnet/android BootAndroidEmulatorTests: physical device passthrough, AdditionalArgs forwarding, ColdBoot flag, cancellation abort — 4 tests

AdbRunner (9):

  • FirstNonEmptyLine parsing (null, empty, whitespace, single value, multiline, mixed) — 9 tests

Review Feedback Addressed

  • LaunchEmulator validates avdName parameter (throws ArgumentException)
  • LaunchEmulator drains stdout/stderr pipes via BeginOutputReadLine()/BeginErrorReadLine()
  • RunShellCommandAsync returns full stdout (not just first line)
  • ✅ Added structured RunShellCommandAsync overload (no shell interpretation)
  • ✅ Added 12 new unit tests (LaunchEmulator validation + FirstNonEmptyLine parsing)
  • ✅ Shell methods log stderr via logger on failure
  • ✅ Removed TOCTOU HasExited guard from TryKillProcess
  • ✅ Process handle disposed on successful boot (no handle leak)
  • ListAvdNamesAsync checks exit code
  • TryKillProcess uses typed catch (Exception ex) with logging
  • RunShellCommandAsync XML doc warns about shell interpretation
  • ✅ Fixed RS0026/RS0027 PublicAPI analyzer warnings
  • EmulatorBootResult uses init-only properties (immutable record)
  • ✅ Ported 6 additional tests from dotnet/android BootAndroidEmulatorTests
  • ✅ Fixed AVD name detection for emulator v36+ (getprop fallback)

CopilotAI review requested due to automatic review settings February 23, 2026 17:39

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 adds a new EmulatorRunner to Xamarin.Android.Tools.AndroidSdk intended to wrap Android emulator CLI operations, alongside new shared infrastructure for running Android SDK command-line tools with environment setup and result modeling.

Changes:

  • Added EmulatorRunner to start an AVD, stop an emulator, and list available AVD names.
  • Added AndroidToolRunner utility to run SDK tools sync/async (with timeouts) and to start long-running background processes.
  • Added AndroidEnvironmentHelper and ToolRunnerResult / ToolRunnerResult<T> to standardize tool environment and execution results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

FileDescription
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.csIntroduces emulator wrapper methods (start/stop/list AVDs) built on the tool runner infrastructure.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.csAdds process execution helpers (sync/async + background) with timeout/output capture.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.csAdds env var setup and mapping helpers (ABI/API/tag display names).
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.csAdds a shared result model for tool execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
@rmarinhormarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho
rmarinho requested a review from RedthFebruary 23, 2026 17:51
@jonathanpeppers

Copy link
Copy Markdown
Member

I'd like to get the System.Diagnostics.Process code unified like mentioned here:

rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #284 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:
- Methods now throw InvalidOperationException on failure
- Uses ProcessUtils.RunToolAsync() and StartToolBackground()
- Removed complex ToolRunnerResult wrapper types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from f1aa44f to 826d4aaCompareFebruary 24, 2026 14:15
rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #283/#284 feedback to use existing ProcessUtils.
Simplifies API by throwing exceptions on failure instead of
returning result types with error states.
Changes:
- AdbRunner: Simplified using ProcessUtils.RunToolAsync()
- EmulatorRunner: Uses ProcessUtils.StartToolBackground()
- Removed duplicate AndroidDeviceInfo from Models directory
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 2 times, most recently from 39617c8 to 5268300CompareFebruary 24, 2026 19:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 5 times, most recently from 1b10889 to ee31e4bCompareMarch 3, 2026 14:36
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from ee31e4b to 3a788bbCompareMarch 3, 2026 18:23
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Review feedback addressed — commit references

FeedbackCommitDetails
Port BootAndroidEmulator logic from dotnet/android0088e39BootAndWaitAsync with 3-phase boot, GetShellPropertyAsync, RunShellCommandAsync, 6 new tests

New files:

  • Models/EmulatorBootResult.cs, Models/EmulatorBootOptions.cs
  • Tests: 6 async boot scenarios ported from BootAndroidEmulatorTests.cs

Modified:

  • Runners/EmulatorRunner.csBootAndWaitAsync, FindRunningAvdSerial, WaitForFullBootAsync
  • Runners/AdbRunner.csGetShellPropertyAsync, RunShellCommandAsync (+ ListDevicesAsync made virtual for testability)

Draft dotnet/android consumer PR to follow.

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 AI Review Summary

Found 7 issues: 1 correctness, 2 error handling, 1 API design, 1 code duplication, 1 code organization, 1 naming.

  • Correctness: StartAvd redirects stdout/stderr but never drains the pipes — OS buffer fill will deadlock the emulator process (EmulatorRunner.cs:74)
  • API design: AdditionalArgs is a single string — will be treated as one argument by ProcessUtils.ArgumentList, breaking multi-token args like -gpu swiftshader_indirect (EmulatorBootOptions.cs:14)
  • Error handling: ListDevicesAsync ignores the exit code from ProcessUtils.StartProcess while sibling methods in AvdManagerRunner check it consistently (AdbRunner.cs:72)
  • Code duplication: AvdManagerRunner.AvdManagerPath reimplements the cmdline-tools version scanning that ProcessUtils.FindCmdlineTool (added in this same PR) already provides (AvdManagerRunner.cs:33)
  • Error handling: Bare catch { } swallows all exceptions without capturing them (AdbRunner.cs:107)

👍 Solid three-phase boot logic ported faithfully from dotnet/android. Good use of virtual on AdbRunner methods to enable clean test mocking. Thorough test coverage with 13+ unit tests covering parsing, edge cases, and the full boot flow. Nice extraction of AndroidEnvironmentHelper for shared env var setup.


This review was generated by the android-tools-reviewer skill based on review guidelines established by @jonathanpeppers.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
jonathanpeppers added a commit that referenced this pull request Mar 4, 2026
This skill let's you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
rmarinho added a commit to dotnet/android that referenced this pull request Mar 16, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

@jonathanpeppers Here's the dotnet/android consumer PR you requested: dotnet/android#10948

It replaces the 454-line BootAndroidEmulator task with a ~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync(). Same MSBuild interface, same error codes (XA0143/XA0145), but all the process management and polling logic is now in the shared library.

The PR is in draft since it depends on this PR (#284) merging first — the submodule currently points to feature/emulator-runner.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Note: the consumer PR was recreated after a branch rename — the correct link is now dotnet/android#10949 (the previous #10948 was auto-closed).

Port additional test coverage from dotnet/android PR #10949:
- AlreadyOnlinePhysicalDevice: physical device serial passthrough
- AdditionalArgs_PassedToLaunchEmulator: verify extra args reach process
- CancellationToken_AbortsBoot: cancellation during polling phase
- ColdBoot_PassesNoSnapshotLoad: verify -no-snapshot-load flag
- BootEmulatorAsync_NullAdbRunner_Throws: null guard validation
- BootEmulatorAsync_EmptyDeviceName_Throws: empty string guard
Total EmulatorRunner test count: 24 (18 existing + 6 new)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried it locally, but it errors:

Image

Is anything different from the code <BootAndroidEmulator/> had before?

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
The 'adb emu avd name' console command returns empty output on emulator
v36+ due to gRPC authentication requirements. This causes
BootEmulatorAsync to never match the running emulator by AVD name,
resulting in a perpetual polling loop and eventual timeout.
Add a fallback to 'adb shell getprop ro.boot.qemu.avd_name' which reads
the boot property set by the emulator kernel. This property is always
available and doesn't require console authentication.
The fix benefits all consumers of ListDevicesAsync/GetEmulatorAvdNameAsync,
not just BootEmulatorAsync.
Verified locally: BootEmulatorAsync now completes in ~3s (was timing out
at 120s) on emulator v36.4.9 with API 36 image.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔬 Definitive Proof: adb emu avd name Bug on Emulator v36+

Following up on the AVD name detection fix — I did thorough live testing on a properly running emulator (v36.4.9, API 36, arm64) to confirm the behavior.

Test Environment

  • Emulator: v36.4.9.0 (build 14788078), AVD MAUI_Emulator_API_36
  • ADB: v37.0.0-14910828
  • macOS: Darwin 25.3.0 (arm64, Apple M3 Pro)
  • Emulator fully booted: sys.boot_completed=1, adb devices shows device state

Results

MethodResult
adb -s emulator-5554 emu avd nameEMPTY (exit code 0, no output)
adb shell getprop ro.boot.qemu.avd_nameMAUI_Emulator_API_36
echo "avd name" | nc localhost 5554 (raw telnet)MAUI_Emulator_API_36
Console port 5554OPEN (nc -z succeeds)

Analysis

  1. The console port IS accessible — raw telnet to 5554 returns the AVD name correctly
  2. adb emu returns emptyadb uses a different protocol path than raw telnet, and something changed in emulator v36 that breaks it
  3. The emulator warns: The emulator now requires a signed jwt token for gRPC access! — while gRPC (port 8554) differs from telnet console (port 5554), this may affect how adb authenticates to the console

Impact on dotnet/android

The original BootAndroidEmulator.GetRunningAvdName() on main uses the exact same command:

MonoAndroidHelper.RunProcess(adbPath,$"-s {serial} emu avd name", ...);

This means FindRunningEmulatorForAvd would fail to match the AVD → WaitForEmulatorOnline would poll indefinitely → timeout after 120s. This is exactly the bug @jonathanpeppers reported.

Fix Validation

Our getprop ro.boot.qemu.avd_name fallback in AdbRunner.GetEmulatorAvdNameAsync:

  • Completes in 13ms (vs infinite timeout)
  • BootEmulatorAsync end-to-end: 2.8 seconds (vs 120s timeout)
  • All 259 existing tests pass

@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔄 Correction: ADB v37 Regression (not emulator v36 issue)

After deeper investigation, the root cause is more specific:

The Real Issue: ADB v37.0.0 broke adb emu commands

Platform-tools 37.0.0 (ADB 37.0.0-14910828) returns empty output for ALL adb emu subcommands — not just avd name. This is a regression from ADB 36.x where these commands work fine.

I verified with a .NET test program using bothMonoAndroidHelper.RunProcess-style (event-based) and ProcessUtils.StartProcess-style (stream-based) approaches — both get identical empty results. It's not a process execution issue.

Why dotnet/android CI works today

dotnet/android's Configuration.props pins XAPlatformToolsVersion to 36.0.0, so CI uses ADB 36.x where adb emu avd name works correctly. Users who manually upgrade to platform-tools 37 will hit this bug.

The getprop fallback is forward-compatible

The getprop ro.boot.qemu.avd_name fallback works regardless of ADB version, making EmulatorRunner robust against both the current ADB 37 regression and any future changes to the console protocol.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

📋 Research: ADB v37.0.0 adb emu Regression — Evidence & References

Following up on the correction comment with formal evidence supporting the getprop fallback fix.

1. Platform-Tools 37.0.0 is a stable public release

  • Google's official download servers host it:
    • https://dl.google.com/android/repository/platform-tools_r37.0.0-{win,linux,darwin}.zip
  • GitHub Actions macOS 15 runner images ship with it (image version 20260303):
    • Android SDK Platform-Tools | 37.0.0 (source)
    • Same emulator version we tested: Android Emulator | 36.4.9
  • Listed as "Latest Stable Release" in ADB-Explorer's version catalog
  • Google's official release notes at developer.android.com haven't been updated past 36.0.2 yet — the version is released but undocumented

2. Known Google Bug: adb emu returns empty

  • Google Issue Tracker #251776353: "adb avd id returns empty"
  • Originally reported on Intel Macs (platform-tools 34.x), still open/unresolved
  • Our testing confirms it now affects Apple Silicon (M3 Pro) with ADB 37.0.0
  • ALL adb emu subcommands return empty (not just avd name/id) — the entire console-via-ADB pathway is broken
  • Raw telnet to the console port (5554) works perfectly — proving the emulator console itself is fine

3. Why dotnet/android CI is not affected (yet)

  • Configuration.props pins XAPlatformToolsVersion=36.0.0 → CI uses ADB 36.x where adb emu works
  • Any CI/CD using macos-15 GitHub Actions runners WILL be affected — they already have pt 37.0.0
  • Developers using Android Studio (which auto-updates SDK components) will also hit this

4. The getprop fallback is the correct fix

  • getprop ro.boot.qemu.avd_name uses adb shell (standard ADB transport), not the emulator console protocol
  • Works on all ADB versions (35.x, 36.x, 37.x) — we verified this
  • Avoids the broken console-via-ADB pathway entirely
  • Available since Android API 21+ (emulator sets ro.boot.qemu.avd_name at boot)
  • Completes in ~13ms vs 120s timeout with broken adb emu

Summary

EvidenceFinding
Platform-tools 37.0.0✅ Stable, public release on dl.google.com
GitHub Actions macOS 15✅ Ships with pt 37.0.0 + emulator 36.4.9
Google Issue Tracker#251776353 — known open bug
dotnet/android CIUses pt 36.0.0 (pinned) — not yet affected
getprop fallbackWorks on ALL ADB versions — forward-compatible fix

rmarinhoand others added 2 commits March 17, 2026 12:22
Changes:
- Convert EmulatorBootOptions from class to record with init properties
- Change AdditionalArgs from IEnumerable to List for collection initializers
- Remove REMOVED lines from PublicAPI.Unshipped.txt files
- Remove local Log function, inline logger calls
- Simplify while loop condition in WaitForFullBootAsync
- Remove entireProcessTree from process termination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace logger?.Invoke with logger.Invoke using a static
NullLogger no-op delegate in EmulatorRunner, AdbRunner, and
AvdManagerRunner. The constructor assigns logger ?? NullLogger
so the field is never null. Static methods use logger ??= NullLogger
at entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
rmarinhoand others added 4 commits March 18, 2026 09:09
Add structured error classification enum (None, LaunchFailed, Timeout,
Cancelled, Unknown) so consumers can switch on ErrorKind instead of
parsing ErrorMessage strings. Set ErrorKind on all BootEmulatorAsync
return paths.
Addresses review feedback from dotnet/android#10949.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract shared NullLogger to RunnerDefaults utility class
- Remove duplicate NullLogger from AdbRunner, EmulatorRunner, AvdManagerRunner
Addresses review feedback from @jonathanpeppers on PR #284.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment incorrectly claimed the getprop fallback was needed because
'emulator 36+ requires auth for console commands'. After reviewing the
actual adb source code (console.cpp), adb emu handles console auth
automatically — it reads ~/.emulator_console_auth_token and sends it
before any command. This has been the case since ~2016.
The real reason for the fallback is that 'adb emu avd name' can return
empty output on some adb/emulator version combinations (observed with
adb v36). Updated both the XML doc and inline comment to accurately
describe the issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add early process exit detection in BootEmulatorAsync boot polling loop.
Previously, if the emulator failed immediately (e.g., insufficient disk
space, missing AVD), the full 300s timeout was wasted before reporting.
On macOS, the emulator binary forks the real QEMU process and the parent
exits with code 0 immediately. Only non-zero exit codes are treated as
immediate failures; exit code 0 continues polling since the real emulator
runs as a separate process.
Context: dotnet/android#10965
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The build failures is an issue rerunning -- attempt number need to be on artifacts (I'm fixing separately).

Merging shortly.

@jonathanpeppers
jonathanpeppers merged commit 39995cf into mainMar 19, 2026
1 of 2 checks passed
@jonathanpeppers
jonathanpeppers deleted the feature/emulator-runner branch March 19, 2026 13:05
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 20, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Mar 23, 2026
* Use shared EmulatorRunner from android-tools for BootAndroidEmulator
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
### Restore XA0144 for unexpected emulator errors
Update XA0144 message format to accept the ErrorMessage from
EmulatorRunner directly. The default switch case (Unknown and
future error kinds) now uses XA0144 with the full error details
instead of the misleading timeout message XA0145.
Error code mapping:
- XA0143: Launch failed (couldn't start emulator)
- XA0144: Unexpected exit/error (process exited, unknown errors)
- XA0145: Boot timeout (didn't finish in time)
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jul 13, 2026
This skill lets you say:
review this PR: dotnet/android-tools#284
Some example code reviews:
* dotnet/android-tools#283 (review)
* dotnet/android-tools#284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@rmarinho@jonathanpeppers@mattleibow
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Add EmulatorRunner for emulator CLI operations - #284

Merged
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner
Mar 19, 2026
Merged

Add EmulatorRunner for emulator CLI operations#284
jonathanpeppers merged 17 commits into
mainfrom
feature/emulator-runner

Conversation

@rmarinho

@rmarinhormarinho commented Feb 23, 2026

Copy link
Copy Markdown
Member

EmulatorRunner: High-level emulator lifecycle management

Adds EmulatorRunner — a managed wrapper over the Android SDK emulator CLI binary, following the same pattern as AdbRunner and AvdManagerRunner.

API Surface

MethodDescription
LaunchEmulator(avdName, options?)Fire-and-forget: starts an emulator process and returns the Process handle. Caller owns the process lifetime. Validates avdName is non-empty.
BootEmulatorAsync(avdName, adb, options?, token?)Full lifecycle: checks if device is already online → checks if emulator process is running → launches emulator → polls adb devices until boot completes or timeout. Returns EmulatorBootResult with status and serial. Disposes Process handle on success (emulator keeps running).
ListAvdNamesAsync(token?)Lists available AVD names via emulator -list-avds. Checks exit code for failures.

Key Design Decisions

  • Naming: LaunchEmulator (fire-and-forget) vs BootEmulatorAsync (full lifecycle) — clear verb distinction matching the emulator domain
  • Kept EmulatorRunner name (not AvdRunner) — follows convention of naming runners after their CLI binary (emulatorEmulatorRunner, adbAdbRunner)
  • Process handle management: LaunchEmulator returns Process (caller-owned); BootEmulatorAsync disposes handle on success (emulator keeps running as detached process), kills+disposes on failure/timeout
  • Pipe draining: LaunchEmulator calls BeginOutputReadLine()/BeginErrorReadLine() after Start() to prevent OS pipe buffer deadlock
  • TryKillProcess: Instance method, uses typed catch (Exception ex) with logger for diagnostics, uses Kill(entireProcessTree: true) on .NET 5+

AdbRunner Enhancements (in this PR)

  • Added optional Action? logger parameter to constructor
  • RunShellCommandAsync(serial, command, ct) — single-string shell command (⚠️ device shell interprets it — documented in XML doc)
  • RunShellCommandAsync(serial, command, args, ct)NEW: structured overload that passes args as separate tokens, bypassing device shell interpretation via exec(). Safer for dynamic input.
  • GetShellPropertyAsync returns first non-empty line (for getprop queries)
  • Shell methods log stderr via logger on non-zero exit codes
  • Fixed RS0026/RS0027: only the most-params overload has optional CancellationToken
  • AVD name detection fix: GetEmulatorAvdNameAsync now falls back to adb shell getprop ro.boot.qemu.avd_name when adb emu avd name returns empty (observed returning empty on some adb/emulator v36 combinations)

Models

  • EmulatorBootOptions — configurable timeout (default 120s), poll interval (default 2s), cold boot, extra args (IEnumerable?)
  • EmulatorBootResult — immutable record with init-only properties: Status (enum), Serial, Message. Statuses: Success, AlreadyRunning, Timeout, Error

Bug Fix: AVD Name Detection on Emulator v36+

The adb emu avd name console command can return empty output on some adb/emulator version combinations (observed with adb v36). This caused BootEmulatorAsync to never match the running emulator by AVD name, resulting in a perpetual polling loop and eventual timeout.

Root cause: GetEmulatorAvdNameAsync relied solely on adb -s <serial> emu avd name. On some adb/emulator version combinations this command silently returns empty output (exit code 0, no content). The exact cause is unclear but the getprop fallback provides reliable AVD name resolution regardless.

Fix: Added fallback to adb shell getprop ro.boot.qemu.avd_name, which reads the boot property set by the emulator kernel. This property is always available via the standard adb shell interface and does not depend on the emulator console protocol.

Verified: BootEmulatorAsync now completes in ~3s (was timing out at 120s) on emulator v36.4.9 with API 36 image.

Consumer PR

  • dotnet/android #10949 — replaces BootAndroidEmulator MSBuild task (~454 lines) with a ~180-line wrapper delegating to EmulatorRunner.BootEmulatorAsync()

Tests (24 EmulatorRunner + 9 AdbRunner = 33 total)

EmulatorRunner (24):

  • Parse emulator -list-avds output (empty, single, multiple, blank lines, Windows newlines) — 4 tests
  • Constructor validation (null/empty/whitespace tool path) — 3 tests
  • LaunchEmulator argument validation (null, empty, whitespace AVD name) — 3 tests
  • BootEmulatorAsync lifecycle: already online device, already running AVD, successful boot after polling, timeout, launch failure, cancellation token — 6 tests
  • BootEmulatorAsync validation: invalid timeout, invalid poll interval, null AdbRunner, empty device name — 4 tests
  • Ported from dotnet/android BootAndroidEmulatorTests: physical device passthrough, AdditionalArgs forwarding, ColdBoot flag, cancellation abort — 4 tests

AdbRunner (9):

  • FirstNonEmptyLine parsing (null, empty, whitespace, single value, multiline, mixed) — 9 tests

Review Feedback Addressed

  • LaunchEmulator validates avdName parameter (throws ArgumentException)
  • LaunchEmulator drains stdout/stderr pipes via BeginOutputReadLine()/BeginErrorReadLine()
  • RunShellCommandAsync returns full stdout (not just first line)
  • ✅ Added structured RunShellCommandAsync overload (no shell interpretation)
  • ✅ Added 12 new unit tests (LaunchEmulator validation + FirstNonEmptyLine parsing)
  • ✅ Shell methods log stderr via logger on failure
  • ✅ Removed TOCTOU HasExited guard from TryKillProcess
  • ✅ Process handle disposed on successful boot (no handle leak)
  • ListAvdNamesAsync checks exit code
  • TryKillProcess uses typed catch (Exception ex) with logging
  • RunShellCommandAsync XML doc warns about shell interpretation
  • ✅ Fixed RS0026/RS0027 PublicAPI analyzer warnings
  • EmulatorBootResult uses init-only properties (immutable record)
  • ✅ Ported 6 additional tests from dotnet/android BootAndroidEmulatorTests
  • ✅ Fixed AVD name detection for emulator v36+ (getprop fallback)

CopilotAI review requested due to automatic review settings February 23, 2026 17:39

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 adds a new EmulatorRunner to Xamarin.Android.Tools.AndroidSdk intended to wrap Android emulator CLI operations, alongside new shared infrastructure for running Android SDK command-line tools with environment setup and result modeling.

Changes:

  • Added EmulatorRunner to start an AVD, stop an emulator, and list available AVD names.
  • Added AndroidToolRunner utility to run SDK tools sync/async (with timeouts) and to start long-running background processes.
  • Added AndroidEnvironmentHelper and ToolRunnerResult / ToolRunnerResult<T> to standardize tool environment and execution results.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.

FileDescription
src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.csIntroduces emulator wrapper methods (start/stop/list AVDs) built on the tool runner infrastructure.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.csAdds process execution helpers (sync/async + background) with timeout/output capture.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.csAdds env var setup and mapping helpers (ABI/API/tag display names).
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.csAdds a shared result model for tool execution.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Outdated
@rmarinhormarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho
rmarinho requested a review from RedthFebruary 23, 2026 17:51
@jonathanpeppers

Copy link
Copy Markdown
Member

I'd like to get the System.Diagnostics.Process code unified like mentioned here:

rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #284 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:
- Methods now throw InvalidOperationException on failure
- Uses ProcessUtils.RunToolAsync() and StartToolBackground()
- Removed complex ToolRunnerResult wrapper types
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from f1aa44f to 826d4aaCompareFebruary 24, 2026 14:15
rmarinho added a commit that referenced this pull request Feb 24, 2026
Addresses PR #283/#284 feedback to use existing ProcessUtils.
Simplifies API by throwing exceptions on failure instead of
returning result types with error states.
Changes:
- AdbRunner: Simplified using ProcessUtils.RunToolAsync()
- EmulatorRunner: Uses ProcessUtils.StartToolBackground()
- Removed duplicate AndroidDeviceInfo from Models directory
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 2 times, most recently from 39617c8 to 5268300CompareFebruary 24, 2026 19:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch 5 times, most recently from 1b10889 to ee31e4bCompareMarch 3, 2026 14:36
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
@rmarinho
rmarinhoforce-pushed the feature/emulator-runner branch from ee31e4b to 3a788bbCompareMarch 3, 2026 18:23
@rmarinho

Copy link
Copy Markdown
MemberAuthor

Review feedback addressed — commit references

FeedbackCommitDetails
Port BootAndroidEmulator logic from dotnet/android0088e39BootAndWaitAsync with 3-phase boot, GetShellPropertyAsync, RunShellCommandAsync, 6 new tests

New files:

  • Models/EmulatorBootResult.cs, Models/EmulatorBootOptions.cs
  • Tests: 6 async boot scenarios ported from BootAndroidEmulatorTests.cs

Modified:

  • Runners/EmulatorRunner.csBootAndWaitAsync, FindRunningAvdSerial, WaitForFullBootAsync
  • Runners/AdbRunner.csGetShellPropertyAsync, RunShellCommandAsync (+ ListDevicesAsync made virtual for testability)

Draft dotnet/android consumer PR to follow.

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 AI Review Summary

Found 7 issues: 1 correctness, 2 error handling, 1 API design, 1 code duplication, 1 code organization, 1 naming.

  • Correctness: StartAvd redirects stdout/stderr but never drains the pipes — OS buffer fill will deadlock the emulator process (EmulatorRunner.cs:74)
  • API design: AdditionalArgs is a single string — will be treated as one argument by ProcessUtils.ArgumentList, breaking multi-token args like -gpu swiftshader_indirect (EmulatorBootOptions.cs:14)
  • Error handling: ListDevicesAsync ignores the exit code from ProcessUtils.StartProcess while sibling methods in AvdManagerRunner check it consistently (AdbRunner.cs:72)
  • Code duplication: AvdManagerRunner.AvdManagerPath reimplements the cmdline-tools version scanning that ProcessUtils.FindCmdlineTool (added in this same PR) already provides (AvdManagerRunner.cs:33)
  • Error handling: Bare catch { } swallows all exceptions without capturing them (AdbRunner.cs:107)

👍 Solid three-phase boot logic ported faithfully from dotnet/android. Good use of virtual on AdbRunner methods to enable clean test mocking. Thorough test coverage with 13+ unit tests covering parsing, edge cases, and the full boot flow. Nice extraction of AndroidEnvironmentHelper for shared env var setup.


This review was generated by the android-tools-reviewer skill based on review guidelines established by @jonathanpeppers.

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/AdbDeviceInfo.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
jonathanpeppers added a commit that referenced this pull request Mar 4, 2026
This skill let's you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
rmarinho added a commit to dotnet/android that referenced this pull request Mar 16, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

@jonathanpeppers Here's the dotnet/android consumer PR you requested: dotnet/android#10948

It replaces the 454-line BootAndroidEmulator task with a ~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync(). Same MSBuild interface, same error codes (XA0143/XA0145), but all the process management and polling logic is now in the shared library.

The PR is in draft since it depends on this PR (#284) merging first — the submodule currently points to feature/emulator-runner.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

Note: the consumer PR was recreated after a branch rename — the correct link is now dotnet/android#10949 (the previous #10948 was auto-closed).

Port additional test coverage from dotnet/android PR #10949:
- AlreadyOnlinePhysicalDevice: physical device serial passthrough
- AdditionalArgs_PassedToLaunchEmulator: verify extra args reach process
- CancellationToken_AbortsBoot: cancellation during polling phase
- ColdBoot_PassesNoSnapshotLoad: verify -no-snapshot-load flag
- BootEmulatorAsync_NullAdbRunner_Throws: null guard validation
- BootEmulatorAsync_EmptyDeviceName_Throws: empty string guard
Total EmulatorRunner test count: 24 (18 existing + 6 new)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I tried it locally, but it errors:

Image

Is anything different from the code <BootAndroidEmulator/> had before?

Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Models/EmulatorBootOptions.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs Outdated
The 'adb emu avd name' console command returns empty output on emulator
v36+ due to gRPC authentication requirements. This causes
BootEmulatorAsync to never match the running emulator by AVD name,
resulting in a perpetual polling loop and eventual timeout.
Add a fallback to 'adb shell getprop ro.boot.qemu.avd_name' which reads
the boot property set by the emulator kernel. This property is always
available and doesn't require console authentication.
The fix benefits all consumers of ListDevicesAsync/GetEmulatorAvdNameAsync,
not just BootEmulatorAsync.
Verified locally: BootEmulatorAsync now completes in ~3s (was timing out
at 120s) on emulator v36.4.9 with API 36 image.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔬 Definitive Proof: adb emu avd name Bug on Emulator v36+

Following up on the AVD name detection fix — I did thorough live testing on a properly running emulator (v36.4.9, API 36, arm64) to confirm the behavior.

Test Environment

  • Emulator: v36.4.9.0 (build 14788078), AVD MAUI_Emulator_API_36
  • ADB: v37.0.0-14910828
  • macOS: Darwin 25.3.0 (arm64, Apple M3 Pro)
  • Emulator fully booted: sys.boot_completed=1, adb devices shows device state

Results

MethodResult
adb -s emulator-5554 emu avd nameEMPTY (exit code 0, no output)
adb shell getprop ro.boot.qemu.avd_nameMAUI_Emulator_API_36
echo "avd name" | nc localhost 5554 (raw telnet)MAUI_Emulator_API_36
Console port 5554OPEN (nc -z succeeds)

Analysis

  1. The console port IS accessible — raw telnet to 5554 returns the AVD name correctly
  2. adb emu returns emptyadb uses a different protocol path than raw telnet, and something changed in emulator v36 that breaks it
  3. The emulator warns: The emulator now requires a signed jwt token for gRPC access! — while gRPC (port 8554) differs from telnet console (port 5554), this may affect how adb authenticates to the console

Impact on dotnet/android

The original BootAndroidEmulator.GetRunningAvdName() on main uses the exact same command:

MonoAndroidHelper.RunProcess(adbPath,$"-s {serial} emu avd name", ...);

This means FindRunningEmulatorForAvd would fail to match the AVD → WaitForEmulatorOnline would poll indefinitely → timeout after 120s. This is exactly the bug @jonathanpeppers reported.

Fix Validation

Our getprop ro.boot.qemu.avd_name fallback in AdbRunner.GetEmulatorAvdNameAsync:

  • Completes in 13ms (vs infinite timeout)
  • BootEmulatorAsync end-to-end: 2.8 seconds (vs 120s timeout)
  • All 259 existing tests pass

@rmarinho

Copy link
Copy Markdown
MemberAuthor

🔄 Correction: ADB v37 Regression (not emulator v36 issue)

After deeper investigation, the root cause is more specific:

The Real Issue: ADB v37.0.0 broke adb emu commands

Platform-tools 37.0.0 (ADB 37.0.0-14910828) returns empty output for ALL adb emu subcommands — not just avd name. This is a regression from ADB 36.x where these commands work fine.

I verified with a .NET test program using bothMonoAndroidHelper.RunProcess-style (event-based) and ProcessUtils.StartProcess-style (stream-based) approaches — both get identical empty results. It's not a process execution issue.

Why dotnet/android CI works today

dotnet/android's Configuration.props pins XAPlatformToolsVersion to 36.0.0, so CI uses ADB 36.x where adb emu avd name works correctly. Users who manually upgrade to platform-tools 37 will hit this bug.

The getprop fallback is forward-compatible

The getprop ro.boot.qemu.avd_name fallback works regardless of ADB version, making EmulatorRunner robust against both the current ADB 37 regression and any future changes to the console protocol.

@rmarinho

Copy link
Copy Markdown
MemberAuthor

📋 Research: ADB v37.0.0 adb emu Regression — Evidence & References

Following up on the correction comment with formal evidence supporting the getprop fallback fix.

1. Platform-Tools 37.0.0 is a stable public release

  • Google's official download servers host it:
    • https://dl.google.com/android/repository/platform-tools_r37.0.0-{win,linux,darwin}.zip
  • GitHub Actions macOS 15 runner images ship with it (image version 20260303):
    • Android SDK Platform-Tools | 37.0.0 (source)
    • Same emulator version we tested: Android Emulator | 36.4.9
  • Listed as "Latest Stable Release" in ADB-Explorer's version catalog
  • Google's official release notes at developer.android.com haven't been updated past 36.0.2 yet — the version is released but undocumented

2. Known Google Bug: adb emu returns empty

  • Google Issue Tracker #251776353: "adb avd id returns empty"
  • Originally reported on Intel Macs (platform-tools 34.x), still open/unresolved
  • Our testing confirms it now affects Apple Silicon (M3 Pro) with ADB 37.0.0
  • ALL adb emu subcommands return empty (not just avd name/id) — the entire console-via-ADB pathway is broken
  • Raw telnet to the console port (5554) works perfectly — proving the emulator console itself is fine

3. Why dotnet/android CI is not affected (yet)

  • Configuration.props pins XAPlatformToolsVersion=36.0.0 → CI uses ADB 36.x where adb emu works
  • Any CI/CD using macos-15 GitHub Actions runners WILL be affected — they already have pt 37.0.0
  • Developers using Android Studio (which auto-updates SDK components) will also hit this

4. The getprop fallback is the correct fix

  • getprop ro.boot.qemu.avd_name uses adb shell (standard ADB transport), not the emulator console protocol
  • Works on all ADB versions (35.x, 36.x, 37.x) — we verified this
  • Avoids the broken console-via-ADB pathway entirely
  • Available since Android API 21+ (emulator sets ro.boot.qemu.avd_name at boot)
  • Completes in ~13ms vs 120s timeout with broken adb emu

Summary

EvidenceFinding
Platform-tools 37.0.0✅ Stable, public release on dl.google.com
GitHub Actions macOS 15✅ Ships with pt 37.0.0 + emulator 36.4.9
Google Issue Tracker#251776353 — known open bug
dotnet/android CIUses pt 36.0.0 (pinned) — not yet affected
getprop fallbackWorks on ALL ADB versions — forward-compatible fix

rmarinhoand others added 2 commits March 17, 2026 12:22
Changes:
- Convert EmulatorBootOptions from class to record with init properties
- Change AdditionalArgs from IEnumerable to List for collection initializers
- Remove REMOVED lines from PublicAPI.Unshipped.txt files
- Remove local Log function, inline logger calls
- Simplify while loop condition in WaitForFullBootAsync
- Remove entireProcessTree from process termination
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace logger?.Invoke with logger.Invoke using a static
NullLogger no-op delegate in EmulatorRunner, AdbRunner, and
AvdManagerRunner. The constructor assigns logger ?? NullLogger
so the field is never null. Static methods use logger ??= NullLogger
at entry.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs Outdated
Comment threadsrc/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Outdated
rmarinhoand others added 4 commits March 18, 2026 09:09
Add structured error classification enum (None, LaunchFailed, Timeout,
Cancelled, Unknown) so consumers can switch on ErrorKind instead of
parsing ErrorMessage strings. Set ErrorKind on all BootEmulatorAsync
return paths.
Addresses review feedback from dotnet/android#10949.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Extract shared NullLogger to RunnerDefaults utility class
- Remove duplicate NullLogger from AdbRunner, EmulatorRunner, AvdManagerRunner
Addresses review feedback from @jonathanpeppers on PR #284.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The comment incorrectly claimed the getprop fallback was needed because
'emulator 36+ requires auth for console commands'. After reviewing the
actual adb source code (console.cpp), adb emu handles console auth
automatically — it reads ~/.emulator_console_auth_token and sends it
before any command. This has been the case since ~2016.
The real reason for the fallback is that 'adb emu avd name' can return
empty output on some adb/emulator version combinations (observed with
adb v36). Updated both the XML doc and inline comment to accurately
describe the issue.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add early process exit detection in BootEmulatorAsync boot polling loop.
Previously, if the emulator failed immediately (e.g., insufficient disk
space, missing AVD), the full 300s timeout was wasted before reporting.
On macOS, the emulator binary forks the real QEMU process and the parent
exits with code 0 immediately. Only non-zero exit codes are treated as
immediate failures; exit code 0 continues polling since the real emulator
runs as a separate process.
Context: dotnet/android#10965
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@jonathanpeppersjonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The build failures is an issue rerunning -- attempt number need to be on artifacts (I'm fixing separately).

Merging shortly.

@jonathanpeppers
jonathanpeppers merged commit 39995cf into mainMar 19, 2026
1 of 2 checks passed
@jonathanpeppers
jonathanpeppers deleted the feature/emulator-runner branch March 19, 2026 13:05
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 19, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
rmarinho added a commit to dotnet/android that referenced this pull request Mar 20, 2026
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers pushed a commit to dotnet/android that referenced this pull request Mar 23, 2026
* Use shared EmulatorRunner from android-tools for BootAndroidEmulator
Replace the 454-line BootAndroidEmulator implementation with a thin
~180-line wrapper that delegates to EmulatorRunner.BootEmulatorAsync()
from Xamarin.Android.Tools.AndroidSdk.
Key changes:
- Remove all process management, polling, and boot detection logic
- Delegate to EmulatorRunner.BootEmulatorAsync() for the full 3-phase
boot: check online → check AVD running → launch + poll + wait
- Map EmulatorBootResult errors to existing XA0143/XA0145 error codes
- Virtual ExecuteBoot() method for clean test mocking
- Update submodule to feature/emulator-runner (d8ee2d5)
Tests updated from 9 to 10 (added ExtraArguments and UnknownError tests)
using simplified mock pattern — MockBootAndroidEmulator overrides
ExecuteBoot() to return canned EmulatorBootResult values.
Depends on: dotnet/android-tools#284
### Restore XA0144 for unexpected emulator errors
Update XA0144 message format to accept the ErrorMessage from
EmulatorRunner directly. The default switch case (Unknown and
future error kinds) now uses XA0144 with the full error details
instead of the misleading timeout message XA0145.
Error code mapping:
- XA0143: Launch failed (couldn't start emulator)
- XA0144: Unexpected exit/error (process exited, unknown errors)
- XA0145: Boot timeout (didn't finish in time)
jonathanpeppers added a commit to dotnet/android that referenced this pull request Jul 13, 2026
This skill lets you say:
review this PR: dotnet/android-tools#284
Some example code reviews:
* dotnet/android-tools#283 (review)
* dotnet/android-tools#284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

copilot`copilot-cli` or other AIs were used to author this

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@rmarinho@jonathanpeppers@mattleibow