Skip to content

Commit 9f37951

Browse files
rmarinhoCopilot
andcommitted
Merge adb-runner + emulator-runner + avdmanager-runner into integration
Resolves merge conflicts: keeps canonical AdbRunner from #283, adds shell methods and virtual ListDevicesAsync for EmulatorRunner BootAndWait, keeps updated AvdManagerRunner with resolved-path constructor from #282. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2 parents 6d30541 + d96296a commit 9f37951

6 files changed

Lines changed: 103 additions & 117 deletions

File tree

src/Xamarin.Android.Tools.AndroidSdk/ProcessUtils.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ internal static void ThrowIfFailed (int exitCode, string command, string? stderr
225225
/// <summary>
226226
/// Overload that accepts <see cref="StringWriter"/> directly so callers don't need to call ToString().
227227
/// </summary>
228-
internal static void ThrowIfFailed (int exitCode, string command, StringWriter? stderr = null, StringWriter? stdout = null)
228+
internal static void ThrowIfFailed (int exitCode, string command, StringWriter? stderr, StringWriter? stdout = null)
229229
{
230230
ThrowIfFailed (exitCode, command, stderr?.ToString (), stdout?.ToString ());
231231
}
@@ -262,6 +262,7 @@ internal static void ValidateNotNullOrEmpty (string? value, string paramName)
262262
}
263263
subdirs.Sort ((a, b) => b.version!.CompareTo (a.version));
264264

265+
// Check versioned directories first (highest version first), then "latest"
265266
foreach (var (name, _) in subdirs) {
266267
var toolPath = Path.Combine (cmdlineToolsDir, name, "bin", toolName + extension);
267268
if (File.Exists (toolPath))
@@ -272,6 +273,7 @@ internal static void ValidateNotNullOrEmpty (string? value, string paramName)
272273
return latestPath;
273274
}
274275

276+
// Legacy fallback: tools/bin/<tool>
275277
var legacyPath = Path.Combine (sdkPath, "tools", "bin", toolName + extension);
276278
return File.Exists (legacyPath) ? legacyPath : null;
277279
}

src/Xamarin.Android.Tools.AndroidSdk/Runners/AdbRunner.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public AdbRunner (string adbPath, IDictionary<string, string>? environmentVariab
4747
/// Lists connected devices using 'adb devices -l'.
4848
/// For emulators, queries the AVD name using 'adb -s &lt;serial&gt; emu avd name'.
4949
/// </summary>
50-
public async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (CancellationToken cancellationToken = default)
50+
public virtual async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (CancellationToken cancellationToken = default)
5151
{
5252
using var stdout = new StringWriter ();
5353
using var stderr = new StringWriter ();
@@ -135,6 +135,40 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati
135135
ProcessUtils.ThrowIfFailed (exitCode, $"adb -s {serial} emu kill", stderr);
136136
}
137137

138+
/// <summary>
139+
/// Runs 'adb -s &lt;serial&gt; shell getprop &lt;property&gt;' and returns the first non-empty line.
140+
/// </summary>
141+
public virtual async Task<string?> GetShellPropertyAsync (string serial, string property, CancellationToken cancellationToken = default)
142+
{
143+
using var stdout = new StringWriter ();
144+
using var stderr = new StringWriter ();
145+
var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "shell", "getprop", property);
146+
await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false);
147+
return FirstNonEmptyLine (stdout.ToString ());
148+
}
149+
150+
/// <summary>
151+
/// Runs 'adb -s &lt;serial&gt; shell &lt;command&gt;' and returns the first non-empty line.
152+
/// </summary>
153+
public virtual async Task<string?> RunShellCommandAsync (string serial, string command, CancellationToken cancellationToken = default)
154+
{
155+
using var stdout = new StringWriter ();
156+
using var stderr = new StringWriter ();
157+
var psi = ProcessUtils.CreateProcessStartInfo (adbPath, "-s", serial, "shell", command);
158+
await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false);
159+
return FirstNonEmptyLine (stdout.ToString ());
160+
}
161+
162+
internal static string? FirstNonEmptyLine (string output)
163+
{
164+
foreach (var line in output.Split (new [] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries)) {
165+
var trimmed = line.Trim ();
166+
if (trimmed.Length > 0)
167+
return trimmed;
168+
}
169+
return null;
170+
}
171+
138172
/// <summary>
139173
/// Parses the output lines from 'adb devices -l'.
140174
/// Accepts an <see cref="IEnumerable{T}"/> to avoid allocating a joined string.

src/Xamarin.Android.Tools.AndroidSdk/Runners/AndroidEnvironmentHelper.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,15 @@ internal static Dictionary<string, string> GetEnvironmentVariables (string? sdkP
3737

3838
return env;
3939
}
40+
41+
/// <summary>
42+
/// Applies Android SDK environment variables directly to a <see cref="System.Diagnostics.ProcessStartInfo"/>.
43+
/// Used by runners that manage their own process lifecycle (e.g., EmulatorRunner).
44+
/// </summary>
45+
internal static void ConfigureEnvironment (System.Diagnostics.ProcessStartInfo psi, string? sdkPath, string? jdkPath)
46+
{
47+
var env = GetEnvironmentVariables (sdkPath, jdkPath);
48+
foreach (var kvp in env)
49+
psi.Environment [kvp.Key] = kvp.Value;
50+
}
4051
}

