Skip to content

Comments

Add EmulatorRunner for emulator CLI operations#284

Open
rmarinho wants to merge 2 commits intomainfrom
feature/emulator-runner
Open

Add EmulatorRunner for emulator CLI operations#284
rmarinho wants to merge 2 commits intomainfrom
feature/emulator-runner

Conversation

@rmarinho
Copy link
Member

Summary

Adds EmulatorRunner class to wrap emulator CLI operations for starting and managing Android emulators.

Addresses #278

New API

public class EmulatorRunner
{
    // Start an emulator for an AVD (returns Process handle)
    Process? StartEmulator(string avdName, IEnumerable<string>? additionalArgs = null);
    
    // Start emulator and wait for boot
    Task<Process?> StartEmulatorAsync(string avdName, TimeSpan? bootTimeout = null, CancellationToken ct = default);
    
    // Wait for emulator to complete boot
    Task<bool> WaitForBootAsync(string serial, TimeSpan timeout, CancellationToken ct = default);
    
    // List running emulator serials
    Task<IReadOnlyList<string>> ListRunningEmulatorsAsync(CancellationToken ct = default);
}

Usage

var emulator = new EmulatorRunner(androidSdkPath);

// Start emulator and wait for boot (default 2 min timeout)
var process = await emulator.StartEmulatorAsync(""Pixel_6_API_33"");

// Or start without waiting
var process = emulator.StartEmulator(""Pixel_6_API_33"", new[] { ""-no-snapshot"" });

// Check boot status separately
var booted = await emulator.WaitForBootAsync(""emulator-5554"", TimeSpan.FromMinutes(3));

// List running emulators
var running = await emulator.ListRunningEmulatorsAsync();

Dependencies

Requires:

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

rmarinho and others added 2 commits February 23, 2026 17:15
- AndroidToolRunner: Base class for running Android SDK CLI tools
- AndroidEnvironmentHelper: Environment variable setup, ABI mapping
- ToolRunnerResult: Generic result model for tool execution

This provides the foundation for specific tool runners.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- EmulatorRunner: Start and manage Android emulators

API:
- StartEmulator(): Start an emulator for an AVD (returns Process)
- StartEmulatorAsync(): Start emulator and wait for boot
- WaitForBootAsync(): Wait for emulator to complete boot
- ListRunningEmulatorsAsync(): List running emulator serials

Addresses #278

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 23, 2026 17:39
Copy link

Copilot AI left a comment

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.

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

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

Comment on lines +113 to +114
"google_apis_playstore" => "Google API's, Play Store",
"google_apis" => playStoreEnabled ? "Google API's, Play Store" : "Google API's",
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The display strings use "Google API's" (possessive) where the intended plural is "Google APIs". This is user-visible output, so it should be corrected for grammar/branding consistency.

Suggested change
"google_apis_playstore" => "Google API's, Play Store",
"google_apis" => playStoreEnabled ? "Google API's, Play Store" : "Google API's",
"google_apis_playstore" => "Google APIs, Play Store",
"google_apis" => playStoreEnabled ? "Google APIs, Play Store" : "Google APIs",

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +52
public static string? MapAbiToArchitecture (string? abi)
{
return abi switch {
"arm64-v8a" => "arm64",
"armeabi-v7a" => "arm",
"x86_64" => "x64",
"x86" => "x86",
_ => abi
};
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

AndroidEnvironmentHelper introduces several non-trivial mapping functions (ABI -> architecture, API level -> version, tag -> display name) but there are no unit tests validating the mappings. Given the repo already has a Xamarin.Android.Tools.AndroidSdk-Tests suite, adding targeted tests would help prevent regressions when mappings are updated.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +32
readonly AdbRunner adbRunner;

/// <summary>
/// Creates a new EmulatorRunner.
/// </summary>
/// <param name="getSdkPath">Function to get the Android SDK path.</param>
/// <param name="getJdkPath">Function to get the JDK path.</param>
public EmulatorRunner (Func<string?> getSdkPath, Func<string?> getJdkPath)
{
this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath));
this.getJdkPath = getJdkPath ?? throw new ArgumentNullException (nameof (getJdkPath));
this.adbRunner = new AdbRunner (getSdkPath);
}
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

