Skip to content

Use shared AdbRunner from android-tools for device listing#10880

Open
rmarinho wants to merge 8 commits intomainfrom
feature/use-shared-adb-runner
Open

Use shared AdbRunner from android-tools for device listing#10880
rmarinho wants to merge 8 commits intomainfrom
feature/use-shared-adb-runner

Conversation

@rmarinho
Copy link
Member

@rmarinho rmarinho commented Mar 3, 2026

Summary

Delegates the adb devices -l parsing, description building, and device/emulator merging logic from the GetAvailableAndroidDevices MSBuild task to the shared AdbRunner in Xamarin.Android.Tools.AndroidSdk (via the external/xamarin-android-tools submodule).

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

  • Submodule update: external/xamarin-android-toolsfeature/adb-runner branch (82de0a1)
    • Includes TCP console fallback for AVD name resolution (fixes macOS issue where adb emu avd name returns empty)
    • Bare catch fix with exception logging
    • Regex fix for explicit adb states and tab support
    • Nullable reference type fixes
  • GetAvailableAndroidDevices.cs: Rewritten to delegate to AdbRunner.ParseAdbDevicesOutput(IEnumerable<string>), AdbRunner.BuildDeviceDescription, and AdbRunner.MergeDevicesAndEmulators instead of having its own parsing logic. Added ConvertToTaskItems to bridge AdbDeviceInfoITaskItem.
  • GetAvailableAndroidDevicesTests.cs: Updated to use AdbRunner/AdbDeviceInfo directly instead of reflection. All tests preserved with equivalent coverage.

Key improvements from updated submodule

The updated AdbRunner includes a TCP console fallback for AVD name resolution. On macOS, adb -s emulator-XXXX emu avd name often returns exit code 0 but with empty output, causing:

  1. Running emulators to show as "sdk gphone64 arm64" instead of their AVD name
  2. MergeDevicesAndEmulators unable to match running emulators with AVD definitions (potential duplicates)

The fallback connects to the emulator console port and queries avd name directly, which reliably returns the correct name.

Review feedback addressed

  • ParseAdbDevicesOutput accepts IEnumerable<string> to avoid string.Join allocation — callers pass List<string> directly
  • BuildDeviceDescription and MergeDevicesAndEmulators accept optional Action<TraceLevel, string> logger callback following existing CreateTaskLogger pattern, restoring MSBuild debug diagnostics
  • GetAvailableAndroidDevices still extends AndroidAdb (needs base.RunTask(), ToolPath/ToolExe, LogEventsFromTextOutput)

Dependencies

/cc @jonathanpeppers

Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

https://github.com/dotnet/android-tools/blob/6a6de1f24572d93048f3bbaa997bcfcf43a472dd/src/Microsoft.Android.Build.BaseTasks/MSBuildExtensions.cs#L191-L207

So, from the MSBuild task, it passes in a Action<TraceLevel, string> for dotnet/android-tools to log with.

rmarinho added a commit to dotnet/android-tools that referenced this pull request Mar 4, 2026
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>
@rmarinho
Copy link
Member Author

rmarinho commented Mar 4, 2026

Done — added optional Action<TraceLevel, string>? logger parameters to BuildDeviceDescription and MergeDevicesAndEmulators in android-tools (dotnet/android-tools@9d4b4f5), following the existing CreateTaskLogger pattern from MSBuildExtensions.

Restored debug messages:

  • AVD name formatting (original → formatted)
  • Running emulator AVD names set
  • Skipping already-running emulators
  • Added non-running emulators

GetAvailableAndroidDevices now passes this.CreateTaskLogger() to both calls so all messages route through the MSBuild logger.

@rmarinho rmarinho force-pushed the feature/use-shared-adb-runner branch from 0995ff2 to 30124fa Compare March 4, 2026 14:48
Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@rmarinho rmarinho force-pushed the feature/use-shared-adb-runner branch from b5df11c to 91b9ca6 Compare March 5, 2026 12:05
rmarinho added a commit to dotnet/android-tools that referenced this pull request Mar 5, 2026
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>
rmarinho added a commit to dotnet/android-tools that referenced this pull request Mar 5, 2026
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>
Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI Review Summary

Found 1 issue: API design alignment with upstream.

  • API design: ConvertToTaskItems parameter should accept IReadOnlyList<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/AdbDeviceInfo API calls.
  • ConvertToTaskItems correctly bridges the shared AdbDeviceInfo model to MSBuild ITaskItem without losing any metadata.
  • Preserves the existing CreateTaskLogger pattern for MSBuild debug diagnostics.

Review generated by android-tools-reviewer from review guidelines by @jonathanpeppers.

rmarinho and others added 7 commits March 6, 2026 08:56
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>
@rmarinho rmarinho force-pushed the feature/use-shared-adb-runner branch from 7480593 to eff03e5 Compare March 6, 2026 08:56
…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>
@rmarinho rmarinho requested a review from jonathanpeppers March 6, 2026 11:50
@rmarinho rmarinho marked this pull request as ready for review March 6, 2026 11:50
Copilot AI review requested due to automatic review settings March 6, 2026 11:51
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

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-tools submodule to a commit that includes improved emulator AVD name resolution and parsing fixes.
  • Reworked GetAvailableAndroidDevices to use AdbRunner for parsing, description building, and device/emulator merging, with a bridge to ITaskItem.
  • Updated tests to exercise AdbRunner/AdbDeviceInfo directly (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 AdbDeviceInfoITaskItem 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.

Comment on lines +61 to +67
// 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 ());
}
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
var mergedDevices = MergeDevicesAndEmulators (adbDevices, availableEmulators);
Devices = mergedDevices.ToArray ();
// Merge using shared logic
var mergedDevices = AdbRunner.MergeDevicesAndEmulators (adbDevices, availableEmulators, this.CreateTaskLogger ());
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +95
item.SetMetadata ("Description", device.Description);
item.SetMetadata ("Type", device.Type.ToString ());
item.SetMetadata ("Status", device.Status.ToString ());
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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").

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +73
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);
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
jonathanpeppers added a commit to dotnet/android-tools that referenced this pull request Mar 6, 2026
## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants