Skip to content

Commit b2b296f

Browse files
authored
Add android adb command (dotnet#782)
1 parent ed75873 commit b2b296f

File tree

12 files changed

+240
-141
lines changed

12 files changed

+240
-141
lines changed

src/Microsoft.DotNet.XHarness.Android/AdbRunner.cs

Lines changed: 55 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ public class AdbRunner
2727
private readonly string _absoluteAdbExePath;
2828
private readonly ILogger _log;
2929
private readonly IAdbProcessManager _processManager;
30-
private readonly Dictionary<string, string> _commandList = new()
30+
private readonly Dictionary<string, string[]> _commandList = new()
3131
{
32-
{ "architecture", "shell getprop ro.product.cpu.abilist" },
33-
{ "app", "shell pm list packages -3" }
32+
{ "architecture", new[] { "shell", "getprop", "ro.product.cpu.abilist" } },
33+
{ "app", new[] { "shell", "pm", "list", "packages", "-3" } },
3434
};
3535

3636
public int APIVersion => _api ?? GetAPIVersion();
@@ -85,7 +85,7 @@ public void SetActiveDevice(string? deviceSerialNumber)
8585

8686
private int GetAPIVersion()
8787
{
88-
var output = RunAdbCommand("shell getprop ro.build.version.sdk");
88+
var output = RunAdbCommand(new[] { "shell", "getprop", "ro.build.version.sdk" });
8989
return int.Parse(output.StandardOutput);
9090
}
9191

@@ -113,22 +113,22 @@ private static string GetCliAdbExePath()
113113

114114
public TimeSpan TimeToWaitForBootCompletion { get; set; } = TimeSpan.FromMinutes(5);
115115

116-
public string GetAdbVersion() => RunAdbCommand("version").StandardOutput;
116+
public string GetAdbVersion() => RunAdbCommand(new[] { "version" }).StandardOutput;
117117

118-
public string GetAdbState() => RunAdbCommand("get-state").StandardOutput;
118+
public string GetAdbState() => RunAdbCommand(new[] { "get-state" }).StandardOutput;
119119

120-
public string RebootAndroidDevice() => RunAdbCommand("reboot").StandardOutput;
120+
public string RebootAndroidDevice() => RunAdbCommand(new[] { "reboot" }).StandardOutput;
121121

122-
public void ClearAdbLog() => RunAdbCommand("logcat -c");
122+
public void ClearAdbLog() => RunAdbCommand(new[] { "logcat", "-c" });
123123

124-
public void EnableWifi(bool enable) => RunAdbCommand($"shell svc wifi {(enable ? "enable" : "disable")}");
124+
public void EnableWifi(bool enable) => RunAdbCommand(new[] { "shell", "svc", "wifi", enable ? "enable" : "disable" });
125125

126126
public void DumpAdbLog(string outputFilePath, string filterSpec = "")
127127
{
128128
// Workaround: Doesn't seem to have a flush() function and sometimes it doesn't have the full log on emulators.
129129
Thread.Sleep(3000);
130130

131-
var result = RunAdbCommand($"logcat -d {filterSpec}", TimeSpan.FromMinutes(2));
131+
var result = RunAdbCommand(new[] { "logcat", "-d", filterSpec }, TimeSpan.FromMinutes(2));
132132
if (result.ExitCode != 0)
133133
{
134134
// Could throw here, but it would tear down a possibly otherwise acceptable execution.
@@ -155,7 +155,7 @@ public void WaitForDevice()
155155
// (Returns instantly if device is ready)
156156
// This can fail if _currentDevice is unset if there are multiple devices.
157157
_log.LogInformation("Waiting for device to be available (max 5 minutes)");
158-
var result = RunAdbCommand("wait-for-device", TimeSpan.FromMinutes(5));
158+
var result = RunAdbCommand(new[] { "wait-for-device" }, TimeSpan.FromMinutes(5));
159159
_log.LogDebug($"{result.StandardOutput}");
160160
if (result.ExitCode != 0)
161161
{
@@ -167,11 +167,11 @@ public void WaitForDevice()
167167
// to be '1' (as opposed to empty) to make subsequent automation happy.
168168
var began = DateTimeOffset.UtcNow;
169169
var waitingUntil = began.Add(TimeToWaitForBootCompletion);
170-
var bootCompleted = RunAdbCommand($"shell getprop {AdbShellPropertyForBootCompletion}");
170+
var bootCompleted = RunAdbCommand(new[] { "shell", "getprop", AdbShellPropertyForBootCompletion });
171171

172172
while (!bootCompleted.StandardOutput.Trim().StartsWith("1") && DateTimeOffset.UtcNow < waitingUntil)
173173
{
174-
bootCompleted = RunAdbCommand($"shell getprop {AdbShellPropertyForBootCompletion}");
174+
bootCompleted = RunAdbCommand(new[] { "shell", "getprop", AdbShellPropertyForBootCompletion });
175175
_log.LogDebug($"{AdbShellPropertyForBootCompletion} = '{bootCompleted.StandardOutput.Trim()}'");
176176
Thread.Sleep((int)TimeSpan.FromSeconds(10).TotalMilliseconds);
177177
}
@@ -188,7 +188,7 @@ public void WaitForDevice()
188188

189189
public void StartAdbServer()
190190
{
191-
var result = RunAdbCommand("start-server");
191+
var result = RunAdbCommand(new[] { "start-server" });
192192
_log.LogDebug($"{result.StandardOutput}");
193193
if (result.ExitCode != 0)
194194
{
@@ -198,7 +198,7 @@ public void StartAdbServer()
198198

199199
public void KillAdbServer()
200200
{
201-
var result = RunAdbCommand("kill-server");
201+
var result = RunAdbCommand(new[] { "kill-server" });
202202
if (result.ExitCode != 0)
203203
{
204204
throw new Exception($"Error killing ADB Server. Std out:{result.StandardOutput} Std. Err: {result.StandardError}");
@@ -217,7 +217,7 @@ public int InstallApk(string apkPath)
217217
throw new FileNotFoundException($"Could not find {apkPath}", apkPath);
218218
}
219219

220-
var result = RunAdbCommand($"install \"{apkPath}\"");
220+
var result = RunAdbCommand(new[] { "install", apkPath });
221221

222222
// Two possible retry scenarios, theoretically both can happen on the same run:
223223

@@ -227,7 +227,7 @@ public int InstallApk(string apkPath)
227227
_log.LogWarning($"Hit broken pipe error; Will make one attempt to restart ADB server, then retry the install");
228228
KillAdbServer();
229229
StartAdbServer();
230-
result = RunAdbCommand($"install \"{apkPath}\"");
230+
result = RunAdbCommand(new[] { "install", apkPath });
231231
}
232232

233233
// 2. Installation cache on device is messed up; restarting the device reliably seems to unblock this (unless the device is actually full, if so this will error the same)
@@ -236,7 +236,7 @@ public int InstallApk(string apkPath)
236236
_log.LogWarning($"It seems the package installation cache may be full on the device. We'll try to reboot it before trying one more time.{Environment.NewLine}Output:{result}");
237237
RebootAndroidDevice();
238238
WaitForDevice();
239-
result = RunAdbCommand($"install \"{apkPath}\"");
239+
result = RunAdbCommand(new[] { "install", apkPath });
240240
}
241241

242242
// 3. Installation timed out or failed with exception; restarting the ADB server, reboot the device and give more time for installation
@@ -248,7 +248,7 @@ public int InstallApk(string apkPath)
248248
StartAdbServer();
249249
RebootAndroidDevice();
250250
WaitForDevice();
251-
result = RunAdbCommand($"install \"{apkPath}\"", TimeSpan.FromMinutes(10));
251+
result = RunAdbCommand(new[] { "install", apkPath }, TimeSpan.FromMinutes(10));
252252
}
253253

254254
if (result.ExitCode != 0)
@@ -271,7 +271,7 @@ public int UninstallApk(string apkName)
271271
}
272272

273273
_log.LogInformation($"Attempting to remove apk '{apkName}': ");
274-
var result = RunAdbCommand($"uninstall {apkName}");
274+
var result = RunAdbCommand(new[] { "uninstall", apkName });
275275

276276
// See note above in install()
277277
if (result.ExitCode == (int)AdbExitCodes.ADB_BROKEN_PIPE)
@@ -280,7 +280,7 @@ public int UninstallApk(string apkName)
280280

281281
KillAdbServer();
282282
StartAdbServer();
283-
result = RunAdbCommand($"uninstall {apkName}");
283+
result = RunAdbCommand(new[] { "uninstall", apkName });
284284
}
285285

286286
if (result.ExitCode == (int)AdbExitCodes.SUCCESS)
@@ -303,7 +303,7 @@ public int UninstallApk(string apkName)
303303
public int KillApk(string apkName)
304304
{
305305
_log.LogInformation($"Killing all running processes for '{apkName}': ");
306-
var result = RunAdbCommand($"shell am kill --user all {apkName}");
306+
var result = RunAdbCommand(new[] { "shell", "am", "kill", "--user", "all", apkName});
307307
if (result.ExitCode != (int)AdbExitCodes.SUCCESS)
308308
{
309309
_log.LogError($"Error:{Environment.NewLine}{result}");
@@ -331,7 +331,7 @@ public List<string> PullFiles(string devicePath, string localPath)
331331
_log.LogInformation($"Attempting to pull contents of {devicePath} to {localPath}");
332332
var copiedFiles = new List<string>();
333333

334-
var result = RunAdbCommand($"pull {devicePath} {tempFolder}");
334+
var result = RunAdbCommand(new[] { "pull", devicePath, tempFolder });
335335

336336
if (result.ExitCode != (int)AdbExitCodes.SUCCESS)
337337
{
@@ -372,9 +372,20 @@ public List<string> PullFiles(string devicePath, string localPath)
372372
{
373373
var devicesAndProperties = new Dictionary<string, string?>();
374374

375-
string command = _commandList[property];
375+
IEnumerable<string> GetAdbArguments(string deviceSerial)
376+
{
377+
var args = new List<string>
378+
{
379+
"-s",
380+
deviceSerial,
381+
};
382+
383+
args.AddRange(_commandList[property]);
376384

377-
var result = RunAdbCommand("devices -l", TimeSpan.FromSeconds(30));
385+
return args;
386+
}
387+
388+
var result = RunAdbCommand(new[] { "devices", "-l" }, TimeSpan.FromSeconds(30));
378389
string[] standardOutputLines = result.StandardOutput.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
379390

380391
// Retry up to 3 mins til we get output; if the ADB server isn't started the output will come from a child process and we'll miss it.
@@ -386,7 +397,7 @@ public List<string> PullFiles(string devicePath, string localPath)
386397
{
387398
_log.LogDebug($"Unexpected response from adb devices -l:{Environment.NewLine}Exit code={result.ExitCode}{Environment.NewLine}Std. Output: {result.StandardOutput} {Environment.NewLine}Std. Error: {result.StandardError}");
388399
Thread.Sleep(10000);
389-
result = RunAdbCommand("devices -l", TimeSpan.FromSeconds(30));
400+
result = RunAdbCommand(new[] { "devices", "-l" }, TimeSpan.FromSeconds(30));
390401
standardOutputLines = result.StandardOutput.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
391402
}
392403

@@ -402,7 +413,7 @@ public List<string> PullFiles(string devicePath, string localPath)
402413
{
403414
var deviceSerial = lineParts[0];
404415

405-
var shellResult = RunAdbCommand($"-s {deviceSerial} {command}", TimeSpan.FromSeconds(30));
416+
var shellResult = RunAdbCommand(GetAdbArguments(deviceSerial), TimeSpan.FromSeconds(30));
406417

407418
// Assumption: All Devices on a machine running Xharness should attempt to be online or disconnected.
408419
retriesLeft = 30; // Max 5 minutes (30 attempts * 10 second waits)
@@ -411,7 +422,7 @@ public List<string> PullFiles(string devicePath, string localPath)
411422
_log.LogWarning($"Device '{deviceSerial}' is offline; retrying up to one minute.");
412423
Thread.Sleep(10000);
413424

414-
shellResult = RunAdbCommand($"-s {deviceSerial} {command}", TimeSpan.FromSeconds(30));
425+
shellResult = RunAdbCommand(GetAdbArguments(deviceSerial), TimeSpan.FromSeconds(30));
415426
}
416427

417428
if (shellResult.ExitCode == (int)AdbExitCodes.SUCCESS)
@@ -502,30 +513,28 @@ public Dictionary<string, string> GetAllDevicesToUse(ILogger logger, IEnumerable
502513
public ProcessExecutionResults RunApkInstrumentation(string apkName, string? instrumentationClassName, Dictionary<string, string> args, TimeSpan timeout)
503514
{
504515
string displayName = string.IsNullOrEmpty(instrumentationClassName) ? "{default}" : instrumentationClassName;
505-
string appArguments = "";
506-
if (args.Count > 0)
516+
517+
var adbArgs = new List<string>
507518
{
508-
foreach (string key in args.Keys)
509-
{
510-
appArguments = $"{appArguments} -e {key} {args[key]}";
511-
}
512-
}
519+
"shell", "am", "instrument"
520+
};
521+
522+
adbArgs.AddRange(args.SelectMany(arg => new[] { "-e", arg.Key, arg.Value }));
523+
adbArgs.Add("-w");
513524

514-
string command = $"shell am instrument {appArguments} -w {apkName}";
515525
if (string.IsNullOrEmpty(instrumentationClassName))
516526
{
517527
_log.LogInformation($"Starting default instrumentation class on {apkName} (exit code 0 == success)");
528+
adbArgs.Add(apkName);
518529
}
519530
else
520531
{
521532
_log.LogInformation($"Starting instrumentation class '{instrumentationClassName}' on {apkName}");
522-
command = $"{command}/{instrumentationClassName}";
533+
adbArgs.Add($"{apkName}/{instrumentationClassName}");
523534
}
524-
_log.LogDebug($"Raw command: '{command}'");
525535

526-
var stopWatch = new Stopwatch();
527-
stopWatch.Start();
528-
var result = RunAdbCommand(command, timeout);
536+
var stopWatch = Stopwatch.StartNew();
537+
var result = RunAdbCommand(adbArgs, timeout);
529538
stopWatch.Stop();
530539

531540
if (result.ExitCode == (int)AdbExitCodes.INSTRUMENTATION_TIMEOUT)
@@ -536,24 +545,26 @@ public ProcessExecutionResults RunApkInstrumentation(string apkName, string? ins
536545
{
537546
_log.LogInformation($"Running instrumentation class {displayName} took {stopWatch.Elapsed.TotalSeconds} seconds");
538547
}
548+
539549
_log.LogDebug(result.ToString());
550+
540551
return result;
541552
}
542553

543554
#endregion
544555

545556
#region Process runner helpers
546557

547-
public ProcessExecutionResults RunAdbCommand(string command) => RunAdbCommand(command, TimeSpan.FromMinutes(5));
558+
public ProcessExecutionResults RunAdbCommand(IEnumerable<string> arguments) => RunAdbCommand(arguments, TimeSpan.FromMinutes(5));
548559

549-
public ProcessExecutionResults RunAdbCommand(string command, TimeSpan timeOut)
560+
public ProcessExecutionResults RunAdbCommand(IEnumerable<string> arguments, TimeSpan timeOut)
550561
{
551562
if (!File.Exists(_absoluteAdbExePath))
552563
{
553564
throw new FileNotFoundException($"Provided path for adb.exe was not valid ('{_absoluteAdbExePath}')", _absoluteAdbExePath);
554565
}
555566

556-
return _processManager.Run(_absoluteAdbExePath, command, timeOut);
567+
return _processManager.Run(_absoluteAdbExePath, arguments, timeOut);
557568
}
558569

559570
#endregion

src/Microsoft.DotNet.XHarness.Android/Execution/AdbProcessManager.cs

Lines changed: 27 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
using System;
2+
using System.Collections.Generic;
23
using System.Diagnostics;
34
using System.IO;
5+
using System.Linq;
46
using System.Text;
5-
using System.Threading;
7+
using Microsoft.DotNet.XHarness.Common.Utilities;
68
using Microsoft.Extensions.Logging;
79

810
namespace Microsoft.DotNet.XHarness.Android.Execution;
@@ -18,25 +20,32 @@ public class AdbProcessManager : IAdbProcessManager
1820
/// </summary>
1921
public string DeviceSerial { get; set; } = string.Empty;
2022

21-
public ProcessExecutionResults Run(string adbExePath, string arguments) => Run(adbExePath, arguments, TimeSpan.FromMinutes(5));
22-
23-
public ProcessExecutionResults Run(string adbExePath, string arguments, TimeSpan timeOut)
23+
public ProcessExecutionResults Run(string adbExePath, IEnumerable<string> arguments, TimeSpan timeOut)
2424
{
25-
string deviceSerialArgs = string.IsNullOrEmpty(DeviceSerial) ? string.Empty : $"-s {DeviceSerial}";
26-
27-
_log.LogDebug($"Executing command: '{adbExePath} {deviceSerialArgs} {arguments}'");
25+
if (!string.IsNullOrEmpty(DeviceSerial))
26+
{
27+
arguments = arguments.Prepend(DeviceSerial).Prepend("-s");
28+
}
2829

29-
var processStartInfo = new ProcessStartInfo
30+
var processStartInfo = new ProcessStartInfo()
3031
{
3132
CreateNoWindow = true,
3233
UseShellExecute = false,
3334
WorkingDirectory = Path.GetDirectoryName(adbExePath) ?? throw new ArgumentNullException(nameof(adbExePath)),
3435
RedirectStandardOutput = true,
3536
RedirectStandardError = true,
3637
FileName = adbExePath,
37-
Arguments = $"{deviceSerialArgs} {arguments}",
3838
};
39+
40+
foreach (var arg in arguments)
41+
{
42+
processStartInfo.ArgumentList.Add(arg);
43+
}
44+
45+
_log.LogDebug($"Executing command: '{adbExePath} {StringUtils.FormatArguments(processStartInfo.ArgumentList)}'");
46+
3947
var p = new Process() { StartInfo = processStartInfo };
48+
4049
var standardOut = new StringBuilder();
4150
var standardErr = new StringBuilder();
4251

@@ -63,7 +72,6 @@ public ProcessExecutionResults Run(string adbExePath, string arguments, TimeSpan
6372
};
6473

6574
p.Start();
66-
6775
p.BeginOutputReadLine();
6876
p.BeginErrorReadLine();
6977

@@ -90,15 +98,15 @@ public ProcessExecutionResults Run(string adbExePath, string arguments, TimeSpan
9098
p.Close();
9199

92100
lock (standardOut)
93-
lock (standardErr)
101+
lock (standardErr)
102+
{
103+
return new ProcessExecutionResults()
94104
{
95-
return new ProcessExecutionResults()
96-
{
97-
ExitCode = exitCode,
98-
StandardOutput = standardOut.ToString(),
99-
StandardError = standardErr.ToString(),
100-
TimedOut = timedOut
101-
};
102-
}
105+
ExitCode = exitCode,
106+
StandardOutput = standardOut.ToString(),
107+
StandardError = standardErr.ToString(),
108+
TimedOut = timedOut
109+
};
110+
}
103111
}
104112
}

src/Microsoft.DotNet.XHarness.Android/Execution/Api23AndOlderReportManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public string DumpBugReport(AdbRunner runner, string outputFilePathWithoutFormat
2323
// give some time for bug report to be available
2424
Thread.Sleep(3000);
2525

26-
var result = runner.RunAdbCommand($"bugreport", TimeSpan.FromMinutes(5));
26+
var result = runner.RunAdbCommand(new[] { "bugreport" }, TimeSpan.FromMinutes(5));
2727

2828
if (result.ExitCode != 0)
2929
{

0 commit comments

Comments
 (0)