Add AvdManagerRunner for avdmanager CLI operations#282
Add AvdManagerRunner for avdmanager CLI operations#282
Conversation
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.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidToolRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
|
I'd like to get the System.Diagnostics.Process code unified like mentioned here: |
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>
b286a7a to
6e09e61
Compare
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
be6eb7f to
eb4b66a
Compare
| // Detect orphaned AVD directory (folder exists without .ini registration). | ||
| var avdDir = Path.Combine ( | ||
| Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), | ||
| ".android", "avd", $"{name}.avd"); | ||
| if (Directory.Exists (avdDir)) | ||
| force = true; |
There was a problem hiding this comment.
CreateAvdAsync hard-codes the AVD directory as $HOME/.android/avd/<name>.avd for orphan detection and for the returned AvdInfo.Path. avdmanager can place AVDs elsewhere when ANDROID_USER_HOME/ANDROID_AVD_HOME are set, so this can incorrectly force --force and/or return the wrong path. Consider deriving the AVD location from the tool output (or re-list after creation and return the matching entry) and basing orphan detection on the same resolved location/env vars.
| public async Task DeleteAvdAsync (string name, CancellationToken cancellationToken = default) | ||
| { | ||
| var avdManagerPath = RequireAvdManagerPath (); | ||
|
|
||
| using var stderr = new StringWriter (); | ||
| var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "delete", "avd", "--name", name); | ||
| ConfigureEnvironment (psi); |
There was a problem hiding this comment.
DeleteAvdAsync doesn't validate name (null/empty/whitespace). Passing an empty name will invoke avdmanager with an invalid --name value and can lead to confusing errors; please add the same argument validation used in CreateAvdAsync.
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs
Outdated
Show resolved
Hide resolved
src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs
Outdated
Show resolved
Hide resolved
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw ArgumentException for empty strings, ArgumentNullException for null, and validate before RequireAvdManagerPath() - Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment - Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD location across tools (matches SdkManager behavior) - Add AndroidUserHome constant to EnvironmentVariableNames - Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects ANDROID_AVD_HOME and ANDROID_USER_HOME env vars - Re-list after CreateAvdAsync to get actual path from avdmanager - Add 5 new validation tests for Create/DeleteAvdAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw ArgumentException for empty strings, ArgumentNullException for null, and validate before RequireAvdManagerPath() - Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment - Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD location across tools (matches SdkManager behavior) - Add AndroidUserHome constant to EnvironmentVariableNames - Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects ANDROID_AVD_HOME and ANDROID_USER_HOME env vars - Re-list after CreateAvdAsync to get actual path from avdmanager - Add 5 new validation tests for Create/DeleteAvdAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4c8ce13 to
3a788bb
Compare
jonathanpeppers
left a comment
There was a problem hiding this comment.
🤖 AI Review Summary
Found 5 issues: 1 security concern, 1 error handling gap, 1 consistency gap, 1 missing constant, 1 convention violation.
- Security / API design:
additionalArgsas single string risks injection and breaks multi-arg callers (EmulatorRunner.cs:57) - Error handling: Bare
catchblock silently swallows exceptions (AdbRunner.cs:106) - Consistency: Exit code not checked in
ListDevicesAsync/ListAvdNamesAsyncwhile other methods do (AdbRunner.cs:71) - Naming: Raw
"ANDROID_AVD_HOME"string instead ofEnvironmentVariableNamesconstant (AvdManagerRunner.cs:212) - Code organization: Three types in one file (
AdbDeviceInfo.cs)
👍 Excellent test coverage with thorough parsing tests. Good CancellationToken propagation throughout, correct OperationCanceledException rethrow. AndroidEnvironmentHelper is a clean factoring of shared env setup. Orphaned AVD detection in CreateAvdAsync is a smart touch.
Review generated by android-tools-reviewer from review guidelines by @jonathanpeppers.
| AndroidEnvironmentHelper.ConfigureEnvironment (psi, getSdkPath (), getJdkPath?.Invoke ()); | ||
| } | ||
|
|
||
| public Process StartAvd (string avdName, bool coldBoot = false, string? additionalArgs = null) |
There was a problem hiding this comment.
🤖 Security / API design — string? additionalArgs is a command injection risk and a correctness bug. If a caller passes "-no-window -no-audio", it becomes a single argument to the emulator (not two), silently breaking. Change to IEnumerable<string>? additionalArgs and use args.AddRange(additionalArgs) so each flag is a separate argument handled safely by ProcessUtils.CreateProcessStartInfo's ArgumentList.
Rule: Structured args, not string interpolation (Postmortem #50)
| } | ||
| } catch (OperationCanceledException) { | ||
| throw; | ||
| } catch { |
There was a problem hiding this comment.
🤖 Error handling — This bare catch block swallows all exceptions without logging. Even if failures are expected (emulator may not support the command), the exception should be captured and logged for diagnostics. At minimum: catch (Exception ex) { /* log ex */ }. Silent swallowing makes debugging null AVD names nearly impossible.
Rule: No empty catch blocks (Postmortem #11)
| var adb = RequireAdb (); | ||
| using var stdout = new StringWriter (); | ||
| var psi = CreateAdbProcess (adb, "devices", "-l"); | ||
| await ProcessUtils.StartProcess (psi, stdout, null, cancellationToken).ConfigureAwait (false); |
There was a problem hiding this comment.
🤖 Consistency — ListDevicesAsync discards the exit code from ProcessUtils.StartProcess, but AvdManagerRunner.ListAvdsAsync and DeleteAvdAsync both check it and throw on failure. Same gap in EmulatorRunner.ListAvdNamesAsync. If adb devices fails (e.g., daemon not running), this silently returns an empty list instead of surfacing the error.
Rule: Check exit codes consistently (Postmortem #48)
| { | ||
| // ANDROID_AVD_HOME takes highest priority | ||
| var avdHome = Environment.GetEnvironmentVariable ("ANDROID_AVD_HOME"); | ||
| if (!string.IsNullOrEmpty (avdHome)) |
There was a problem hiding this comment.
🤖 Naming — Raw "ANDROID_AVD_HOME" string literal. The PR already adds EnvironmentVariableNames.AndroidUserHome — add an AndroidAvdHome constant too and use it here. Typos in env var names cause silent, hard-to-debug failures.
Rule: Environment variable constants (Postmortem #18)
| /// <summary> | ||
| /// Represents the type of an Android device. | ||
| /// </summary> | ||
| public enum AdbDeviceType |
There was a problem hiding this comment.
🤖 Code organization — Three public types (AdbDeviceType, AdbDeviceStatus, AdbDeviceInfo) in one file. Per repo conventions, each public type should be in its own .cs file named after the type: AdbDeviceType.cs, AdbDeviceStatus.cs, AdbDeviceInfo.cs.
Rule: One type per file (Postmortem #9)
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw ArgumentException for empty strings, ArgumentNullException for null, and validate before RequireAvdManagerPath() - Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment - Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD location across tools (matches SdkManager behavior) - Add AndroidUserHome constant to EnvironmentVariableNames - Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects ANDROID_AVD_HOME and ANDROID_USER_HOME env vars - Re-list after CreateAvdAsync to get actual path from avdmanager - Add 5 new validation tests for Create/DeleteAvdAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw ArgumentException for empty strings, ArgumentNullException for null, and validate before RequireAvdManagerPath() - Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment - Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD location across tools (matches SdkManager behavior) - Add AndroidUserHome constant to EnvironmentVariableNames - Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects ANDROID_AVD_HOME and ANDROID_USER_HOME env vars - Re-list after CreateAvdAsync to get actual path from avdmanager - Add 5 new validation tests for Create/DeleteAvdAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7a84734 to
37c4ffe
Compare
…ed args
- AdbRunner: check exit codes consistently in ListDevicesAsync, WaitForDeviceAsync, StopEmulatorAsync
- AdbRunner: bare catch {} → catch (Exception) in GetEmulatorAvdNameAsync
- AdbDeviceInfo.cs: split 3 public types into separate files (AdbDeviceType.cs, AdbDeviceStatus.cs)
- EnvironmentVariableNames: add AndroidAvdHome constant, use in AvdManagerRunner
- EmulatorRunner.StartAvd: additionalArgs string? → IEnumerable<string>? for proper arg separation
- EmulatorRunner.StartAvd: set RedirectStandardOutput/Error=false to prevent pipe deadlock
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- GetToolEnvironment(): Set ANDROID_HOME, JAVA_HOME, and PATH Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ing on StringWriter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… add tests - Replace manual ProcessStartInfo with ProcessUtils.CreateProcessStartInfo - Use AndroidEnvironmentHelper.ConfigureEnvironment instead of inline env setup - Extract ParseDeviceListOutput as internal static for testability - Return IReadOnlyList<AdbDeviceInfo> from ListDevicesAsync - Add AdbRunnerTests (7 tests): parsing, path discovery, null/missing sdk Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Port the adb device parsing, display name formatting, status mapping, and emulator merging logic from dotnet/android's GetAvailableAndroidDevices MSBuild task into the shared android-tools library. Changes: - AdbDeviceInfo: Add Type, Status, Description, AvdName, Product, TransportId fields using AdbDeviceType/AdbDeviceStatus enums - AdbRunner.ParseAdbDevicesOutput: Regex-based parser matching dotnet/android (handles device/offline/unauthorized/no permissions states) - AdbRunner.BuildDeviceDescription: Priority order AVD name > model > product > device > serial, with underscore-to-space cleanup - AdbRunner.FormatDisplayName: Title case + API capitalization for AVD names - AdbRunner.MapAdbStateToStatus: Maps adb states to AdbDeviceStatus enum - AdbRunner.MergeDevicesAndEmulators: Merges running + non-running emulators, deduplicates by AVD name (case-insensitive), sorts online first - AdbRunner.GetEmulatorAvdNameAsync: Queries AVD name via adb emu avd name - AdbRunnerTests: 33 tests ported from dotnet/android test cases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bare catch block was swallowing cancellation tokens, causing cancelled operations to silently continue spawning adb processes instead of propagating the cancellation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- StartAvd(): Start an emulator for an AVD - ListAvdNamesAsync(): List installed AVD names Minimal implementation without external dependencies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add XML documentation to all public members - Use 'is not null' instead of '!= null' - Improve code formatting - Remove unused variable assignment 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 - Applied to StartAvd and ListAvdNamesAsync - 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>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…riter Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… add tests - Replace manual ProcessStartInfo with ProcessUtils.CreateProcessStartInfo - Use AndroidEnvironmentHelper.ConfigureEnvironment instead of duplicated method - Extract ParseListAvdsOutput as internal static for testability - Return IReadOnlyList<string> from ListAvdNamesAsync - Add RequireEmulatorPath helper - Add EmulatorRunnerTests (7 tests): parsing, path discovery, null/missing sdk Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- 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>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add 'using' to StringWriter instances in ListAvdsAsync and CreateAvdAsync to ensure proper disposal - Add AvdManagerRunnerTests with 5 focused tests for ParseAvdListOutput: multiple AVDs, Windows newlines, empty output, no AVDs, missing device Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Convert AvdManagerRunner.cs and AvdInfo.cs to file-scoped namespaces per repo conventions for new files - Remove accidentally tracked nupkg (already in .gitignore) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ironmentVariableNames, exit code checks - AvdManagerPath: enumerate versioned cmdline-tools dirs descending (mirrors SdkManager.FindSdkManagerPath pattern), then latest, then legacy tools/bin - Use ProcessUtils.CreateProcessStartInfo with separate args instead of building Arguments string manually (all 3 methods) - Use EnvironmentVariableNames.AndroidHome/.JavaHome constants instead of hard-coded strings - Check exit codes in ListAvdsAsync and DeleteAvdAsync, throw on failure - Return IReadOnlyList<AvdInfo> from ListAvdsAsync - Set Path on AvdInfo returned from CreateAvdAsync for consistency - Extract RequireAvdManagerPath helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Delegate to AndroidEnvironmentHelper.ConfigureEnvironment instead of inline setup - Add 5 path discovery tests: versioned dir, higher version priority, latest fallback, null/missing sdk Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw ArgumentException for empty strings, ArgumentNullException for null, and validate before RequireAvdManagerPath() - Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment - Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD location across tools (matches SdkManager behavior) - Add AndroidUserHome constant to EnvironmentVariableNames - Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects ANDROID_AVD_HOME and ANDROID_USER_HOME env vars - Re-list after CreateAvdAsync to get actual path from avdmanager - Add 5 new validation tests for Create/DeleteAvdAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eTool - ProcessUtils.ThrowIfFailed: consistent exit code checking with stderr/stdout context - ProcessUtils.ValidateNotNullOrEmpty: reusable null/empty argument validation - ProcessUtils.FindCmdlineTool: version-aware cmdline-tools directory search with legacy fallback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace cmdline-tools path lookup with ProcessUtils.FindCmdlineTool (~20 lines removed) - Replace duplicated null/empty validation with ProcessUtils.ValidateNotNullOrEmpty - Replace inline exit code checks with ProcessUtils.ThrowIfFailed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix argument validation: CreateAvdAsync/DeleteAvdAsync now throw ArgumentException for empty strings, ArgumentNullException for null, and validate before RequireAvdManagerPath() - Make AndroidEnvironmentHelper internal, remove unused GetToolEnvironment - Add ANDROID_USER_HOME to ConfigureEnvironment for consistent AVD location across tools (matches SdkManager behavior) - Add AndroidUserHome constant to EnvironmentVariableNames - Fix hard-coded AVD directory: use GetAvdRootDirectory() that respects ANDROID_AVD_HOME and ANDROID_USER_HOME env vars - Re-list after CreateAvdAsync to get actual path from avdmanager - Add 5 new validation tests for Create/DeleteAvdAsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
797964e to
271b5c1
Compare
Summary
Wraps
avdmanagerCLI 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
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.