Skip to content

Add AvdManagerRunner for avdmanager CLI operations#282

Open
rmarinho wants to merge 6 commits intomainfrom
feature/avdmanager-runner
Open

Add AvdManagerRunner for avdmanager CLI operations#282
rmarinho wants to merge 6 commits intomainfrom
feature/avdmanager-runner

Conversation

@rmarinho
Copy link
Member

@rmarinho rmarinho commented Feb 23, 2026

Summary

Wraps avdmanager CLI operations for programmatic AVD management. Addresses #277.

Changes

New constructor overload with JDK path

public AvdManagerRunner (Func<string?> getSdkPath, Func<string?>? getJdkPath)

Sets JAVA_HOME and ANDROID_HOME on all child process ProcessStartInfo via ConfigureEnvironment().
The existing 1-arg constructor remains backward compatible.

New: CreateAvdAsync

  • Duplicate detection: checks ListAvdsAsync first, returns existing AVD if name matches
  • Orphaned AVD handling: detects .avd directory without .ini registration and auto-applies --force
  • stdin handling: answers "no" to avdmanager hardware profile customization prompt
  • Error reporting: captures stdout/stderr and throws InvalidOperationException on failure

New: ParseAvdListOutput (internal static)

Extracted from ListAvdsAsync for testability.

Updated: ListAvdsAsync / DeleteAvdAsync

Now call ConfigureEnvironment(psi) before ProcessUtils.StartProcess to pass JAVA_HOME.

Why

avdmanager.bat requires JAVA_HOME to locate the JVM. Without it, operations fail silently on Windows when the system JAVA_HOME is stale or missing.

Copilot AI review requested due to automatic review settings February 23, 2026 17:39
Copy link
Contributor

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

Adds a new “runner” layer to Xamarin.Android.Tools.AndroidSdk to execute Android SDK command-line tools programmatically, with an initial AvdManagerRunner implementation for creating/listing/deleting AVDs.

Changes:

  • Introduces AndroidToolRunner for running SDK tools with timeout/stdout/stderr capture (sync + async) and a background-start helper.
  • Adds AvdManagerRunner to locate and invoke avdmanager, parse list avd output, and enrich results from config.ini.
  • Adds supporting models (ToolRunnerResult, AvdInfo) and environment setup utilities (AndroidEnvironmentHelper).

Reviewed changes

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

Show a summary per file
File Description
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs Adds an avdmanager wrapper with list/create/delete and output parsing/enrichment.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs Adds process execution utilities (sync/async + background start).
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs Provides JAVA_HOME / ANDROID_HOME setup plus ABI/API/tag mapping helpers.
src/Xamarin.Android.Tools.AndroidSdk/Models/ToolRunnerResult.cs Introduces a result type for tool execution (typed + untyped).
src/Xamarin.Android.Tools.AndroidSdk/Models/AvdInfo.cs Adds an AVD info model used by AvdManagerRunner.

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

Comment on lines 265 to 271
return new AvdInfo {
Name = name,
DeviceProfile = device,
Path = path,
SystemImage = target,
Target = target
};
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.

CreateAvdFromParsedData assigns SystemImage = target and Target = target. The parsed value here is the human-readable “Target:”/“Based on:” text, not the system image package path (e.g., system-images;android-33;...). This makes AvdInfo.SystemImage misleading for listed AVDs; consider leaving SystemImage null here or parsing the actual system image from config.ini (image.sysdir.1) and keeping Target as the display string.

Copilot uses AI. Check for mistakes.
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.