EmulatorRunner depends on AdbRunner, but there is no AdbRunner type in the repository (searching the repo for class AdbRunner returns no matches). This will not compile unless the missing runner is added in this PR or the dependency is included/updated.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +73
/// <summary>
/// Starts an AVD.
/// </summary>
/// <param name="avdName">Name of the AVD to start.</param>
/// <param name="coldBoot">Whether to perform a cold boot (no snapshot).</param>
/// <param name="waitForBoot">Whether to wait for the device to fully boot.</param>
/// <param name="additionalArgs">Additional emulator arguments.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Result with the emulator process.</returns>
public async Task<ToolRunnerResult<Process?>> StartAvdAsync (
string avdName,
bool coldBoot = false,
bool waitForBoot = false,
string? additionalArgs = null,
CancellationToken cancellationToken = default)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

The implemented public API (StartAvdAsync, StopEmulatorAsync, ListAvdNamesAsync) does not match the PR description / Issue #278 proposed surface (e.g., StartEmulator, StartEmulatorAsync, WaitForBootAsync, ListRunningEmulatorsAsync). Either update the implementation to match the described API or update the PR description/issue expectations so consumers know what to call.

Copilot uses AI. Check for mistakes.
Comment on lines +107 to +112
if (waitForBoot) {
var waitResult = await adbRunner.WaitForDeviceAsync (
null, // Wait for any device
TimeSpan.FromMinutes (2),
cancellationToken
).ConfigureAwait (false);
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

waitForBoot currently calls adbRunner.WaitForDeviceAsync(null, ...) ("wait for any device"). If there is already a physical device or another emulator connected, this can return success immediately without the started AVD booting. The runner needs to wait for the specific emulator instance it started (e.g., determine the new emulator serial and wait for that).

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +41
public static ToolRunnerResult Run (
string fileName,
string arguments = "",
string? workingDirectory = null,
Dictionary<string, string>? environmentVariables = null,
TimeSpan? timeout = null,
CancellationToken cancellationToken = default)
{
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

AndroidToolRunner.Run(...) accepts a CancellationToken but never observes it. Either remove the parameter or honor it (e.g., periodically check cancellationToken.IsCancellationRequested and kill the process / return a cancelled result) to avoid misleading API behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +90
var completed = process.WaitForExit ((int) effectiveTimeout.TotalMilliseconds);

if (!completed) {
try { KillProcess (process); } catch { }
stopwatch.Stop ();
return new ToolRunnerResult {
ExitCode = -1,
StandardOutput = stdoutBuilder.ToString (),
StandardError = stderrBuilder.ToString (),
Duration = stopwatch.Elapsed,
TimedOut = true,
ErrorMessage = $"Process timed out after {effectiveTimeout.TotalSeconds}s"
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

process.WaitForExit((int) effectiveTimeout.TotalMilliseconds) will throw if effectiveTimeout.TotalMilliseconds is > int.MaxValue or < 0. Consider clamping to int.MaxValue (and/or validating timeout) to prevent unexpected exceptions for larger timeouts.

Copilot uses AI. Check for mistakes.
Comment on lines +287 to +288
RedirectStandardOutput = true,
RedirectStandardError = true,
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

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

StartBackground sets RedirectStandardOutput/RedirectStandardError = true but does not start any readers. This can cause the emulator process to block once stdout/stderr buffers fill up. For long-running background processes, either do not redirect these streams by default or immediately begin async reads to drain them.

Suggested change
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardOutput = false,
RedirectStandardError = false,

Copilot uses AI. Check for mistakes.
@rmarinho rmarinho added the copilot `copilot-cli` or other AIs were used to author this label Feb 23, 2026
@rmarinho rmarinho requested a review from Redth February 23, 2026 17:51
@rmarinho rmarinho requested a review from mattleibow February 23, 2026 17:51
@jonathanpeppers
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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.

3 participants