Skip to content
This repository was archived by the owner on Jul 13, 2026. It is now read-only.

Commit 25e7711

Browse files
rmarinhoCopilot
andcommitted
Simplify AdbRunner constructor: require full adb path
Address review feedback (threads 41-43): replace Func<string?> getSdkPath constructor with string adbPath that takes the full path to the adb executable. Remove AdbPath property, IsAvailable property, RequireAdb(), PATH discovery fallback, and getSdkPath/getJdkPath fields. Callers are now responsible for resolving the adb path before constructing. Environment variables can optionally be passed via the constructor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 90ef8b5 commit 25e7711

3 files changed

Lines changed: 50 additions & 92 deletions

File tree

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

Lines changed: 23 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
usingSystem.Diagnostics;
77
usingSystem.Globalization;
88
usingSystem.IO;
9-
usingSystem.Linq;
109
usingSystem.Net.Sockets;
1110
usingSystem.Text.RegularExpressions;
1211
usingSystem.Threading;
@@ -20,52 +19,28 @@ namespace Xamarin.Android.Tools;
2019
/// </summary>
2120
publicclassAdbRunner
2221
{
23-
readonlyFunc<string?>getSdkPath;
24-
readonlyFunc<string?>?getJdkPath;
22+
readonlystringadbPath;
23+
readonlyIDictionary<string,string>?environmentVariables;
2524

2625
// Pattern to match device lines: <serial> <state> [key:value ...]
27-
// Requires 2+ spaces between serial and state (adb pads serials).
28-
// Matches known adb device states. Uses \s+ to handle both space and tab separators.
26+
// Uses \s+ to handle both space and tab separators.
2927
// Explicit state list prevents false positives from non-device lines.
3028
staticreadonlyRegexAdbDevicesRegex=newRegex(
3129
@"^([^\s]+)\s+(device|offline|unauthorized|authorizing|no permissions|recovery|sideload|bootloader|connecting|host)\s*(.*)$",
3230
RegexOptions.Compiled|RegexOptions.IgnoreCase);
3331
staticreadonlyRegexApiRegex=newRegex(@"\bApi\b",RegexOptions.Compiled);
3432

35-
publicAdbRunner(Func<string?>getSdkPath)
36-
:this(getSdkPath,null)
37-
{
38-
}
39-
40-
publicAdbRunner(Func<string?>getSdkPath,Func<string?>?getJdkPath)
41-
{
42-
this.getSdkPath=getSdkPath??thrownewArgumentNullException(nameof(getSdkPath));
43-
this.getJdkPath=getJdkPath;
44-
}
45-
46-
publicstring?AdbPath{
47-
get{
48-
varsdkPath=getSdkPath();
49-
if(!string.IsNullOrEmpty(sdkPath)){
50-
varext=OS.IsWindows?".exe":"";
51-
varsdkAdb=Path.Combine(sdkPath,"platform-tools","adb"+ext);
52-
if(File.Exists(sdkAdb))
53-
returnsdkAdb;
54-
}
55-
returnProcessUtils.FindExecutablesInPath("adb").FirstOrDefault();
56-
}
57-
}
58-
59-
publicboolIsAvailable=>AdbPathis not null;
60-
61-
stringRequireAdb()
62-
{
63-
returnAdbPath??thrownewInvalidOperationException("ADB not found.");
64-
}
65-
66-
IDictionary<string,string>GetEnvironmentVariables()
33+
/// <summary>
34+
/// Creates a new AdbRunner with the full path to the adb executable.
35+
/// </summary>
36+
/// <param name="adbPath">Full path to the adb executable (e.g., "/path/to/sdk/platform-tools/adb").</param>
37+
/// <param name="environmentVariables">Optional environment variables to pass to adb processes.</param>
38+
publicAdbRunner(stringadbPath,IDictionary<string,string>?environmentVariables=null)
6739
{
68-
returnAndroidEnvironmentHelper.GetEnvironmentVariables(getSdkPath(),getJdkPath?.Invoke());
40+
if(string.IsNullOrWhiteSpace(adbPath))
41+
thrownewArgumentException("Path to adb must not be empty.",nameof(adbPath));
42+
this.adbPath=adbPath;
43+
this.environmentVariables=environmentVariables;
6944
}
7045

7146
/// <summary>
@@ -74,12 +49,10 @@ IDictionary<string, string> GetEnvironmentVariables ()
7449
/// </summary>
7550
publicasyncTask<IReadOnlyList<AdbDeviceInfo>>ListDevicesAsync(CancellationTokencancellationToken=default)
7651
{
77-
varadb=RequireAdb();
78-
varenvVars=GetEnvironmentVariables();
7952
usingvarstdout=newStringWriter();
8053
usingvarstderr=newStringWriter();
81-
varpsi=ProcessUtils.CreateProcessStartInfo(adb,"devices","-l");
82-
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,envVars).ConfigureAwait(false);
54+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"devices","-l");
55+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
8356

