-
Notifications
You must be signed in to change notification settings - Fork 31
Add EmulatorRunner for emulator CLI operations #284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
9e408d8
d0d188a
5fb7e6e
7e42eff
97c05e0
b473917
21f8b94
f19b014
d8ee2d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System; | ||
| using System.Collections.Generic; | ||
|
|
||
| namespace Xamarin.Android.Tools; | ||
|
|
||
| /// <summary> | ||
| /// Options for booting an Android emulator. | ||
| /// </summary> | ||
| public class EmulatorBootOptions | ||
| { | ||
| public TimeSpan BootTimeout { get; set; } = TimeSpan.FromSeconds (300); | ||
| public IEnumerable<string>? AdditionalArgs { get; set; } | ||
| public bool ColdBoot { get; set; } | ||
| public TimeSpan PollInterval { get; set; } = TimeSpan.FromMilliseconds (500); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| namespace Xamarin.Android.Tools; | ||
|
|
||
| /// <summary> | ||
| /// Result of an emulator boot operation. | ||
| /// </summary> | ||
| public record EmulatorBootResult | ||
| { | ||
| public bool Success { get; init; } | ||
| public string? Serial { get; init; } | ||
| public string? ErrorMessage { get; init; } | ||
|
Comment on lines
+9
to
+13
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ public class AdbRunner | |
| { | ||
| readonly string adbPath; | ||
| readonly IDictionary<string, string>? environmentVariables; | ||
| readonly Action<TraceLevel, string>? logger; | ||
|
|
||
| // Pattern to match device lines: <serial> <state> [key:value ...] | ||
| // Uses \s+ to match one or more whitespace characters (spaces or tabs) between fields. | ||
|
|
@@ -35,19 +36,21 @@ public class AdbRunner | |
| /// </summary> | ||
| /// <param name="adbPath">Full path to the adb executable (e.g., "/path/to/sdk/platform-tools/adb").</param> | ||
| /// <param name="environmentVariables">Optional environment variables to pass to adb processes.</param> | ||
| public AdbRunner (string adbPath, IDictionary<string, string>? environmentVariables = null) | ||
| /// <param name="logger">Optional logger callback for diagnostic messages.</param> | ||
| public AdbRunner (string adbPath, IDictionary<string, string>? environmentVariables = null, Action<TraceLevel, string>? logger = null) | ||
| { | ||
| if (string.IsNullOrWhiteSpace (adbPath)) | ||
| throw new ArgumentException ("Path to adb must not be empty.", nameof (adbPath)); | ||
| this.adbPath = adbPath; | ||
| this.environmentVariables = environmentVariables; | ||
| this.logger = logger; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Lists connected devices using 'adb devices -l'. | ||
| /// For emulators, queries the AVD name using 'adb -s <serial> emu avd name'. | ||
| /// </summary> | ||
| public async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (CancellationToken cancellationToken = default) | ||
| public virtual async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (CancellationToken cancellationToken = default) | ||
| { | ||
| using var stdout = new StringWriter (); | ||
| using var stderr = new StringWriter (); | ||
|
|
@@ -135,6 +138,92 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati | |
| ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} emu kill", stderr); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Gets a system property from a device via 'adb -s <serial> shell getprop <property>'. | ||
| /// Returns the property value (first non-empty line of stdout), or <c>null</c> on failure. | ||
| /// </summary> | ||
| public virtual async Task<string?> GetShellPropertyAsync (string serial, string propertyName, CancellationToken cancellationToken = default) | ||
| { | ||
| using var stdout = new StringWriter (); | ||
| using var stderr = new StringWriter (); | ||
| var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "shell", "getprop", propertyName); | ||
| var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); | ||
| if (exitCode != 0) { | ||
| var stderrText = stderr.ToString ().Trim (); | ||
| if (stderrText.Length > 0) | ||
| logger?.Invoke (TraceLevel.Warning, $"adb shell getprop {propertyName} failed (exit {exitCode}): {stderrText}"); | ||
| return null; | ||
| } | ||
| return FirstNonEmptyLine (stdout.ToString ()); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Runs a shell command on a device via 'adb -s <serial> shell <command>'. | ||
| /// Returns the full stdout output trimmed, or <c>null</c> on failure. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// The <paramref name="command"/> is passed as a single argument to <c>adb shell</c>, | ||
| /// which means the device's shell interprets it (shell expansion, pipes, semicolons are active). | ||
| /// Do not pass untrusted or user-supplied input without proper validation. | ||
| /// </remarks> | ||
| public virtual async Task<string?> RunShellCommandAsync (string serial, string command, CancellationToken cancellationToken) | ||
| { | ||
| using var stdout = new StringWriter (); | ||
| using var stderr = new StringWriter (); | ||
| var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "shell", command); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 💡 API design — \command\ is passed as a single argument to \�db shell, so the device shell interprets it (expansion, pipes, semicolons). Fine for current hardcoded callers, but since this is \public virtual, a future caller could pass unsanitized input. Consider documenting the shell-interpretation behavior, or offering a structured overload where \�db shell\ receives multiple args and \�xec()\s directly. Rule: Structured args, not string interpolation (Postmortem #49) |
||
| var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); | ||
| if (exitCode != 0) { | ||
| var stderrText = stderr.ToString ().Trim (); | ||
| if (stderrText.Length > 0) | ||
| logger?.Invoke (TraceLevel.Warning, $"adb shell {command} failed (exit {exitCode}): {stderrText}"); | ||
| return null; | ||
| } | ||
| var output = stdout.ToString ().Trim (); | ||
| return output.Length > 0 ? output : null; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Runs a shell command on a device via <c>adb -s <serial> shell <command> <args></c>. | ||
| /// Returns the full stdout output trimmed, or <c>null</c> on failure. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// When <c>adb shell</c> receives the command and arguments as separate tokens, it uses | ||
| /// <c>exec()</c> directly on the device — bypassing the device's shell interpreter. | ||
| /// This avoids shell expansion, pipes, and injection risks, making it safer for dynamic input. | ||
| /// </remarks> | ||
| public virtual async Task<string?> RunShellCommandAsync (string serial, string command, string[] args, CancellationToken cancellationToken = default) | ||
| { | ||
| using var stdout = new StringWriter (); | ||
| using var stderr = new StringWriter (); | ||
| // Build: adb -s <serial> shell <command> <arg1> <arg2> ... | ||
| var allArgs = new string [3 + 1 + args.Length]; | ||
| allArgs [0] = "-s"; | ||
| allArgs [1] = serial; | ||
| allArgs [2] = "shell"; | ||
| allArgs [3] = command; | ||
| Array.Copy (args, 0, allArgs, 4, args.Length); | ||
| var psi = ProcessUtils.CreateProcessStartInfo (adbPath, allArgs); | ||
| var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false); | ||
| if (exitCode != 0) { | ||
| var stderrText = stderr.ToString ().Trim (); | ||
| if (stderrText.Length > 0) | ||
| logger?.Invoke (TraceLevel.Warning, $"adb shell {command} failed (exit {exitCode}): {stderrText}"); | ||
| return null; | ||
| } | ||
| var output = stdout.ToString ().Trim (); | ||
| return output.Length > 0 ? output : null; | ||
| } | ||
|
|
||
| internal static string? FirstNonEmptyLine (string output) | ||
| { | ||
| foreach (var line in output.Split ('\n')) { | ||
| var trimmed = line.Trim (); | ||
| if (trimmed.Length > 0) | ||
| return trimmed; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Parses the output lines from 'adb devices -l'. | ||
| /// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.