blob: 5632901020e85e6a86050c197afa82a4d898cdd7 [file]
// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import "dart:async";
import "dart:collection";
import "dart:convert" show LineSplitter, utf8;
import "dart:core";
import "dart:io";
import "dart:math" as math;
import "path.dart";
import "utils.dart";
class AdbCommandResult {
final String command;
final String stdout;
final String stderr;
final int exitCode;
final bool timedOut;
AdbCommandResult(
this.command,
this.stdout,
this.stderr,
this.exitCode,
this.timedOut,
);
void throwIfFailed() {
if (exitCode != 0) {
var error =
"Running: $command failed:"
"stdout:\n ${stdout.trim()}\n"
"stderr:\n ${stderr.trim()}\n"
"exitCode: $exitCode\n"
"timedOut: $timedOut";
throw Exception(error);
}
}
}
/// [_executeCommand] will write [stdin] to the standard input of the created
/// process and will return a tuple (stdout, stderr).
///
/// If the exit code of the process was nonzero it will complete with an error.
/// If starting the process failed, it will complete with an error as well.
Future<AdbCommandResult> _executeCommand(
String executable,
List<String> args, {
String? stdin,
Duration? timeout,
}) {
Future<String> getOutput(Stream<List<int>> stream) {
return stream
.transform(utf8.decoder)
.toList()
.then((data) => data.join(""));
}
return Process.start(executable, args).then((Process process) async {
if (stdin != null && stdin != '') {
process.stdin.write(stdin);
await process.stdin.flush();
}
process.stdin.close();
Timer? timer;
var timedOut = false;
if (timeout != null) {
timer = Timer(timeout, () {
timedOut = true;
process.kill(ProcessSignal.sigterm);
timer = null;
});
}
var results = await Future.wait([
getOutput(process.stdout),
getOutput(process.stderr),
process.exitCode,
]);
timer?.cancel();
var command = "$executable ${args.join(' ')}";
return AdbCommandResult(
command,
results[0] as String,
results[1] as String,
results[2] as int,
timedOut,
);
});
}
/// Helper class to loop through all adb ports.
///
/// The ports come in pairs:
/// - even number: console connection
/// - odd number: adb connection
/// Note that this code doesn't check if the ports are used.
class AdbServerPortPool {
static const _minPort = 5554;
static const _maxPort = 5584;
static int _nextPort = _minPort;
static int next() {
var port = _nextPort;
if (port > _maxPort) throw Exception("All ports are used.");
_nextPort += 2;
return port;
}
}
/// Used for communicating with an emulator or with a real device.
class AdbDevice {
static const _adbServerStartupTime = Duration(seconds: 3);
static int _count = 0;
final String deviceId;
final String deviceDir;
final Map<String, String> _cachedData = {};
factory AdbDevice(String deviceId) => AdbDevice.withSlice(deviceId, _count++);
AdbDevice.withSlice(this.deviceId, int slice)
: deviceDir = "/data/local/tmp/testing$slice";
/// Blocks execution until the device is online.
Future waitForDevice() {
return _adbCommand(['wait-for-device']);
}
/// Polls the 'sys.boot_completed' property. Returns as soon as the property
/// is 1.
Future<void> waitForBootCompleted() async {
while (true) {
try {
var result = await _adbCommand([
'shell',
'getprop',
'sys.boot_completed',
]);
if (result.stdout.trim() == '1') return;
} catch (_) {}
await Future<void>.delayed(const Duration(seconds: 2));
}
}
/// Put adb in root mode.
Future<bool> adbRoot() {
var adbRootCompleter = Completer<bool>();
_adbCommand(['root'])
.then((_) {
// TODO: Figure out a way to wait until the adb daemon was restarted in
// 'root mode' on the device.
Timer(_adbServerStartupTime, () => adbRootCompleter.complete(true));
})
.catchError((Object error) {
adbRootCompleter.completeError(error);
});
return adbRootCompleter.future;
}
/// Download data from the device.
Future pullData(Path remote, Path local) {
return _adbCommand(['pull', '$remote', '$local']);
}
/// Upload data to the device.
Future pushData(Path local, Path remote) {
return _adbCommand(['push', '$local', '$remote']);
}
/// Upload data to the device, unless [local] is the same as the most recently
/// used source for [remote].
Future<AdbCommandResult> pushCachedData(String local, String remote) {
if (_cachedData[remote] == local) {
return Future.value(
AdbCommandResult("Skipped cached push", "", "", 0, false),
);
}
_cachedData[remote] = local;
return _adbCommand(['push', local, remote]);
}
/// Change permission of directory recursively.
Future chmod(String mode, Path directory) {
var arguments = ['shell', 'chmod', '-R', mode, '$directory'];
return _adbCommand(arguments);
}
/// Install an application on the device.
Future installApk(Path filename) {
return _adbCommand([
'install',
'-i',
'com.google.android.feedback',
'-r',
'$filename',
]);
}
/// Start the given intent on the device.
Future startActivity(Intent intent) {
return _adbCommand([
'shell',
'am',
'start',
'-W',
'-a',
intent.action,
'-n',
"${intent.package}/${intent.activity}",
if (intent.dataUri != null) ...['-d', intent.dataUri!],
]);
}
/// Force to stop everything associated with [package].
Future forceStop(String package) {
return _adbCommand(['shell', 'am', 'force-stop', package]);
}
/// Set system property name to value.
Future setProp(String name, String value) {
return _adbCommand(['shell', 'setprop', name, value]);
}
/// Kill all background processes.
Future killAll() {
return _adbCommand(['shell', 'am', 'kill-all']);
}
Future<AdbCommandResult> runAdbCommand(
List<String> adbArgs, {
Duration? timeout,
}) {
return _executeCommand(
"adb",
_deviceSpecificArgs(adbArgs),
timeout: timeout,
);
}
Future<AdbCommandResult> runAdbShellCommand(
List<String> shellArgs, {
Duration? timeout,
}) async {
const marker = 'AdbShellExitCode: ';
// The exitcode of 'adb shell ...' can be 0 even though the command failed
// with a non-zero exit code. We therefore explicitly print it to stdout and
// search for it.
var args = ['shell', "${shellArgs.join(' ')} ; echo $marker \$?"];
var result = await _executeCommand(
"adb",
_deviceSpecificArgs(args),
timeout: timeout,
);
var exitCode = result.exitCode;
var lines = result.stdout
.split('\n')
.where((line) => line.trim().isNotEmpty)
.toList();
if (lines.isNotEmpty) {
var index = lines.last.indexOf(marker);
if (index >= 0) {
exitCode = int.parse(
lines.last.substring(index + marker.length).trim(),
);
if (exitCode > 128 && exitCode <= 128 + 31) {
// Return negative exit codes for signals 1..31 (128+N for signal N).
exitCode = 128 - exitCode;
}
} else {
// In case of timeouts, for example, we won't get the exitcode marker.
// TODO(mkroghj): Some times tests fail with the assert below. To better
// investigate, write out debug info.
DebugLogger.info("======= THIS IS DEBUG INFORMATION =======");
DebugLogger.info("arguments: $args");
DebugLogger.info("exitCode: ${result.exitCode}");
DebugLogger.info("timedOut: ${result.timedOut}");
DebugLogger.info("---- std out ----");
DebugLogger.info(result.stdout);
DebugLogger.info("---- std out end ----");
DebugLogger.info("---- std error ----");
DebugLogger.info(result.stderr);
DebugLogger.info("---- std error end ----");
DebugLogger.info("======= THIS IS NO LONGER DEBUG INFORMATION =======");
assert(result.exitCode != 0);
}
}
return AdbCommandResult(
result.command,
result.stdout,
result.stderr,
exitCode,
result.timedOut,
);
}
Future<AdbCommandResult> _adbCommand(List<String> adbArgs) async {
var result = await _executeCommand("adb", _deviceSpecificArgs(adbArgs));
result.throwIfFailed();
return result;
}
List<String> _deviceSpecificArgs(List<String> adbArgs) {
return ['-s', deviceId, ...adbArgs];
}
}
/// Helper to list all adb devices available.
class AdbHelper {
static final RegExp _deviceLineRegexp = RegExp(
r'^([a-zA-Z0-9:_.\-]+)[ \t]+device$',
multiLine: true,
);
static final RegExp _fastbootDeviceLineRegexp = RegExp(
r'^([a-zA-Z0-9:_.\-]+)[ \t]+fastboot$',
multiLine: true,
);
static Future<List<String>> listDevices() {
return Process.run('adb', ['devices']).then((ProcessResult result) {
if (result.exitCode != 0) {
throw Exception(
"Could not list devices [stdout: ${result.stdout},"
"stderr: ${result.stderr}]",
);
}
return _deviceLineRegexp
.allMatches(result.stdout as String)
.map((Match m) => m[1]!)
.toList();
});
}
static Future<List<String>> listFastbootDevices() async {
final result = await Process.run('fastboot', ['devices']);
if (result.exitCode != 0) {
throw Exception(
"Could not list fastboot devices [stdout: ${result.stdout},"
"stderr: ${result.stderr}]",
);
}
return _fastbootDeviceLineRegexp
.allMatches(result.stdout as String)
.map((Match m) => m[1]!)
.toList();
}
static Future<void> rebootFastbootDevices() async {
final result = await Process.run('fastboot', ['reboot']);
if (result.exitCode != 0) {
throw Exception(
"Could not list fastboot devices [stdout: ${result.stdout},"
"stderr: ${result.stderr}]",
);
}
}
}
/// Represents an android intent.
class Intent {
String action;
String package;
String activity;
String? dataUri;
Intent(this.action, this.package, this.activity, [this.dataUri]);
}
class AndroidEmulators {
static final _processes = <Process>[];
static void _forward(Process process, String label) {
const LineSplitter()
.bind(utf8.decoder.bind(process.stdout))
.listen((d) => print("[$label] $d"));
const LineSplitter()
.bind(utf8.decoder.bind(process.stderr))
.listen((d) => print("[$label] $d"));
}
static final _environment = {
"ANDROID_HOME": Uri.base
.resolve("third_party/android_tools/sdk")
.toFilePath(),
"JAVA_HOME": Uri.base.resolve("third_party/openjdk").toFilePath(),
};
static Future start() {
return _start;
}
static final _start = _startOnce();
static Future _startOnce() async {
const sdkmanager =
"./third_party/android_tools/sdk/cmdline-tools/latest/bin/sdkmanager";
var p = await Process.start(
sdkmanager,
["--install", "emulator"],
environment: _environment,
mode: ProcessStartMode.inheritStdio,
);
var e = await p.exitCode;
if (e != 0) {
throw "install emulator failed: $e";
}
p = await Process.start(
sdkmanager,
["--install", "system-images;android-30;default;x86_64"],
environment: _environment,
mode: ProcessStartMode.inheritStdio,
);
e = await p.exitCode;
if (e != 0) {
throw "install image failed: $e";
}
final numEmulators = 1;
// Maybe Platform.numberOfProcessors ~/ 6, but unexplained crashes.
final starts = <Future>[];
for (var i = 0; i < numEmulators; i++) {
final port = "${5555 + i * 2}"; // adb's prefered port range.
starts.add(startEmulator(port));
}
return Future.wait(starts);
}
static Future startEmulator(String port) async {
const avdmanager =
"./third_party/android_tools/sdk/cmdline-tools/latest/bin/avdmanager";
const emulator = "./third_party/android_tools/sdk/emulator/emulator";
const adb = "./third_party/android_tools/sdk/platform-tools/adb";
var p = await Process.start(avdmanager, [
"create",
"avd",
"--force",
"--name",
"test-$port",
"--package",
"system-images;android-30;default;x86_64",
], environment: _environment);
_forward(p, "avdmanager create");
p.stdin.writeln("no"); // Create custom hardware profile?
p.stdin.close();
var e = await p.exitCode;
if (e != 0) {
throw "create avd failed: $e";
}
final deviceId = "emulator-$port";
final stopwatch = Stopwatch();
stopwatch.start();
p = await Process.start(
emulator,
[
"-port",
port,
"-avd",
"test-$port",
"-read-only",
"-accel",
"on", // Get an error if kvm isn't available.
"-no-window",
"-no-snapshot",
"-no-audio",
"-no-boot-anim",
"-no-metrics",
"-cores",
"6", // Seems ignored. nproc reports 6 regardless of value here.
"-memory",
"8192", // Limited to 1.5-8G.
],
environment: _environment,
mode: ProcessStartMode.inheritStdio,
);
_processes.add(p);
for (var i = 0; i < 10; i++) {
await Future.delayed(const Duration(seconds: 20));
var p = await Process.run(adb, [
"-s",
deviceId,
"shell",
"getprop",
"sys.boot_completed",
]);
if (p.stdout.trim() == "1") {
print("$deviceId ready after ${stopwatch.elapsed}");
return "okay";
}
print("still waiting for $deviceId to boot after ${stopwatch.elapsed}");
}
throw "timed out waiting for $deviceId to boot after ${stopwatch.elapsed}";
}
static Future stop() async {
while (_processes.isNotEmpty) {
_processes.removeLast().kill();
}
}
}
/// Discovers all available devices and supports acquire/release.
class AdbDevicePool {
final Queue<AdbDevice> _idleDevices = Queue();
final Queue<Completer<AdbDevice>> _waiter = Queue();
AdbDevicePool(List<AdbDevice> idleDevices) {
_idleDevices.addAll(idleDevices);
}
static Future<AdbDevicePool> create() async {
var names = await AdbHelper.listDevices();
var devices = names.map(AdbDevice.new).toList();
if (devices.isEmpty) {
var fastbootDevices = await AdbHelper.listFastbootDevices();
if (fastbootDevices.isNotEmpty) {
print('Connected device $fastbootDevices found in fastboot mode...');
AdbHelper.rebootFastbootDevices();
final sw = Stopwatch()..start();
const waitForDeviceToRebootInSeconds = 60;
while (sw.elapsed.inSeconds < waitForDeviceToRebootInSeconds) {
print('Waiting for device to comeback after fastboot reboot...\n');
names = await AdbHelper.listDevices();
if (names.isNotEmpty) {
devices = names.map(AdbDevice.new).toList();
break;
}
await Future.delayed(const Duration(seconds: 5));
}
}
if (devices.isEmpty) {
throw Exception(
'No android devices found. '
'Please make sure "adb devices" shows your device!',
);
}
}
print("Found ${devices.length} Android devices.");
var splitDevices = await _splitDevices(devices);
print("Using as ${splitDevices.length} Android devices.");
return AdbDevicePool(splitDevices);
}
static Future<List<AdbDevice>> _splitDevices(List<AdbDevice> devices) async {
var splitDevices = <AdbDevice>[];
for (var d in devices) {
var result = await Process.run("adb", [
"-s",
d.deviceId,
"shell",
"df",
"/data/local/tmp",
]);
int disk;
try {
// Sample input that is being parsed below:
//
// Filesystem 1K-blocks Used Available Use% Mounted on
// /dev/block/dm-64 114786388 110709944 3945372 97% /data/user/0
disk =
int.parse(
result.stdout
.split('\n')[1]
.split(' ')
.where((String v) => v.isNotEmpty)
.toList()[3]
.trim()
as String,
) *
512;
} catch (_) {
print(result.stdout);
rethrow;
}
result = await Process.run("adb", [
"-s",
d.deviceId,
"shell",
"cat",
"/proc/meminfo",
]);
int mem;
try {
mem =
int.parse(
RegExp(
r"^MemTotal:\s*(\d+)\s*kB",
multiLine: true,
).firstMatch(result.stdout as String)![1]
as String,
) *
1024;
} catch (_) {
print(result.stdout);
rethrow;
}
result = await Process.run("adb", ["-s", d.deviceId, "shell", "nproc"]);
int harts;
try {
harts = int.parse(result.stdout.trim() as String);
} catch (e) {
print(e);
print(result.stdout);
print(result.stderr);
harts = 1;
}
print("${d.deviceId} harts=$harts mem=$mem free_disk=$disk");
var splitCount = harts;
const kb = 1024;
const mb = 1024 * kb;
const gb = 1024 * mb;
splitCount = math.min(splitCount, mem ~/ gb);
splitCount = math.min(splitCount, disk ~/ (256 * mb));
splitCount = math.max(splitCount, 1);
for (var i = 0; i < splitCount; i++) {
splitDevices.add(AdbDevice.withSlice(d.deviceId, i));
}
}
return splitDevices;
}
Future<AdbDevice> acquireDevice() async {
if (_idleDevices.isNotEmpty) {
return _idleDevices.removeFirst();
} else {
var completer = Completer<AdbDevice>();
_waiter.add(completer);
return completer.future;
}
}
void releaseDevice(AdbDevice device) {
if (_waiter.isNotEmpty) {
var completer = _waiter.removeFirst();
completer.complete(device);
} else {
_idleDevices.add(device);
}
}
}