Add AvdManagerRunner for avdmanager CLI operations#282
Add AvdManagerRunner for avdmanager CLI operations#282
Conversation
- 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>
There was a problem hiding this comment.
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
AndroidToolRunnerfor running SDK tools with timeout/stdout/stderr capture (sync + async) and a background-start helper. - Adds
AvdManagerRunnerto locate and invokeavdmanager, parselist avdoutput, and enrich results fromconfig.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.
| return new AvdInfo { | ||
| Name = name, | ||
| DeviceProfile = device, | ||
| Path = path, | ||
| SystemImage = target, | ||
| Target = target | ||
| }; |
There was a problem hiding this comment.
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.
| "google_apis_playstore" => "Google API's, Play Store", | ||
| "google_apis" => playStoreEnabled ? "Google API's, Play Store" : "Google API's", |
There was a problem hiding this comment.
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”.
| "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", |
| 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; } | ||
|
|
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| process.StartInfo = new ProcessStartInfo { | ||
| FileName = fileName, | ||
| Arguments = arguments, | ||
| WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory, | ||
| UseShellExecute = false, | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true, | ||
| CreateNoWindow = true | ||
| }; |
There was a problem hiding this comment.
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.
| public static ToolRunnerResult Run ( | ||
| string fileName, | ||
| string arguments = "", | ||
| string? workingDirectory = null, | ||
| Dictionary<string, string>? environmentVariables = null, | ||
| TimeSpan? timeout = null, | ||
| CancellationToken cancellationToken = default) | ||
| { |
There was a problem hiding this comment.
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.
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true, |
There was a problem hiding this comment.
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.
| RedirectStandardOutput = true, | |
| RedirectStandardError = true, | |
| RedirectStandardOutput = false, | |
| RedirectStandardError = false, |
| public class AvdManagerRunner | ||
| { | ||
| readonly Func<string?> getSdkPath; | ||
| readonly Func<string?> getJdkPath; | ||
|
|
There was a problem hiding this comment.
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.
|
I'd like to get the System.Diagnostics.Process code unified like mentioned here: |
Addresses PR #281 feedback from @jonathanpeppers: > 'This is inventing lots of new code that just wraps > System.Diagnostics.Process. Can we just use the existing code > for this instead? ProcessUtils.cs' Changes: - Extended ProcessUtils with RunToolAsync() and StartToolBackground() - Added GetExecutableExtension() and GetBatchExtension() utilities - Removed AndroidToolRunner.cs (-347 lines) - Removed ToolRunnerResult.cs (-74 lines) The new methods leverage the existing StartProcess() implementation which already handles cancellation, output redirection, and process lifecycle correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- AvdManagerRunner: Create/delete/list Android Virtual Devices - AvdInfo: Model for AVD metadata API: - ListAvdsAsync(): List installed AVDs - CreateAvdAsync(): Create a new AVD - DeleteAvdAsync(): Delete an AVD - ListDevicesAsync(): List available device definitions - ListTargetsAsync(): List available system image targets Addresses #277 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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>
b9b08f7 to
d9a65ca
Compare
Summary
Adds
AvdManagerRunnerclass to wrapavdmanagerCLI operations for programmatic AVD management.Addresses #277
New API
Usage
Dependencies
Requires #281 (base tool runner infrastructure) to be merged first.
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com