src/Xamarin.Android.Tools.AndroidSdk/Runners/AvdManagerRunner.cs

Lines changed: 25 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.Diagnostics;
76
using System.IO;
87
using System.Linq;
98
using System.Threading;
@@ -16,91 +15,39 @@ namespace Xamarin.Android.Tools;
1615
/// </summary>
1716
public class AvdManagerRunner
1817
{
19-
readonly Func<string?> getSdkPath;
20-
readonly Func<string?>? getJdkPath;
18+
readonly string avdManagerPath;
19+
readonly IDictionary<string, string>? environmentVariables;
2120

22-
public AvdManagerRunner (Func<string?> getSdkPath)
23-
: this (getSdkPath, null)
24-
{
25-
}
26-
27-
public AvdManagerRunner (Func<string?> getSdkPath, Func<string?>? getJdkPath)
28-
{
29-
this.getSdkPath = getSdkPath ?? throw new ArgumentNullException (nameof (getSdkPath));
30-
this.getJdkPath = getJdkPath;
31-
}
32-
33-
public string? AvdManagerPath {
34-
get {
35-
var sdkPath = getSdkPath ();
36-
if (string.IsNullOrEmpty (sdkPath))
37-
return null;
38-
39-
var ext = OS.IsWindows ? ".bat" : "";
40-
var cmdlineToolsDir = Path.Combine (sdkPath, "cmdline-tools");
41-
42-
if (Directory.Exists (cmdlineToolsDir)) {
43-
// Versioned dirs sorted descending, then "latest" as fallback
44-
var searchDirs = Directory.GetDirectories (cmdlineToolsDir)
45-
.Select (Path.GetFileName)
46-
.Where (n => n != "latest" && !string.IsNullOrEmpty (n))
47-
.OrderByDescending (n => Version.TryParse (n, out var v) ? v : new Version (0, 0))
48-
.Append ("latest");
49-
50-
foreach (var dir in searchDirs) {
51-
var toolPath = Path.Combine (cmdlineToolsDir, dir!, "bin", "avdmanager" + ext);
52-
if (File.Exists (toolPath))
53-
return toolPath;
54-
}
55-
}
56-
57-
// Legacy fallback: tools/bin/avdmanager
58-
var legacyPath = Path.Combine (sdkPath, "tools", "bin", "avdmanager" + ext);
59-
return File.Exists (legacyPath) ? legacyPath : null;
60-
}
61-
}
62-
63-
public bool IsAvailable => !string.IsNullOrEmpty (AvdManagerPath);
64-
65-
string RequireAvdManagerPath ()
66-
{
67-
return AvdManagerPath ?? throw new InvalidOperationException ("AVD Manager not found.");
68-
}
69-
70-
void ConfigureEnvironment (ProcessStartInfo psi)
21+
/// <summary>
22+
/// Creates a new AvdManagerRunner with the full path to the avdmanager executable.
23+
/// </summary>
24+
/// <param name="avdManagerPath">Full path to avdmanager (e.g., "/path/to/sdk/cmdline-tools/latest/bin/avdmanager").</param>
25+
/// <param name="environmentVariables">Optional environment variables to pass to avdmanager processes.</param>
26+
public AvdManagerRunner (string avdManagerPath, IDictionary<string, string>? environmentVariables = null)
7127
{
72-
AndroidEnvironmentHelper.ConfigureEnvironment (psi, getSdkPath (), getJdkPath?.Invoke ());
28+
if (string.IsNullOrWhiteSpace (avdManagerPath))
29+
throw new ArgumentException ("Path to avdmanager must not be empty.", nameof (avdManagerPath));
30+
this.avdManagerPath = avdManagerPath;
31+
this.environmentVariables = environmentVariables;
7332
}
7433

7534
public async Task<IReadOnlyList<AvdInfo>> ListAvdsAsync (CancellationToken cancellationToken = default)
7635
{
77-
var avdManagerPath = RequireAvdManagerPath ();
78-
7936
using var stdout = new StringWriter ();
8037
using var stderr = new StringWriter ();
8138
var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "list", "avd");
82-
ConfigureEnvironment (psi);
83-
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken).ConfigureAwait (false);
39+
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables).ConfigureAwait (false);
8440

85-
if (exitCode != 0)
86-
throw new InvalidOperationException ($"avdmanager list avd failed (exit code {exitCode}): {stderr.ToString ().Trim ()}");
41+
ProcessUtils.ThrowIfFailed (exitCode, "avdmanager list avd", stderr);
8742

8843
return ParseAvdListOutput (stdout.ToString ());
8944
}
9045

