9d9f467195
This reverts commit 8044e32fc5.
Reason for revert: The device was fixed, and there is now one device per bot. Blacklisting a single device is no longer the right approach because it effectively destroys a bot.
Original change's description:
> [infra] Blacklist android device id in attempt to make buildbot builder more stable
>
> This particular device sometimes fails with errors such as:
>
> * "protocol error"
> * "protocol error (no status)"
> * "device not found"
>
> Change-Id: Iced97ae8f5e891c43436a3d745004517dc59b42c
> Reviewed-on: https://dart-review.googlesource.com/c/84905
> Reviewed-by: Alexander Thomas <athom@google.com>
> Reviewed-by: Vyacheslav Egorov <vegorov@google.com>
> Commit-Queue: Martin Kustermann <kustermann@google.com>
TBR=whesse@google.com,vegorov@google.com,kustermann@google.com,athom@google.com
# Not skipping CQ checks because original CL landed > 1 day ago.
Change-Id: I8422a75d10518ec06de737a0a719f9648279fbf8
Reviewed-on: https://dart-review.googlesource.com/c/89500
Reviewed-by: Alexander Thomas <athom@google.com>
Commit-Queue: Alexander Thomas <athom@google.com>
466 lines
13 KiB
Dart
466 lines
13 KiB
Dart
// 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.
|
|
|
|
library android;
|
|
|
|
import "dart:async";
|
|
import "dart:convert" show LineSplitter, utf8;
|
|
import "dart:core";
|
|
import "dart:collection";
|
|
import "dart:io";
|
|
|
|
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 new 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;
|
|
bool timedOut = false;
|
|
if (timeout != null) {
|
|
timer = new Timer(timeout, () {
|
|
timedOut = true;
|
|
process.kill(ProcessSignal.sigterm);
|
|
timer = null;
|
|
});
|
|
}
|
|
|
|
var results = await Future.wait([
|
|
getOutput(process.stdout),
|
|
getOutput(process.stderr),
|
|
process.exitCode
|
|
]);
|
|
if (timer != null) timer.cancel();
|
|
|
|
String command = "$executable ${args.join(' ')}";
|
|
return new 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 int MIN_PORT = 5554;
|
|
static int MAX_PORT = 5584;
|
|
|
|
static int _nextPort = MIN_PORT;
|
|
|
|
static int next() {
|
|
var port = _nextPort;
|
|
if (port > MAX_PORT) {
|
|
throw new Exception("All ports are used.");
|
|
}
|
|
_nextPort += 2;
|
|
return port;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents the interface to the emulator.
|
|
* New emulators can be launched by calling the static [launchNewEmulator]
|
|
* method.
|
|
*/
|
|
class AndroidEmulator {
|
|
int _port;
|
|
Process _emulatorProcess;
|
|
AdbDevice _adbDevice;
|
|
|
|
int get port => _port;
|
|
|
|
AdbDevice get adbDevice => _adbDevice;
|
|
|
|
static Future<AndroidEmulator> launchNewEmulator(String avdName) {
|
|
var portNumber = AdbServerPortPool.next();
|
|
var args = ['-avd', '$avdName', '-port', "$portNumber" /*, '-gpu', 'on'*/];
|
|
return Process.start("emulator64-arm", args).then((Process process) {
|
|
var adbDevice = new AdbDevice('emulator-$portNumber');
|
|
return new AndroidEmulator._private(portNumber, adbDevice, process);
|
|
});
|
|
}
|
|
|
|
AndroidEmulator._private(this._port, this._adbDevice, this._emulatorProcess) {
|
|
Stream<String> getLines(Stream s) {
|
|
return s.transform(utf8.decoder).transform(new LineSplitter());
|
|
}
|
|
|
|
getLines(_emulatorProcess.stdout).listen((line) {
|
|
log("stdout: ${line.trim()}");
|
|
});
|
|
getLines(_emulatorProcess.stderr).listen((line) {
|
|
log("stderr: ${line.trim()}");
|
|
});
|
|
_emulatorProcess.exitCode.then((exitCode) {
|
|
log("emulator exited with exitCode: $exitCode.");
|
|
});
|
|
}
|
|
|
|
Future<bool> kill() {
|
|
var completer = new Completer<bool>();
|
|
if (_emulatorProcess.kill()) {
|
|
_emulatorProcess.exitCode.then((exitCode) {
|
|
// TODO: Should we use exitCode to do something clever?
|
|
completer.complete(true);
|
|
});
|
|
} else {
|
|
log("Sending kill signal to emulator process failed");
|
|
completer.complete(false);
|
|
}
|
|
return completer.future;
|
|
}
|
|
|
|
void log(String msg) {
|
|
DebugLogger.info("AndroidEmulator(${_adbDevice.deviceId}): $msg");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper class to create avd device configurations.
|
|
*/
|
|
class AndroidHelper {
|
|
static Future createAvd(String name, String target) async {
|
|
var args = [
|
|
'--silent',
|
|
'create',
|
|
'avd',
|
|
'--name',
|
|
'$name',
|
|
'--target',
|
|
'$target',
|
|
'--force',
|
|
'--abi',
|
|
'armeabi-v7a'
|
|
];
|
|
// We're adding newlines to stdin to simulate <enter>.
|
|
var result = await _executeCommand("android", args, stdin: "\n\n\n\n");
|
|
result.throwIfFailed();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Used for communicating with an emulator or with a real device.
|
|
*/
|
|
class AdbDevice {
|
|
static const _adbServerStartupTime = const Duration(seconds: 3);
|
|
String _deviceId;
|
|
Map<String, String> _cachedData = new Map<String, String>();
|
|
|
|
String get deviceId => _deviceId;
|
|
|
|
AdbDevice(this._deviceId);
|
|
|
|
/**
|
|
* 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<Null> waitForBootCompleted() async {
|
|
while (true) {
|
|
try {
|
|
AdbCommandResult result =
|
|
await _adbCommand(['shell', 'getprop', 'sys.boot_completed']);
|
|
if (result.stdout.trim() == '1') return;
|
|
} catch (_) {}
|
|
await new Future<Null>.delayed(const Duration(seconds: 2));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Put adb in root mode.
|
|
*/
|
|
Future<bool> adbRoot() {
|
|
var adbRootCompleter = new Completer<bool>();
|
|
_adbCommand(['root']).then((_) {
|
|
// TODO: Figure out a way to wait until the adb daemon was restarted in
|
|
// 'root mode' on the device.
|
|
new Timer(_adbServerStartupTime, () => adbRootCompleter.complete(true));
|
|
}).catchError((error) => adbRootCompleter.completeError(error));
|
|
return adbRootCompleter.future;
|
|
}
|
|
|
|
/**
|
|
* Download data form 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 new Future.value(
|
|
new 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) {
|
|
var arguments = [
|
|
'shell',
|
|
'am',
|
|
'start',
|
|
'-W',
|
|
'-a',
|
|
intent.action,
|
|
'-n',
|
|
"${intent.package}/${intent.activity}"
|
|
];
|
|
if (intent.dataUri != null) {
|
|
arguments.addAll(['-d', intent.dataUri]);
|
|
}
|
|
return _adbCommand(arguments);
|
|
}
|
|
|
|
/**
|
|
* Force to stop everything associated with [package].
|
|
*/
|
|
Future forceStop(String package) {
|
|
var arguments = ['shell', 'am', 'force-stop', package];
|
|
return _adbCommand(arguments);
|
|
}
|
|
|
|
/**
|
|
* Set system property name to value.
|
|
*/
|
|
Future setProp(String name, String value) {
|
|
return _adbCommand(['shell', 'setprop', name, value]);
|
|
}
|
|
|
|
/**
|
|
* Kill all background processes.
|
|
*/
|
|
Future killAll() {
|
|
var arguments = ['shell', 'am', 'kill-all'];
|
|
return _adbCommand(arguments);
|
|
}
|
|
|
|
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 \$?"];
|
|
AdbCommandResult result = await _executeCommand(
|
|
"adb", _deviceSpecificArgs(args),
|
|
timeout: timeout);
|
|
int exitCode = result.exitCode;
|
|
var lines = result.stdout
|
|
.split('\n')
|
|
.where((line) => line.trim().length > 0)
|
|
.toList();
|
|
if (lines.length > 0) {
|
|
int 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 new 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) {
|
|
if (_deviceId != null) {
|
|
var extendedAdbArgs = ['-s', _deviceId];
|
|
extendedAdbArgs.addAll(adbArgs);
|
|
adbArgs = extendedAdbArgs;
|
|
}
|
|
return adbArgs;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to list all adb devices available.
|
|
*/
|
|
class AdbHelper {
|
|
static RegExp _deviceLineRegexp =
|
|
new RegExp(r'^([a-zA-Z0-9_-]+)[ \t]+device$', multiLine: true);
|
|
|
|
static Future<List<String>> listDevices() {
|
|
return Process.run('adb', ['devices']).then((ProcessResult result) {
|
|
if (result.exitCode != 0) {
|
|
throw new Exception("Could not list devices [stdout: ${result.stdout},"
|
|
"stderr: ${result.stderr}]");
|
|
}
|
|
return _deviceLineRegexp
|
|
.allMatches(result.stdout as String)
|
|
.map((Match m) => m.group(1))
|
|
.toList();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Represents an android intent.
|
|
*/
|
|
class Intent {
|
|
String action;
|
|
String package;
|
|
String activity;
|
|
String dataUri;
|
|
|
|
Intent(this.action, this.package, this.activity, [this.dataUri]);
|
|
}
|
|
|
|
/**
|
|
* Discovers all available devices and supports acquire/release.
|
|
*/
|
|
class AdbDevicePool {
|
|
final Queue<AdbDevice> _idleDevices = new Queue<AdbDevice>();
|
|
final Queue<Completer> _waiter = new Queue<Completer>();
|
|
|
|
AdbDevicePool(List<AdbDevice> idleDevices) {
|
|
_idleDevices.addAll(idleDevices);
|
|
}
|
|
|
|
static Future<AdbDevicePool> create() async {
|
|
var names = await AdbHelper.listDevices();
|
|
var devices = names.map((id) => new AdbDevice(id)).toList();
|
|
if (devices.length == 0) {
|
|
throw new Exception('No android devices found. '
|
|
'Please make sure "adb devices" shows your device!');
|
|
}
|
|
print("Found ${devices.length} Android devices.");
|
|
return new AdbDevicePool(devices);
|
|
}
|
|
|
|
Future<AdbDevice> acquireDevice() async {
|
|
if (_idleDevices.length > 0) {
|
|
return _idleDevices.removeFirst();
|
|
} else {
|
|
var completer = new Completer<AdbDevice>();
|
|
_waiter.add(completer);
|
|
return completer.future;
|
|
}
|
|
}
|
|
|
|
void releaseDevice(AdbDevice device) {
|
|
if (_waiter.length > 0) {
|
|
Completer completer = _waiter.removeFirst();
|
|
completer.complete(device);
|
|
} else {
|
|
_idleDevices.add(device);
|
|
}
|
|
}
|
|
}
|