diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs index 0dd60476..ce8a1bd7 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs @@ -48,7 +48,8 @@ public AdbRunner (string adbPath, IDictionary? environmentVariab /// /// Lists connected devices using 'adb devices -l'. - /// For emulators, queries the AVD name using 'adb -s <serial> emu avd name'. + /// For online emulators, queries the AVD name via getprop / emu avd name. + /// Offline emulators are included but without AVD names (querying them would fail). /// public virtual async Task> ListDevicesAsync (CancellationToken cancellationToken = default) { @@ -61,9 +62,11 @@ public virtual async Task> 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); } @@ -74,15 +77,26 @@ public virtual async Task> ListDevicesAsync (Cancel /// /// Queries the emulator for its AVD name. - /// Tries adb -s <serial> emu avd name first (emulator console protocol), - /// then falls back to adb shell getprop ro.boot.qemu.avd_name which reads the - /// boot property set by the emulator kernel. The fallback is needed because - /// emu avd name can return empty output on some adb/emulator version - /// combinations (observed with adb v36). + /// Tries adb shell getprop ro.boot.qemu.avd_name first (reliable on all modern + /// emulators), then falls back to adb -s <serial> emu avd name (emulator + /// console protocol) for older emulator versions. The emu avd name command returns + /// empty output on emulator 36.4.10+ (observed with adb v36), so getprop is the + /// preferred method. /// internal async Task 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"); @@ -97,17 +111,6 @@ public virtual async Task> 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 } diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs index d2ac7f58..4a4bd6e8 100644 --- a/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs +++ b/src/Xamarin.Android.Tools.AndroidSdk/Runners/EmulatorRunner.cs @@ -204,10 +204,33 @@ public async Task 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);