9146
public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, string? deviceProfile = null,
9247
bool force = false, CancellationToken cancellationToken = default)
9348
{
94-
if (name is null)
95-
throw new ArgumentNullException (nameof (name));
96-
if (name.Length == 0)
97-
throw new ArgumentException ("Value cannot be an empty string.", nameof (name));
98-
if (systemImage is null)
99-
throw new ArgumentNullException (nameof (systemImage));
100-
if (systemImage.Length == 0)
101-
throw new ArgumentException ("Value cannot be an empty string.", nameof (systemImage));
102-
103-
var avdManagerPath = RequireAvdManagerPath ();
49+
ProcessUtils.ValidateNotNullOrEmpty (name, nameof (name));
50+
ProcessUtils.ValidateNotNullOrEmpty (systemImage, nameof (systemImage));
10451

10552
// Check if AVD already exists — return it instead of failing
10653
if (!force) {
@@ -116,7 +63,7 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
11663
force = true;
11764

11865
var args = new List<string> { "create", "avd", "-n", name, "-k", systemImage };
119-
if (!string.IsNullOrEmpty (deviceProfile))
66+
if (deviceProfile is { Length: > 0 })
12067
args.AddRange (new [] { "-d", deviceProfile });
12168
if (force)
12269
args.Add ("--force");
@@ -125,10 +72,9 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
12572
using var stderr = new StringWriter ();
12673
var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, args.ToArray ());
12774
psi.RedirectStandardInput = true;
128-
ConfigureEnvironment (psi);
12975

13076
// avdmanager prompts "Do you wish to create a custom hardware profile?" — answer "no"
131-
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken,
77+
var exitCode = await ProcessUtils.StartProcess (psi, stdout, stderr, cancellationToken, environmentVariables,
13278
onStarted: p => {
13379
try {
13480
p.StandardInput.WriteLine ("no");
@@ -138,12 +84,7 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
13884
}
13985
}).ConfigureAwait (false);
14086

141-
if (exitCode != 0) {
142-
var errorOutput = stderr.ToString ().Trim ();
143-
if (string.IsNullOrEmpty (errorOutput))
144-
errorOutput = stdout.ToString ().Trim ();
145-
throw new InvalidOperationException ($"Failed to create AVD '{name}': {errorOutput}");
146-
}
87+
ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager create avd -n {name}", stderr, stdout);
14788

14889
// Re-list to get the actual path from avdmanager (respects ANDROID_USER_HOME/ANDROID_AVD_HOME)
14990
var avds = await ListAvdsAsync (cancellationToken).ConfigureAwait (false);
@@ -161,20 +102,13 @@ public async Task<AvdInfo> CreateAvdAsync (string name, string systemImage, stri
161102

162103
public async Task DeleteAvdAsync (string name, CancellationToken cancellationToken = default)
163104
{
164-
if (name is null)
165-
throw new ArgumentNullException (nameof (name));
166-
if (name.Length == 0)
167-
throw new ArgumentException ("Value cannot be an empty string.", nameof (name));
168-
169-
var avdManagerPath = RequireAvdManagerPath ();
105+
ProcessUtils.ValidateNotNullOrEmpty (name, nameof (name));
170106

171107
using var stderr = new StringWriter ();
172108
var psi = ProcessUtils.CreateProcessStartInfo (avdManagerPath, "delete", "avd", "--name", name);
173-
ConfigureEnvironment (psi);
174-
var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken).ConfigureAwait (false);
109+
var exitCode = await ProcessUtils.StartProcess (psi, null, stderr, cancellationToken, environmentVariables).ConfigureAwait (false);
175110

176-
if (exitCode != 0)
177-
throw new InvalidOperationException ($"Failed to delete AVD '{name}': {stderr.ToString ().Trim ()}");
111+
ProcessUtils.ThrowIfFailed (exitCode, $"avdmanager delete avd --name {name}", stderr);
178112
}
179113

180114
internal static List<AvdInfo> ParseAvdListOutput (string output)
@@ -208,13 +142,13 @@ internal static List<AvdInfo> ParseAvdListOutput (string output)
208142
static string GetAvdRootDirectory ()
209143
{
210144
// ANDROID_AVD_HOME takes highest priority
211-
var avdHome = Environment.GetEnvironmentVariable ("ANDROID_AVD_HOME");
212-
if (!string.IsNullOrEmpty (avdHome))
145+
var avdHome = Environment.GetEnvironmentVariable (EnvironmentVariableNames.AndroidAvdHome);
146+
if (avdHome is { Length: > 0 })
213147
return avdHome;
214148

215149
// ANDROID_USER_HOME/avd is the next option
216150
var userHome = Environment.GetEnvironmentVariable (EnvironmentVariableNames.AndroidUserHome);
217-
if (!string.IsNullOrEmpty (userHome))
151+
if (userHome is { Length: > 0 })
218152
return Path.Combine (userHome, "avd");
219153

220154
// Default: ~/.android/avd

0 commit comments

Comments
 (0)