Use shared AdbRunner from android-tools for device listing#10880
Use shared AdbRunner from android-tools for device listing#10880
Conversation
src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs
Outdated
Show resolved
Hide resolved
jonathanpeppers
left a comment
There was a problem hiding this comment.
All the calls here need to make it to the MSBuild logger, we lost some log messages with this change.
MSBuild tasks have a TaskLoggingHelper Log property, we need to pass this in to android-tools to use it for logging.
We could setup something like this:
So, from the MSBuild task, it passes in a Action<TraceLevel, string> for dotnet/android-tools to log with.
Avoids allocating a joined string when callers already have individual lines (e.g., MSBuild LogEventsFromTextOutput). The existing string overload now delegates to the new one. Addresses review feedback from @jonathanpeppers on dotnet/android#10880. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Done — added optional Restored debug messages:
|
0995ff2 to
30124fa
Compare
jonathanpeppers
left a comment
There was a problem hiding this comment.
CI fails with:
2026-03-04T15:12:54.1988548Z D:\a\_work\1\s\external\xamarin-android-tools\src\Xamarin.Android.Tools.AndroidSdk\Runners\AdbRunner.cs(139,55): error CS8620: Argument of type 'string?[]' cannot be used for parameter 'args' of type 'string[]' in 'ProcessStartInfo ProcessUtils.CreateProcessStartInfo(string fileName, params string[] args)' due to differences in the nullability of reference types. [D:\a\_work\1\s\external\xamarin-android-tools\src\Xamarin.Android.Tools.AndroidSdk\Xamarin.Android.Tools.AndroidSdk.csproj::TargetFramework=netstandard2.0]
2026-03-04T15:12:54.1989406Z 1 Warning(s)
2026-03-04T15:12:54.1989550Z 3 Error(s)
2026-03-04T15:12:54.1989613Z
2026-03-04T15:12:54.1989758Z Time Elapsed 00:10:16.48
b5df11c to
91b9ca6
Compare
Avoids allocating a joined string when callers already have individual lines (e.g., MSBuild LogEventsFromTextOutput). The existing string overload now delegates to the new one. Addresses review feedback from @jonathanpeppers on dotnet/android#10880. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Avoids allocating a joined string when callers already have individual lines (e.g., MSBuild LogEventsFromTextOutput). The existing string overload now delegates to the new one. Addresses review feedback from @jonathanpeppers on dotnet/android#10880. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jonathanpeppers
left a comment
There was a problem hiding this comment.
🤖 AI Review Summary
Found 1 issue: API design alignment with upstream.
- API design:
ConvertToTaskItemsparameter should acceptIReadOnlyList<AdbDeviceInfo>to match upstream return type (GetAvailableAndroidDevices.cs:87)
👍 Positives:
- Clean removal of ~200 lines of duplicated parsing/formatting/merging code.
- Good delegation pattern — the MSBuild task retains its synchronous
GetEmulatorAvdName(protected virtual for testability) while delegating the pure-logic methods to the shared library. - Tests migrated cleanly from fragile reflection-based invocation to direct
AdbRunner/AdbDeviceInfoAPI calls. ConvertToTaskItemscorrectly bridges the sharedAdbDeviceInfomodel to MSBuildITaskItemwithout losing any metadata.- Preserves the existing
CreateTaskLoggerpattern for MSBuild debug diagnostics.
Review generated by android-tools-reviewer from review guidelines by @jonathanpeppers.
src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs
Outdated
Show resolved
Hide resolved
Delegates adb devices parsing, description building, and device/emulator merging from GetAvailableAndroidDevices to AdbRunner in the shared xamarin-android-tools submodule. Removes ~200 lines of duplicated logic. - ParseAdbDevicesOutput accepts IEnumerable<string> to avoid string.Join - BuildDeviceDescription/MergeDevicesAndEmulators accept optional Action<TraceLevel, string> logger for MSBuild diagnostics - Tests updated to use AdbRunner/AdbDeviceInfo directly Depends on dotnet/android-tools#283. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bumps external/xamarin-android-tools from 9d4b4f5 to f8fbca3 on feature/adb-runner. The new commit fixes CS8601/CS8620 nullable reference type warnings that are treated as errors by this project (WarningsAsErrors=Nullable). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Updates xamarin-android-tools to dbf54f9 which fixes the device parsing regex to handle tab-separated adb output and use explicit state names to prevent false positives. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…82de0a1) Picks up: - TCP console fallback for AVD name resolution when adb emu avd name fails - Bare catch fix with exception logging - Regex fix for explicit adb states and tab support - Nullable reference type fixes for compatibility Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Submodule updated from 82de0a1 to 3cadb58 (feature/adb-runner rebased on main). Changes in submodule: - Constructor refactor: Func<string?> → string adbPath - ThrowIfFailed StringWriter overload - Delete ParseAdbDevicesOutput(string) overload - Replace null-forgiving with property patterns - MapAdbStateToStatus switch expression Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…(da3000c) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix CS8620 nullable string?[] → string[] mismatch in AdbRunner.WaitForDeviceAsync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7480593 to
eff03e5
Compare
…ertToTaskItems android-tools changes: - ParseAdbDevicesOutput returns IReadOnlyList<AdbDeviceInfo> instead of List - MergeDevicesAndEmulators returns IReadOnlyList<AdbDeviceInfo> instead of List - GetEmulatorAvdNameAsync doc comment fixed (removed stale TCP reference) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Delegates adb devices -l parsing/description/merge logic from the GetAvailableAndroidDevices MSBuild task to the shared AdbRunner in Xamarin.Android.Tools.AndroidSdk, reducing duplicated logic and aligning behavior across consumers.
Changes:
- Updated
external/xamarin-android-toolssubmodule to a commit that includes improved emulator AVD name resolution and parsing fixes. - Reworked
GetAvailableAndroidDevicesto useAdbRunnerfor parsing, description building, and device/emulator merging, with a bridge toITaskItem. - Updated tests to exercise
AdbRunner/AdbDeviceInfodirectly (removing reflection-based invocation).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/Xamarin.Android.Build.Tasks/Tasks/GetAvailableAndroidDevices.cs | Replaces in-task parsing/merging with AdbRunner and adds AdbDeviceInfo → ITaskItem conversion. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GetAvailableAndroidDevicesTests.cs | Refactors tests to use AdbRunner APIs and AdbDeviceInfo types instead of reflection. |
| external/xamarin-android-tools | Bumps submodule pointer to pick up shared AdbRunner improvements. |
You can also share your feedback on Copilot code review. Take the survey.
| // For emulators, query AVD names | ||
| foreach (var device in adbDevices) { | ||
| if (device.Type == AdbDeviceType.Emulator) { | ||
| device.AvdName = GetEmulatorAvdName (device.Serial); | ||
| device.Description = AdbRunner.BuildDeviceDescription (device, this.CreateTaskLogger ()); | ||
| } | ||
| } |
There was a problem hiding this comment.
this.CreateTaskLogger() is invoked repeatedly (once per emulator, and again for merging). Create it once (e.g., var logger = this.CreateTaskLogger();) and reuse it for all AdbRunner calls to avoid repeated delegate allocations and keep logging configuration consistent.
| var mergedDevices = MergeDevicesAndEmulators (adbDevices, availableEmulators); | ||
| Devices = mergedDevices.ToArray (); | ||
| // Merge using shared logic | ||
| var mergedDevices = AdbRunner.MergeDevicesAndEmulators (adbDevices, availableEmulators, this.CreateTaskLogger ()); |
There was a problem hiding this comment.
this.CreateTaskLogger() is invoked repeatedly (once per emulator, and again for merging). Create it once (e.g., var logger = this.CreateTaskLogger();) and reuse it for all AdbRunner calls to avoid repeated delegate allocations and keep logging configuration consistent.
| item.SetMetadata ("Description", device.Description); | ||
| item.SetMetadata ("Type", device.Type.ToString ()); | ||
| item.SetMetadata ("Status", device.Status.ToString ()); |
There was a problem hiding this comment.
MSBuild item metadata values are effectively part of the task’s output contract. Relying on enum.ToString() couples that contract to enum member names (which can change if the shared library renames members). Consider mapping AdbDeviceType/AdbDeviceStatus to explicit, stable strings that match the existing expectations (e.g., "Device"/"Emulator" and "Online"/"Offline"/"Unauthorized"/"NoPermissions"/"NotRunning"/"Unknown").
| var devices = AdbRunner.ParseAdbDevicesOutput (lines); | ||
|
|
||
| // Apply AVD name resolution for emulators (same logic as RunTask) | ||
| foreach (var device in devices) { | ||
| if (device.Type == AdbDeviceType.Emulator) { | ||
| device.AvdName = task.GetEmulatorAvdNameForTest (device.Serial); | ||
| device.Description = AdbRunner.BuildDeviceDescription (device); | ||
| } | ||
| } | ||
|
|
||
| return GetAvailableAndroidDevices.ConvertToTaskItems (devices); |
There was a problem hiding this comment.
This test helper re-implements a subset of RunTask behavior (AVD name resolution + description rebuild). To reduce drift between tests and production logic, consider extracting an internal helper in GetAvailableAndroidDevices (e.g., “parse + resolve emulator AVD names + build descriptions”) and have both RunTask and tests call that shared helper.
## Summary Wraps `adb` CLI operations for device management. Addresses #279. Parsing/formatting/merging logic ported from `dotnet/android` `GetAvailableAndroidDevices` MSBuild task, enabling code sharing via the `external/xamarin-android-tools` submodule. See draft PR: dotnet/android#10880 ## Public API ```csharp public class AdbRunner { // Constructor — requires full path to adb executable public AdbRunner(string adbPath, IDictionary<string, string>? environmentVariables = null); // Instance methods (async, invoke adb process) public Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync(CancellationToken ct = default); public Task WaitForDeviceAsync(string? serial = null, TimeSpan? timeout = null, CancellationToken ct = default); public Task StopEmulatorAsync(string serial, CancellationToken ct = default); // Static helpers — public so dotnet/android can call without instantiating AdbRunner public static List<AdbDeviceInfo> ParseAdbDevicesOutput(IEnumerable<string> lines); public static AdbDeviceStatus MapAdbStateToStatus(string adbState); public static string BuildDeviceDescription(AdbDeviceInfo device, Action<TraceLevel, string>? logger = null); public static string FormatDisplayName(string avdName); public static List<AdbDeviceInfo> MergeDevicesAndEmulators( IReadOnlyList<AdbDeviceInfo> adbDevices, IReadOnlyList<string> availableEmulators, Action<TraceLevel, string>? logger = null); } ``` **Internal methods** (not part of public API): - `GetEmulatorAvdNameAsync` — queries AVD name via `adb emu avd name` with TCP console fallback - `ProcessUtils.ThrowIfFailed` — shared exit code validation (string and StringWriter overloads) ## Key Design Decisions - **Constructor takes `string adbPath`**: Callers pass the resolved path; no lazy `Func<>` indirection. Optional `environmentVariables` dictionary for ANDROID_HOME/JAVA_HOME/PATH. - **Static parsing methods** are `public static` so `dotnet/android` can call them without instantiating `AdbRunner` (e.g., `GetAvailableAndroidDevices` MSBuild task passes `List<string>` to `ParseAdbDevicesOutput`) - **`IEnumerable<string>` overload**: `dotnet/android` passes `List<string>` directly from output lines - **Logger parameter**: `BuildDeviceDescription` and `MergeDevicesAndEmulators` accept `Action<TraceLevel, string>?` — `dotnet/android` passes `this.CreateTaskLogger()` for MSBuild trace output - **Regex with explicit state list**: Uses `\s+` separator to match one or more whitespace characters (spaces or tabs). Matches explicit known states with `IgnoreCase`. Daemon startup lines (`*`) are pre-filtered. - **Exit code checking**: `ListDevicesAsync`, `WaitForDeviceAsync`, and `StopEmulatorAsync` throw `InvalidOperationException` with stderr context on non-zero exit via `ProcessUtils.ThrowIfFailed` (internal) - **`MapAdbStateToStatus` as switch expression**: Simple value mapping uses C# switch expression for conciseness - **Property patterns instead of null-forgiving**: Uses `is { Length: > 0 }` patterns throughout for null checks on `netstandard2.0` where `string.IsNullOrEmpty()` lacks `[NotNullWhen(false)]` - **FormatDisplayName**: Lowercases before `ToTitleCase` to normalize mixed-case input (e.g., "PiXeL" → "Pixel") - **Environment variables via `StartProcess`**: Runners pass env vars dictionary to `ProcessUtils.StartProcess`. `AndroidEnvironmentHelper.GetEnvironmentVariables()` builds the dict. ## Tests **45 unit tests** (`AdbRunnerTests.cs`): - **ParseAdbDevicesOutput**: real-world data, empty output, single/multiple devices, mixed states, daemon messages, IP:port, Windows newlines, recovery/sideload, tab-separated output - **FormatDisplayName**: underscores, title case, API capitalization, mixed case, special chars, empty - **MapAdbStateToStatus**: all known states + unknown (recovery, sideload) - **MergeDevicesAndEmulators**: no emulators, no running, mixed, case-insensitive dedup, sorting - **Constructor**: valid path, null/empty throws - **WaitForDeviceAsync**: timeout validation (negative, zero) **4 integration tests** (`RunnerIntegrationTests.cs`): - Run only when `TF_BUILD=True` or `CI=true` (case-insensitive truthy check), skipped locally - Require pre-installed JDK (`JAVA_HOME`) and Android SDK (`ANDROID_HOME`) on CI agent - `Assert.Ignore` when `ANDROID_HOME` missing (no bootstrap/network dependency) - Cover: constructor, `ListDevicesAsync`, `WaitForDeviceAsync` timeout, tool discovery Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
Summary
Delegates the
adb devices -lparsing, description building, and device/emulator merging logic from theGetAvailableAndroidDevicesMSBuild task to the sharedAdbRunnerinXamarin.Android.Tools.AndroidSdk(via theexternal/xamarin-android-toolssubmodule).This removes ~200 lines of duplicated parsing/formatting/merging code from dotnet/android and consolidates it in the shared android-tools library where it can be reused by other consumers (e.g., the MAUI DevTools CLI).
Changes
external/xamarin-android-tools→feature/adb-runnerbranch (82de0a1)adb emu avd namereturns empty)AdbRunner.ParseAdbDevicesOutput(IEnumerable<string>),AdbRunner.BuildDeviceDescription, andAdbRunner.MergeDevicesAndEmulatorsinstead of having its own parsing logic. AddedConvertToTaskItemsto bridgeAdbDeviceInfo→ITaskItem.AdbRunner/AdbDeviceInfodirectly instead of reflection. All tests preserved with equivalent coverage.Key improvements from updated submodule
The updated
AdbRunnerincludes a TCP console fallback for AVD name resolution. On macOS,adb -s emulator-XXXX emu avd nameoften returns exit code 0 but with empty output, causing:MergeDevicesAndEmulatorsunable to match running emulators with AVD definitions (potential duplicates)The fallback connects to the emulator console port and queries
avd namedirectly, which reliably returns the correct name.Review feedback addressed
ParseAdbDevicesOutputacceptsIEnumerable<string>to avoidstring.Joinallocation — callers passList<string>directlyBuildDeviceDescriptionandMergeDevicesAndEmulatorsaccept optionalAction<TraceLevel, string>logger callback following existingCreateTaskLoggerpattern, restoring MSBuild debug diagnosticsGetAvailableAndroidDevicesstill extendsAndroidAdb(needsbase.RunTask(),ToolPath/ToolExe,LogEventsFromTextOutput)Dependencies
feature/adb-runnertomain/cc @jonathanpeppers