MapTagIdToDisplayName uses "Google API's" (apostrophe) in user-facing strings; this is grammatically incorrect and inconsistent with common naming (“Google APIs”). Update the strings to “Google APIs” / “Google APIs, Play Store”.

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 9 to 20
public class AvdInfo
{
/// <summary>
/// AVD name (unique identifier).
/// </summary>
public string Name { get; set; } = string.Empty;

/// <summary>
/// Device profile (e.g., "pixel_6", "Nexus 5X").
/// </summary>
public string? DeviceProfile { get; set; }

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.

AvdInfo is described in the PR as a record with a small fixed shape, but the implementation is a mutable class with different property names (DeviceProfile/SystemImage/etc.) and additional fields. This is a public API surface difference; either align the type/signature with the PR description or update the PR description so downstream consumers (and source compatibility expectations) match reality.

Copilot uses AI. Check for mistakes.
Comment on lines 208 to 261
List<AvdInfo> ParseAvdList (string output)
{
var avds = new List<AvdInfo> ();
var lines = output.Split ('\n');

string? currentName = null;
string? currentDevice = null;
string? currentPath = null;
string? currentTarget = null;

foreach (var line in lines) {
var trimmed = line.Trim ();

if (trimmed.StartsWith ("Name:")) {
// Save previous AVD if exists
if (currentName != null) {
var avd = CreateAvdFromParsedData (currentName, currentDevice, currentPath, currentTarget);
avds.Add (avd);
}
currentName = trimmed.Substring (5).Trim ();
currentDevice = null;
currentPath = null;
currentTarget = null;
} else if (trimmed.StartsWith ("Device:")) {
currentDevice = trimmed.Substring (7).Trim ();
} else if (trimmed.StartsWith ("Path:")) {
currentPath = trimmed.Substring (5).Trim ();
} else if (trimmed.StartsWith ("Target:")) {
currentTarget = trimmed.Substring (7).Trim ();
} else if (trimmed.StartsWith ("Based on:")) {
if (currentTarget == null)
currentTarget = trimmed.Substring (9).Trim ();
}
}

// Don't forget the last one
if (currentName != null) {
var avd = CreateAvdFromParsedData (currentName, currentDevice, currentPath, currentTarget);
avds.Add (avd);
}

// Enrich AVDs with config.ini data
for (int i = 0; i < avds.Count; i++) {
var avd = avds [i];
if (!string.IsNullOrEmpty (avd.Path)) {
var configPath = Path.Combine (avd.Path, "config.ini");
if (File.Exists (configPath)) {
avds [i] = EnrichAvdFromConfig (avd, configPath);
}
}
}

return avds;
}
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.

ParseAvdList and EnrichAvdFromConfig implement non-trivial parsing logic of avdmanager output and config.ini. There are existing unit tests in this repo; adding focused tests for representative avdmanager outputs (including Windows/Unix newlines and multiple AVD entries) would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines 48 to 56
process.StartInfo = new ProcessStartInfo {
FileName = fileName,
Arguments = arguments,
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = 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.

AndroidToolRunner sets UseShellExecute=false and executes the provided fileName directly. On Windows, avdmanager/sdkmanager are typically .bat scripts, which cannot be launched this way (CreateProcess will fail). Consider detecting .bat/.cmd and invoking via cmd.exe /c (while still capturing output), or otherwise adjusting execution so cmdline-tools work on Windows.

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.

Run(...) accepts a CancellationToken but never observes it (no registration / no early exit). This makes the API misleading and can leave callers unable to cancel long-running synchronous operations. Either honor cancellation (kill process on cancellation) or remove the parameter.

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 redirects stdout/stderr but never reads from them. This can cause the child process to block once OS pipe buffers fill (common for emulator output), effectively hanging the background process. For true background start, avoid redirection or actively drain the streams.

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

Copilot uses AI. Check for mistakes.
Comment on lines 16 to 20
public class AvdManagerRunner
{
readonly Func<string?> getSdkPath;
readonly Func<string?> getJdkPath;

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.

PR description advertises additional APIs (e.g., ListDevicesAsync/ListTargetsAsync) and different return types, but AvdManagerRunner currently only implements list/create/delete and returns ToolRunnerResult-wrapped results. Either implement the missing APIs or update the PR description/new API section so consumers don’t code against an API that isn’t present.

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
@jonathanpeppers
Copy link
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 #282 feedback to use existing ProcessUtils instead of
the removed AndroidToolRunner. Simplifies API:

- Methods now throw InvalidOperationException on failure instead of
  returning result types with error states
- Uses ProcessUtils.RunToolAsync() for all tool invocations
- Simplified AvdInfo model
- Removed complex ToolRunnerResult<T> wrapper types

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the feature/avdmanager-runner branch 3 times, most recently from c9b6445 to 4f42441 Compare February 24, 2026 19:09
rmarinho and others added 5 commits February 26, 2026 17:31
- ListAvdsAsync(): List configured AVDs
- DeleteAvdAsync(): Delete an AVD

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add XML documentation to all public members
- Split AvdInfo into its own file (one type per file)
- Use 'is not null' instead of '!= null'
- Improve code formatting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add optional Func<string?> getJdkPath constructor parameter
- ConfigureEnvironment() sets JAVA_HOME and ANDROID_HOME on all ProcessStartInfo
- Add CreateAvdAsync with duplicate detection, orphaned AVD directory handling, stdin 'no' for hardware profile prompt
- Extract ParseAvdListOutput as internal static for testability
- Backward compatible: existing 1-arg constructor still works

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rmarinho rmarinho force-pushed the feature/avdmanager-runner branch from cef13bb to d71ca03 Compare February 26, 2026 17:31
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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