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.
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>
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>
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
…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>
…g, ThrowIfFailed overload Apply review patterns from PR #283 (AdbRunner): - Constructor takes resolved 'string avdManagerPath' instead of Func<string?> delegates - Add IDictionary<string, string>? environmentVariables for ANDROID_HOME/JAVA_HOME - Remove AvdManagerPath property, IsAvailable, RequireAvdManagerPath(), ConfigureEnvironment() - Use 'is { Length: > 0 }' pattern matching for null/empty checks - Add ThrowIfFailed(StringWriter) overload to ProcessUtils - Change ThrowIfFailed/ValidateNotNullOrEmpty/FindCmdlineTool to internal visibility - Update tests: FindCmdlineTool tests replace AvdManagerPath tests, add constructor validation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Wraps
avdmanagerCLI operations for programmatic AVD management. Addresses #277.API Surface
Key Design Decisions
string avdManagerPath(full path to avdmanager binary) instead ofFunc<string?>. Callers resolve the path before constructing the runner. Follows the same pattern as AdbRunner (Add AdbRunner for adb CLI operations #283).IDictionary<string, string>? environmentVariablespassed toProcessUtils.StartProcessfor ANDROID_HOME/JAVA_HOME/PATH configuration. Avoids internal PSI manipulation.ProcessUtils.ThrowIfFailed(exitCode, command, stderr)directly withStringWriterparameters — no.ToString()at call sites.is { Length: > 0 }instead of!string.IsNullOrEmpty()— consistent with codebase conventions and avoids netstandard2.0 nullable narrowing issues.cmdline-tools/{version}/bin/(highest version first), thenlatest, then legacytools/bin/. Consumers call this to resolve the avdmanager path before constructing the runner.Tests
FindCmdlineTool_FindsVersionedDir,_PrefersHigherVersion,_FallsBackToLatest,_MissingSdk_ReturnsNull— test path resolutionConstructor_NullPath_ThrowsArgumentException,Constructor_EmptyPath_ThrowsArgumentException— constructor validationCreateAvdAsync_*,DeleteAvdAsync_*— argument validationCommits
271b5c137c3a79d96296a