Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public AdbRunner (string adbPath, IDictionary<string, string>? environmentVariab

/// <summary>
/// Lists connected devices using 'adb devices -l'.
/// For emulators, queries the AVD name using 'adb -s &lt;serial&gt; emu avd name'.
/// For online emulators, queries the AVD name via <c>getprop</c> / <c>emu avd name</c>.
/// Offline emulators are included but without AVD names (querying them would fail).
/// </summary>
public virtual async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (CancellationToken cancellationToken = default)
{
Expand All @@ -61,9 +62,11 @@ public virtual async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (Cancel

var devices = ParseAdbDevicesOutput (stdout.ToString ().Split ('\n'));

// For each emulator, try to get the AVD name
// For each online emulator, try to get the AVD name.
// Skip offline emulators — neither getprop nor 'emu avd name' work on them
// and attempting these commands causes unnecessary delays during boot polling.
foreach (var device in devices) {
if (device.Type == AdbDeviceType.Emulator) {
if (device.Type == AdbDeviceType.Emulator && device.Status == AdbDeviceStatus.Online) {
device.AvdName = await GetEmulatorAvdNameAsync (device.Serial, cancellationToken).ConfigureAwait (false);
device.Description = BuildDeviceDescription (device);
}
Expand All @@ -74,15 +77,26 @@ public virtual async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (Cancel

/// <summary>
/// Queries the emulator for its AVD name.
/// Tries <c>adb -s &lt;serial&gt; emu avd name</c> first (emulator console protocol),
/// then falls back to <c>adb shell getprop ro.boot.qemu.avd_name</c> which reads the
/// boot property set by the emulator kernel. The fallback is needed because
/// <c>emu avd name</c> can return empty output on some adb/emulator version
/// combinations (observed with adb v36).
/// Tries <c>adb shell getprop ro.boot.qemu.avd_name</c> first (reliable on all modern
/// emulators), then falls back to <c>adb -s &lt;serial&gt; emu avd name</c> (emulator
/// console protocol) for older emulator versions. The <c>emu avd name</c> command returns
/// empty output on emulator 36.4.10+ (observed with adb v36), so <c>getprop</c> is the
/// preferred method.
/// </summary>
internal async Task<string?> GetEmulatorAvdNameAsync (string serial, CancellationToken cancellationToken = default)
{
// Try 1: Console command (fast, works on most emulator versions)
// Try 1: Shell property (reliable on modern emulators, always set by the emulator kernel)
try {
var avdName = await GetShellPropertyAsync (serial, "ro.boot.qemu.avd_name", cancellationToken).ConfigureAwait (false);
if (avdName is { Length: > 0 } name && !string.IsNullOrWhiteSpace (name))
return name.Trim ();
} catch (OperationCanceledException) {
throw;
} catch {
// Fall through to console command fallback
}

// Try 2: Console command (fallback for older emulators where getprop may not be available)
try {
using var stdout = new StringWriter ();
var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "emu", "avd", "name");
Expand All @@ -97,17 +111,6 @@ public virtual async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (Cancel
}
} catch (OperationCanceledException) {
throw;
} catch {
// Fall through to getprop fallback
}

// Try 2: Shell property (fallback when 'adb emu avd name' returns empty on some adb/emulator versions)
try {
var avdName = await GetShellPropertyAsync (serial, "ro.boot.qemu.avd_name", cancellationToken).ConfigureAwait (false);
if (avdName is { Length: > 0 } name && !string.IsNullOrWhiteSpace (name))
return name.Trim ();
} catch (OperationCanceledException) {
throw;
} catch {
// Both methods failed
}
Expand Down
23 changes: 23 additions & 0 deletions src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,33 @@ public async Task<EmulatorBootResult> BootEmulatorAsync (
// Poll for the new emulator serial to appear.
// If the boot times out or is cancelled, terminate the process we spawned
// to avoid leaving orphan emulator processes.
//
// On macOS, the emulator binary may fork the real QEMU process and exit with
// code 0 immediately. The real emulator continues as a separate process and
// will eventually appear in 'adb devices'. We only treat non-zero exit codes
// as immediate failures; exit code 0 means we continue polling.
try {
string? newSerial = null;
bool processExitedWithZero = false;
while (newSerial == null) {
timeoutCts.Token.ThrowIfCancellationRequested ();

// Detect early process exit for fast failure
if (emulatorProcess.HasExited && !processExitedWithZero) {
if (emulatorProcess.ExitCode != 0) {
emulatorProcess.Dispose ();
return new EmulatorBootResult {
Success = false,
ErrorKind = EmulatorBootErrorKind.LaunchFailed,
ErrorMessage = $"Emulator process for '{deviceOrAvdName}' exited with code {emulatorProcess.ExitCode} before becoming available.",
};
}
// Exit code 0: emulator likely forked (common on macOS).
// The real emulator runs as a separate process — keep polling.
logger.Invoke (TraceLevel.Verbose, $"Emulator launcher process exited with code 0 (likely forked). Continuing to poll adb devices.");
processExitedWithZero = true;
}

await Task.Delay (options.PollInterval, timeoutCts.Token).ConfigureAwait (false);

devices = await adbRunner.ListDevicesAsync (timeoutCts.Token).ConfigureAwait (false);
Expand Down