8457
ProcessUtils.ThrowIfFailed(exitCode,"adb devices -l",stderr.ToString());
8558

@@ -88,7 +61,7 @@ public async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (CancellationTo
8861
// For each emulator, try to get the AVD name
8962
foreach(vardeviceindevices){
9063
if(device.Type==AdbDeviceType.Emulator){
91-
device.AvdName=awaitGetEmulatorAvdNameAsync(adb,device.Serial,cancellationToken).ConfigureAwait(false);
64+
device.AvdName=awaitGetEmulatorAvdNameAsync(device.Serial,cancellationToken).ConfigureAwait(false);
9265
device.Description=BuildDeviceDescription(device);
9366
}
9467
}
@@ -101,13 +74,12 @@ public async Task<IReadOnlyList<AdbDeviceInfo>> ListDevicesAsync (CancellationTo
10174
/// falling back to a direct emulator console TCP query if that fails.
10275
/// Ported from dotnet/android GetAvailableAndroidDevices.GetEmulatorAvdName.
10376
/// </summary>
104-
internalasyncTask<string?>GetEmulatorAvdNameAsync(stringadbPath,stringserial,CancellationTokencancellationToken=default)
77+
internalasyncTask<string?>GetEmulatorAvdNameAsync(stringserial,CancellationTokencancellationToken=default)
10578
{
10679
try{
107-
varenvVars=GetEnvironmentVariables();
10880
usingvarstdout=newStringWriter();
10981
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"emu","avd","name");
110-
awaitProcessUtils.StartProcess(psi,stdout,null,cancellationToken,envVars).ConfigureAwait(false);
82+
awaitProcessUtils.StartProcess(psi,stdout,null,cancellationToken,environmentVariables).ConfigureAwait(false);
11183

11284
foreach(varlineinstdout.ToString().Split('\n')){
11385
vartrimmed=line.Trim();
@@ -187,14 +159,11 @@ public async Task WaitForDeviceAsync (string? serial = null, TimeSpan? timeout =
187159
if(effectiveTimeout<=TimeSpan.Zero)
188160
thrownewArgumentOutOfRangeException(nameof(timeout),effectiveTimeout,"Timeout must be a positive value.");
189161

190-
varadb=RequireAdb();
191-
varenvVars=GetEnvironmentVariables();
192-
193162
varargs=string.IsNullOrEmpty(serial)
194163
?new[]{"wait-for-device"}
195-
:new[]{"-s",serial!,"wait-for-device"};
164+
:new[]{"-s",serial,"wait-for-device"};
196165

197-
varpsi=ProcessUtils.CreateProcessStartInfo(adb,args);
166+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,args);
198167

199168
usingvarcts=CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
200169
cts.CancelAfter(effectiveTimeout);
@@ -203,7 +172,7 @@ public async Task WaitForDeviceAsync (string? serial = null, TimeSpan? timeout =
203172
usingvarstderr=newStringWriter();
204173

205174
try{
206-
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cts.Token,envVars).ConfigureAwait(false);
175+
varexitCode=awaitProcessUtils.StartProcess(psi,stdout,stderr,cts.Token,environmentVariables).ConfigureAwait(false);
207176
ProcessUtils.ThrowIfFailed(exitCode,"adb wait-for-device",stderr.ToString(),stdout.ToString());
208177
}catch(OperationCanceledException)when(!cancellationToken.IsCancellationRequested){
209178
thrownewTimeoutException($"Timed out waiting for device after {effectiveTimeout.TotalSeconds}s.");
@@ -215,11 +184,9 @@ public async Task StopEmulatorAsync (string serial, CancellationToken cancellati
215184
if(string.IsNullOrWhiteSpace(serial))
216185
thrownewArgumentException("Serial must not be empty.",nameof(serial));
217186

218-
varadb=RequireAdb();
219-
varenvVars=GetEnvironmentVariables();
220-
varpsi=ProcessUtils.CreateProcessStartInfo(adb,"-s",serial,"emu","kill");
187+
varpsi=ProcessUtils.CreateProcessStartInfo(adbPath,"-s",serial,"emu","kill");
221188
usingvarstderr=newStringWriter();
222-
varexitCode=awaitProcessUtils.StartProcess(psi,null,stderr,cancellationToken,envVars).ConfigureAwait(false);
189+
varexitCode=awaitProcessUtils.StartProcess(psi,null,stderr,cancellationToken,environmentVariables).ConfigureAwait(false);
223190
ProcessUtils.ThrowIfFailed(exitCode,$"adb -s {serial} emu kill",stderr.ToString());
224191
}
225192

‎tests/Xamarin.Android.Tools.AndroidSdk-Tests/AdbRunnerTests.cs‎

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
usingSystem;
45
usingSystem.Collections.Generic;
56
usingSystem.IO;
67
usingNUnit.Framework;
@@ -486,32 +487,21 @@ public void MergeDevicesAndEmulators_EmptyAdbDevices_ReturnsAllAvailable ()
486487
// Consumer: MAUI DevTools Adb provider (AdbPath, IsAvailable properties)
487488

488489
[Test]
489-
publicvoidAdbPath_FindsInSdk()
490+
publicvoidConstructor_NullPath_ThrowsArgumentException()
490491
{
491-
vartempDir=Path.Combine(Path.GetTempPath(),$"adb-test-{Path.GetRandomFileName()}");
492-
varplatformTools=Path.Combine(tempDir,"platform-tools");
493-
Directory.CreateDirectory(platformTools);
494-
495-
try{
496-
varadbName=OS.IsWindows?"adb.exe":"adb";
497-
File.WriteAllText(Path.Combine(platformTools,adbName),"");
498-
499-
varrunner=newAdbRunner(()=>tempDir);
492+
Assert.Throws<ArgumentException>(()=>newAdbRunner(null!));
493+
}
500494

501-
Assert.IsNotNull(runner.AdbPath);
502-
Assert.IsTrue(runner.IsAvailable);
503-
Assert.IsTrue(runner.AdbPath!.Contains("platform-tools"));
504-
}finally{
505-
Directory.Delete(tempDir,true);
506-
}
495+
[Test]
496+
publicvoidConstructor_EmptyPath_ThrowsArgumentException()
497+
{
498+
Assert.Throws<ArgumentException>(()=>newAdbRunner(""));
507499
}
508500

509501
[Test]
510-
publicvoidAdbPath_NullSdkPath_StillSearchesPath()
502+
publicvoidConstructor_WhitespacePath_ThrowsArgumentException()
511503
{
512-
varrunner=newAdbRunner(()=>null);
513-
// Should not throw — falls back to PATH search
514-
_=runner.AdbPath;
504+
Assert.Throws<ArgumentException>(()=>newAdbRunner(" "));
515505
}
516506

517507
[Test]
@@ -627,15 +617,15 @@ public void MapAdbStateToStatus_Sideload_ReturnsUnknown ()
627617
[Test]
628618
publicvoidWaitForDeviceAsync_NegativeTimeout_ThrowsArgumentOutOfRange()
629619
{
630-
varrunner=newAdbRunner(()=>"/fake/sdk");
620+
varrunner=newAdbRunner("/fake/sdk/platform-tools/adb");
631621
Assert.ThrowsAsync<System.ArgumentOutOfRangeException>(
632622
async()=>awaitrunner.WaitForDeviceAsync(timeout:System.TimeSpan.FromSeconds(-1)));
633623
}
634624

635625
[Test]
636626
publicvoidWaitForDeviceAsync_ZeroTimeout_ThrowsArgumentOutOfRange()
637627
{
638-
varrunner=newAdbRunner(()=>"/fake/sdk");
628+
varrunner=newAdbRunner("/fake/sdk/platform-tools/adb");
639629
Assert.ThrowsAsync<System.ArgumentOutOfRangeException>(
640630
async()=>awaitrunner.WaitForDeviceAsync(timeout:System.TimeSpan.Zero));
641631
}

‎tests/Xamarin.Android.Tools.AndroidSdk-Tests/RunnerIntegrationTests.cs‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class RunnerIntegrationTests
2525
{
2626
staticstringsdkPath;
2727
staticstringjdkPath;
28+
staticstringadbPath;
2829
staticSdkManagersdkManager;
2930
staticstringbootstrappedSdkPath;
3031

@@ -88,6 +89,12 @@ public async Task OneTimeSetUp ()
8889
sdkManager.JavaSdkPath=jdkPath;
8990
sdkManager.AndroidSdkPath=sdkPath;
9091
}
92+
93+
// Resolve the full path to adb for AdbRunner
94+
varadbExe=OS.IsWindows?"adb.exe":"adb";
95+
adbPath=Path.Combine(sdkPath,"platform-tools",adbExe);
96+
if(!File.Exists(adbPath))
97+
Assert.Ignore($"adb not found at {adbPath}");
9198
}
9299

93100
[OneTimeTearDown]
@@ -112,19 +119,16 @@ public void OneTimeTearDown ()
112119
// ── AdbRunner integration ──────────────────────────────────────
113120

114121
[Test]
115-
publicvoidAdbRunner_IsAvailable_WithSdk()
122+
publicvoidAdbRunner_Constructor_AcceptsValidPath()
116123
{
117-
varrunner=newAdbRunner(()=>sdkPath);
118-
119-
Assert.IsTrue(runner.IsAvailable,"AdbRunner should find adb in SDK");
120-
Assert.IsNotNull(runner.AdbPath);
121-
Assert.IsTrue(File.Exists(runner.AdbPath),$"adb binary should exist at {runner.AdbPath}");
124+
varrunner=newAdbRunner(adbPath);
125+
Assert.IsNotNull(runner);
122126
}
123127

124128
[Test]
125129
publicasyncTaskAdbRunner_ListDevicesAsync_ReturnsWithoutError()
126130
{
127-
varrunner=newAdbRunner(()=>sdkPath);
131+
varrunner=newAdbRunner(adbPath);
128132

129133
// On CI there are no physical devices or emulators, but the command
130134
// should succeed and return an empty (or non-null) list.
@@ -137,9 +141,7 @@ public async Task AdbRunner_ListDevicesAsync_ReturnsWithoutError ()
137141
[Test]
138142
publicvoidAdbRunner_WaitForDeviceAsync_TimesOut_WhenNoDevice()
139143
{
140-
varrunner=newAdbRunner(()=>sdkPath);
141-
142-
// With no devices connected, wait-for-device should time out
144+
varrunner=newAdbRunner(adbPath);
143145
varex=Assert.ThrowsAsync<TimeoutException>(async()=>
144146
awaitrunner.WaitForDeviceAsync(timeout:TimeSpan.FromSeconds(5)));
145147

@@ -152,11 +154,10 @@ public void AdbRunner_WaitForDeviceAsync_TimesOut_WhenNoDevice ()
152154
[Test]
153155
publicvoidAllRunners_ToolDiscovery_ConsistentWithSdk()
154156
{
155-
varadb=newAdbRunner(()=>sdkPath);
156-
157-
Assert.IsTrue(adb.IsAvailable,"adb should be available");
157+
varrunner=newAdbRunner(adbPath);
158158

159159
// adb path should be under the SDK
160-
StringAssert.StartsWith(sdkPath,adb.AdbPath!);
160+
Assert.IsTrue(File.Exists(adbPath),$"adb should exist at {adbPath}");
161+
StringAssert.StartsWith(sdkPath,adbPath);
161162
}
162163
}

0 commit comments

Comments
 (0)