Conversation
|
I'd like to get the System.Diagnostics.Process code unified like mentioned here: |
Addresses PR #283 feedback to use existing ProcessUtils instead of the removed AndroidToolRunner. Simplifies API: - Methods now throw InvalidOperationException on failure - Uses ProcessUtils.RunToolAsync() for all tool invocations - Added AndroidDeviceInfo model - Removed complex ToolRunnerResult wrapper types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
bb947a5 to
b08dd3f
Compare
Addresses PR #283/#284 feedback to use existing ProcessUtils. Simplifies API by throwing exceptions on failure instead of returning result types with error states. Changes: - AdbRunner: Simplified using ProcessUtils.RunToolAsync() - EmulatorRunner: Uses ProcessUtils.StartToolBackground() - Removed duplicate AndroidDeviceInfo from Models directory Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d378294 to
ec0675f
Compare
923285f to
1cf8fc6
Compare
|
Implemented your suggested approach:
Next steps per your plan:
|
|
The dotnet/android side is now ready as a draft PR: dotnet/android#10880 It delegates Workflow:
|
src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs
Show resolved
Hide resolved
193203e to
5f9a212
Compare
…eout
- Broaden AdbDevicesRegex to match any device state (recovery, sideload, etc.)
using \s{2,} separator to avoid matching random text lines
- Skip daemon startup lines (starting with *) in ParseAdbDevicesOutput
- ListDevicesAsync now captures stderr and throws on non-zero exit code
- WaitForDeviceAsync now checks exit code and throws with stdout/stderr context
- Validate timeout: reject negative and zero TimeSpan values
- Add 6 tests: recovery/sideload parsing, state mapping, timeout validation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eout
- Broaden AdbDevicesRegex to match any device state (recovery, sideload, etc.)
using \s{2,} separator to avoid matching random text lines
- Skip daemon startup lines (starting with *) in ParseAdbDevicesOutput
- ListDevicesAsync now captures stderr and throws on non-zero exit code
- WaitForDeviceAsync now checks exit code and throws with stdout/stderr context
- Validate timeout: reject negative and zero TimeSpan values
- Add 6 tests: recovery/sideload parsing, state mapping, timeout validation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
be56ade to
1d82e36
Compare
Port adb device parsing, formatting, and merging logic from dotnet/android GetAvailableAndroidDevices MSBuild task into a shared AdbRunner class. New files: - AdbRunner.cs — ListDevicesAsync, WaitForDeviceAsync, StopEmulatorAsync, ParseAdbDevicesOutput, BuildDeviceDescription, FormatDisplayName, MapAdbStateToStatus, MergeDevicesAndEmulators - AdbDeviceInfo/AdbDeviceType/AdbDeviceStatus — device models - AndroidEnvironmentHelper — shared env var builder for all runners - ProcessUtils.ThrowIfFailed — shared exit code validation Modified files: - EnvironmentVariableNames — add ANDROID_USER_HOME, ANDROID_AVD_HOME - SdkManager.Process.cs — deduplicate env var logic via AndroidEnvironmentHelper Tests: - 43 unit tests (parsing, formatting, merging, path discovery, timeout) - 5 integration tests (CI-only, real SDK tools) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
72bc2ce to
9f316f4
Compare
- StopEmulatorAsync now captures stderr and calls ThrowIfFailed for consistency with ListDevicesAsync/WaitForDeviceAsync. - ThrowIfFailed changed from public to internal since it is only used within the library. - Remove inaccurate cmdline-tools bootstrap claim from integration test doc comment. 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>
…esAndEmulators Accepts Action<TraceLevel, string> to route debug messages through the caller's logging infrastructure (e.g., MSBuild TaskLoggingHelper). Restores log messages lost when logic moved from dotnet/android to android-tools: AVD name formatting, running emulator detection, and non-running emulator additions. Follows the existing CreateTaskLogger pattern used by JdkInstaller. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.
You can also share your feedback on Copilot code review. Take the survey.
| await sdkManager.InstallAsync (new [] { "platform-tools" }, acceptLicenses: true, cancellationToken: cts.Token); | ||
| TestContext.Progress.WriteLine ($"SDK bootstrapped to: {sdkPath}"); | ||
| } | ||
| catch (Exception ex) when (ex is HttpRequestException || ex is TaskCanceledException || ex is InvalidOperationException) { |
There was a problem hiding this comment.
The bootstrap fallback catches TaskCanceledException, but SdkManager.BootstrapAsync/InstallAsync can also throw OperationCanceledException when the CancellationTokenSource.CancelAfter(...) triggers. In that case this setup will fail the test run instead of skipping. Consider catching OperationCanceledException (or Exception ex with an ex is OperationCanceledException pattern) alongside the existing exceptions so timeouts reliably result in Assert.Ignore as intended.
| catch (Exception ex) when (ex is HttpRequestException || ex is TaskCanceledException || ex is InvalidOperationException) { | |
| catch (Exception ex) when (ex is HttpRequestException || ex is TaskCanceledException || ex is OperationCanceledException || ex is InvalidOperationException) { |
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>
Review feedback addressed — commit references
Corresponding dotnet/android consumer: |
dotnet/android compiles the submodule as netstandard2.0 with WarningsAsErrors=Nullable. In netstandard2.0, string.IsNullOrEmpty lacks [NotNullWhen(false)], so the compiler doesn't narrow string? to string after null checks. Add null-forgiving operators where the preceding guard guarantees non-null. Fixes: CS8601 in AndroidEnvironmentHelper.cs (sdkPath, jdkPath) Fixes: CS8620 in AdbRunner.cs (serial in string[] array literal) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace \s{2,} with \s+ to handle tab-separated adb output
- Use explicit state list (device|offline|unauthorized|etc.) instead
of \S+ to prevent false positives from non-device lines
- Add ParseAdbDevicesOutput_TabSeparator test
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
You can also share your feedback on Copilot code review. Take the survey.
| if (Environment.GetEnvironmentVariable ("TF_BUILD") is null && | ||
| Environment.GetEnvironmentVariable ("CI") is null) { | ||
| Assert.Ignore ("Integration tests only run on CI (TF_BUILD or CI env var must be set)."); |
There was a problem hiding this comment.
The XML doc says these tests only run when TF_BUILD=True or CI=true, but RequireCi() only checks that the variables are present (any non-null value). Please align the documentation with the actual gating logic (or tighten the check to require a truthy value).
| if (Environment.GetEnvironmentVariable ("TF_BUILD") is null && | |
| Environment.GetEnvironmentVariable ("CI") is null) { | |
| Assert.Ignore ("Integration tests only run on CI (TF_BUILD or CI env var must be set)."); | |
| var tfBuild = Environment.GetEnvironmentVariable ("TF_BUILD"); | |
| var ci = Environment.GetEnvironmentVariable ("CI"); | |
| if (!string.Equals (tfBuild, "true", StringComparison.OrdinalIgnoreCase) && | |
| !string.Equals (ci, "true", StringComparison.OrdinalIgnoreCase)) { | |
| Assert.Ignore ("Integration tests only run on CI (TF_BUILD=True or CI=true)."); |
| // Pattern to match device lines: <serial> <state> [key:value ...] | ||
| // Requires 2+ spaces between serial and state (adb pads serials). | ||
| // Matches known adb device states. Uses \s+ to handle both space and tab separators. | ||
| // Explicit state list prevents false positives from non-device lines. | ||
| static readonly Regex AdbDevicesRegex = new Regex ( | ||
| @"^([^\s]+)\s+(device|offline|unauthorized|authorizing|no permissions|recovery|sideload|bootloader|connecting|host)\s*(.*)$", | ||
| RegexOptions.Compiled | RegexOptions.IgnoreCase); |
There was a problem hiding this comment.
The comment above AdbDevicesRegex says it "Requires 2+ spaces between serial and state", but the regex pattern uses \s+ (1+ whitespace, including tabs). Please update the comment to match the actual behavior (or tighten the regex if 2+ spaces is truly required).
jonathanpeppers
left a comment
There was a problem hiding this comment.
🤖 AI Review Summary
Found 1 issue: error handling.
- Error handling: Bare
catchblock inGetEmulatorAvdNameAsyncsilently swallows exceptions without logging (AdbRunner.cs:119)
👍 Excellent PR overall — thorough test coverage (44 unit + 9 integration tests), consistent use of ProcessUtils for all process creation, proper CancellationToken propagation throughout, correct OperationCanceledException rethrow before the bare catch, good use of EnvironmentVariableNames constants, and clean code organization (one type per file, file-scoped namespaces). The refactoring of SdkManager.GetEnvironmentVariables into the shared AndroidEnvironmentHelper is a nice DRY improvement.
This review was generated by the android-tools-reviewer skill based on review guidelines established by @jonathanpeppers.
Address review feedback: replace bare catch with catch(Exception ex) and log via Trace.WriteLine for debuggability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Addressed the bare catch feedback in |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
You can also share your feedback on Copilot code review. Take the survey.
| // Use pre-installed Android SDK from ANDROID_HOME (available on CI agents) | ||
| sdkPath = Environment.GetEnvironmentVariable (EnvironmentVariableNames.AndroidHome); | ||
| if (string.IsNullOrEmpty (sdkPath) || !Directory.Exists (sdkPath)) { | ||
| // Fall back to bootstrapping our own SDK | ||
| TestContext.Progress.WriteLine ("ANDROID_HOME not set — bootstrapping SDK..."); | ||
| try { | ||
| bootstrappedSdkPath = Path.Combine (Path.GetTempPath (), $"runner-integration-{Guid.NewGuid ():N}", "android-sdk"); | ||
| sdkManager = new SdkManager (Log); | ||
| sdkManager.JavaSdkPath = jdkPath; | ||
| using var cts = new CancellationTokenSource (TimeSpan.FromMinutes (10)); | ||
| await sdkManager.BootstrapAsync (bootstrappedSdkPath, cancellationToken: cts.Token); | ||
| sdkPath = bootstrappedSdkPath; | ||
| sdkManager.AndroidSdkPath = sdkPath; | ||
|
|
||
| // Install platform-tools (provides adb) | ||
| await sdkManager.InstallAsync (new [] { "platform-tools" }, acceptLicenses: true, cancellationToken: cts.Token); | ||
| TestContext.Progress.WriteLine ($"SDK bootstrapped to: {sdkPath}"); | ||
| } | ||
| catch (Exception ex) when (ex is HttpRequestException || ex is TaskCanceledException || ex is InvalidOperationException) { | ||
| Assert.Ignore ($"SDK bootstrap failed: {ex.Message}"); | ||
| return; | ||
| } | ||
| } | ||
| else { | ||
| TestContext.Progress.WriteLine ($"Using SDK from ANDROID_HOME: {sdkPath}"); | ||
| sdkManager = new SdkManager (Log); | ||
| sdkManager.JavaSdkPath = jdkPath; | ||
| sdkManager.AndroidSdkPath = sdkPath; | ||
| } |
There was a problem hiding this comment.
These “integration” tests will run in Azure Pipelines because TF_BUILD is set, but our pipeline doesn’t provision ANDROID_HOME (see azure-pipelines.yaml Run Tests step). That means this setup will often bootstrap/download an SDK (and install platform-tools) during CI test runs, adding significant runtime and flakiness due to network dependency. Consider skipping unless an explicit opt-in env var is set (e.g., ANDROID_TOOLS_RUN_INTEGRATION_TESTS), or at minimum Assert.Ignore when ANDROID_HOME is missing instead of bootstrapping in the test suite.
| // Use pre-installed Android SDK from ANDROID_HOME (available on CI agents) | |
| sdkPath = Environment.GetEnvironmentVariable (EnvironmentVariableNames.AndroidHome); | |
| if (string.IsNullOrEmpty (sdkPath) || !Directory.Exists (sdkPath)) { | |
| // Fall back to bootstrapping our own SDK | |
| TestContext.Progress.WriteLine ("ANDROID_HOME not set — bootstrapping SDK..."); | |
| try { | |
| bootstrappedSdkPath = Path.Combine (Path.GetTempPath (), $"runner-integration-{Guid.NewGuid ():N}", "android-sdk"); | |
| sdkManager = new SdkManager (Log); | |
| sdkManager.JavaSdkPath = jdkPath; | |
| using var cts = new CancellationTokenSource (TimeSpan.FromMinutes (10)); | |
| await sdkManager.BootstrapAsync (bootstrappedSdkPath, cancellationToken: cts.Token); | |
| sdkPath = bootstrappedSdkPath; | |
| sdkManager.AndroidSdkPath = sdkPath; | |
| // Install platform-tools (provides adb) | |
| await sdkManager.InstallAsync (new [] { "platform-tools" }, acceptLicenses: true, cancellationToken: cts.Token); | |
| TestContext.Progress.WriteLine ($"SDK bootstrapped to: {sdkPath}"); | |
| } | |
| catch (Exception ex) when (ex is HttpRequestException || ex is TaskCanceledException || ex is InvalidOperationException) { | |
| Assert.Ignore ($"SDK bootstrap failed: {ex.Message}"); | |
| return; | |
| } | |
| } | |
| else { | |
| TestContext.Progress.WriteLine ($"Using SDK from ANDROID_HOME: {sdkPath}"); | |
| sdkManager = new SdkManager (Log); | |
| sdkManager.JavaSdkPath = jdkPath; | |
| sdkManager.AndroidSdkPath = sdkPath; | |
| } | |
| // Use pre-installed Android SDK from ANDROID_HOME (if available) | |
| sdkPath = Environment.GetEnvironmentVariable (EnvironmentVariableNames.AndroidHome); | |
| if (string.IsNullOrEmpty (sdkPath) || !Directory.Exists (sdkPath)) { | |
| Assert.Ignore ("ANDROID_HOME not set or invalid — cannot run integration tests. Provision Android SDK and set ANDROID_HOME to enable these tests."); | |
| return; | |
| } | |
| TestContext.Progress.WriteLine ($"Using SDK from ANDROID_HOME: {sdkPath}"); | |
| sdkManager = new SdkManager (Log); | |
| sdkManager.JavaSdkPath = jdkPath; | |
| sdkManager.AndroidSdkPath = sdkPath; |
When 'adb emu avd name' fails (common on macOS), fall back to querying the emulator console directly via TCP on the console port extracted from the serial (emulator-XXXX -> port XXXX). This fixes duplicate device entries when running emulators can't be matched with their AVD definitions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This skill let's you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
This skill lets you say:
review this PR: #284
Some example code reviews:
* #283 (review)
* #284 (review)
This is built off a combination of previous code reviews, saved in
`docs/CODE_REVIEW_POSTMORTEM.md`, and the review rules in
`references/review-rules.md`.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| public AdbRunner (Func<string?> getSdkPath) | ||
| : this (getSdkPath, null) | ||
| { | ||
| } | ||
|
|
||
| public AdbRunner (Func<string?> getSdkPath, Func<string?>? getJdkPath) |
There was a problem hiding this comment.
Why do we pass in Func<string> of these? that seems really weird.
Can we just pass in a string adbPath of a full path to adb?
| } | ||
| return ProcessUtils.FindExecutablesInPath ("adb").FirstOrDefault (); | ||
| } | ||
| } |
There was a problem hiding this comment.
Why do we have code here searching for adb in $PATH? Can we just pass in the full path to adb?
| public bool IsAvailable => AdbPath is not null; | ||
|
|
||
| string RequireAdb () | ||
| { | ||
| return AdbPath ?? throw new InvalidOperationException ("ADB not found."); | ||
| } |
There was a problem hiding this comment.
Again, it seems like you should require a path, and then you don't need any of this either.
| using var client = new TcpClient (); | ||
| using var cts = CancellationTokenSource.CreateLinkedTokenSource (cancellationToken); | ||
| cts.CancelAfter (TimeSpan.FromSeconds (3)); | ||
|
|
||
| // ConnectAsync with CancellationToken not available on netstandard2.0; | ||
| // use Task.Run + token check instead | ||
| var connectTask = client.ConnectAsync ("127.0.0.1", port); | ||
| var completed = await Task.WhenAny (connectTask, Task.Delay (3000, cts.Token)).ConfigureAwait (false); | ||
| if (completed != connectTask) { | ||
| return null; | ||
| } | ||
| await connectTask.ConfigureAwait (false); // observe exceptions |
There was a problem hiding this comment.
What is going on here? Why are we using a TcpClient?!?
|
|
||
| var args = string.IsNullOrEmpty (serial) | ||
| ? new [] { "wait-for-device" } | ||
| : new [] { "-s", serial!, "wait-for-device" }; |
There was a problem hiding this comment.
We should avoid using !, do you even need it if you use our IsNullOrEmpty() extension method? If it's not in this repo, we could move it here from dotnet/android.
|
|
||
| try { | ||
| var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cts.Token, envVars).ConfigureAwait (false); | ||
| ProcessUtils.ThrowIfFailed (exitCode, "adb wait-for-device", stderr.ToString (), stdout.ToString ()); |
There was a problem hiding this comment.
Should ThrowIfFailed() just have an overload for StringWriter and you wouldn't have to call ToString() from callers?
| /// <summary> | ||
| /// Parses the output of 'adb devices -l'. | ||
| /// Ported from dotnet/android GetAvailableAndroidDevices.ParseAdbDevicesOutput. | ||
| /// </summary> | ||
| public static List<AdbDeviceInfo> ParseAdbDevicesOutput (string output) | ||
| { | ||
| return ParseAdbDevicesOutput (output.Split ('\n')); | ||
| } |
There was a problem hiding this comment.
Can we delete this overload?
| switch (adbState.ToLowerInvariant ()) { | ||
| case "device": return AdbDeviceStatus.Online; | ||
| case "offline": return AdbDeviceStatus.Offline; | ||
| case "unauthorized": return AdbDeviceStatus.Unauthorized; | ||
| case "no permissions": return AdbDeviceStatus.NoPermissions; | ||
| default: return AdbDeviceStatus.Unknown; | ||
| } |
There was a problem hiding this comment.
can this use C# pattern matching, and update copilot-instructions.md to do this for all new code?
| /// Builds environment variables needed to run Android SDK tools. | ||
| /// Pass the result to <see cref="ProcessUtils.StartProcess"/> via the environmentVariables parameter. | ||
| /// </summary> | ||
| internal static Dictionary<string, string> GetEnvironmentVariables (string? sdkPath, string? jdkPath) |
There was a problem hiding this comment.
Why are the paths allowed to be null? Then we have ! below?
Can we make sure copilot-instructions.md says to never use ! and to actually fix nullable warnings correctly?
|
|
||
| // ── Cross-runner: verify tools exist ─────────────────────────── | ||
|
|
There was a problem hiding this comment.
It doesn't seem like we should be writing comments like this, they are basically #region and #endregion
Summary
Wraps
adbCLI operations for device management. Addresses #279.Parsing/formatting/merging logic ported from
dotnet/androidGetAvailableAndroidDevicesMSBuild task, enabling code sharing via theexternal/xamarin-android-toolssubmodule. See draft PR: dotnet/android#10880Public API
Internal methods (not part of public API):
GetEmulatorAvdNameAsync— queries AVD name viaadb emu avd name(logs failures viaTrace.WriteLine)ProcessUtils.ThrowIfFailed— shared exit code validationKey Design Decisions
public staticsodotnet/androidcan call them without instantiatingAdbRunner(e.g.,GetAvailableAndroidDevicesMSBuild task passesList<string>toParseAdbDevicesOutput)IEnumerable<string>overload:ParseAdbDevicesOutput(string)delegates toParseAdbDevicesOutput(IEnumerable<string>)—dotnet/androidpassesList<string>directly from output linesBuildDeviceDescriptionandMergeDevicesAndEmulatorsacceptAction<TraceLevel, string>?—dotnet/androidpassesthis.CreateTaskLogger()for MSBuild trace output\s+separator to handle both space and tab-separated adb output. Matches explicit known states (device|offline|unauthorized|authorizing|no permissions|recovery|sideload|bootloader|connecting|host) withIgnoreCaseto prevent false positives from non-device lines. Daemon startup lines (*) are pre-filtered.ListDevicesAsyncandWaitForDeviceAsyncthrowInvalidOperationExceptionwith stderr context on non-zero exit via sharedProcessUtils.ThrowIfFailed(internal)WaitForDeviceAsyncvalidates timeout beforeRequireAdb()soArgumentOutOfRangeExceptionis thrown even when adb is not installedToTitleCaseto normalize mixed-case input (e.g., "PiXeL" → "Pixel")AdbDeviceInfo,AdbDeviceType,AdbDeviceStatuseach in their own fileTrace.WriteLineinstead of bare catch, so failures querying AVD names are debuggable while remaining non-fatalStartProcess: Runners pass env vars dictionary toProcessUtils.StartProcessinstead of mutatingProcessStartInfodirectly.AndroidEnvironmentHelper.GetEnvironmentVariables()builds the dict (ANDROID_HOME, JAVA_HOME, ANDROID_USER_HOME, PATH).Tests
44 unit tests (
AdbRunnerTests.cs):9 integration tests (
RunnerIntegrationTests.cs):TF_BUILDorCIenv var), skipped locallyJAVA_HOME) and Android SDK (ANDROID_HOME) from CI agentIsAvailable,ListDevicesAsync,WaitForDeviceAsynctimeout,ListAvdsAsync,ListAsyncReview Feedback Addressed
01ed6b9IEnumerable<string>overload for ParseAdbDevicesOutput01ed6b9List<string>directlyAction<TraceLevel, string>logger9d4b4f5CreateTaskLoggerpatternThrowIfFailedvisibility →internal71fff349368682Trace.WriteLineinstead of silently swallowingdbf54f9\s++IgnoreCase+ newParseAdbDevicesOutput_TabSeparatortestDepends On
feature/tool-runner-base(PR Add EmulatorRunner for emulator CLI operations #284) —ProcessUtilsshared helpers,AndroidEnvironmentHelper,EnvironmentVariableNamesCo-authored-by: Copilot 223556219+Copilot@users.noreply.github.com