Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Better Android emulator sync #434

Merged
merged 2 commits into from
Nov 27, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion detox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,13 @@
"detox-server": "^2.1.0",
"fs-extra": "^4.0.2",
"get-port": "^2.1.0",
"ini": "^1.3.4",
"lodash": "^4.14.1",
"npmlog": "^4.0.2",
"shell-utils": "^1.0.9",
"telnet-client": "0.15.3",
"ws": "^1.1.1"
"ws": "^1.1.1",
"tail":"^1.2.3"
},
"engines": {
"node": ">=7.6"
Expand Down
3 changes: 2 additions & 1 deletion detox/src/client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class Client {
}

async cleanup() {
console.log(this.isConnected);
clearTimeout(this.slowInvocationStatusHandler);

if (this.isConnected) {
await this.sendAction(new actions.Cleanup(this.successfulTestRun));
this.isConnected = false;
Expand Down
4 changes: 2 additions & 2 deletions detox/src/devices/AndroidDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class AndroidDriver extends DeviceDriverBase {
}

this.instrumentationProcess = spawn(`adb`, [`-s`, `${deviceId}`, `shell`, `am`, `instrument`, `-w`, `-r`, `${args.join(' ')}`, `-e`, `debug`,
`false`, `${bundleId}.test/android.support.test.runner.AndroidJUnitRunner`]);
`false`, `${bundleId}.test/android.support.test.runner.AndroidJUnitRunner`]);
log.verbose(this.instrumentationProcess.spawnargs.join(" "));
log.verbose('Instrumentation spawned, childProcess.pid: ', this.instrumentationProcess.pid);
this.instrumentationProcess.stdout.on('data', function(data) {
Expand Down Expand Up @@ -93,7 +93,7 @@ class AndroidDriver extends DeviceDriverBase {

terminateInstrumentation() {
if (this.instrumentationProcess) {
this.instrumentationProcess.kill('SIGHUP');
this.instrumentationProcess.kill();
this.instrumentationProcess = null;
}
}
Expand Down
44 changes: 39 additions & 5 deletions detox/src/devices/EmulatorDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,38 @@ const Emulator = require('./android/Emulator');
const EmulatorTelnet = require('./android/EmulatorTelnet');
const Environment = require('../utils/environment');
const AndroidDriver = require('./AndroidDriver');
const ini = require('ini');
const fs = require('fs');
const os = require('os');

class EmulatorDriver extends AndroidDriver {

constructor(client) {
super(client);

this.emulator = new Emulator();
}

async prepare() {
async _fixEmulatorConfigIniSkinName(name) {
const configFile = `${os.homedir()}/.android/avd/${name}.avd/config.ini`;
const config = ini.parse(fs.readFileSync(configFile, 'utf-8'));

if (!config['skin.name']) {
const width = config['hw.lcd.width'];
const height = config['hw.lcd.height'];

if (width === undefined || height === undefined) {
throw new Error(`Emulator with name ${name} has a corrupt config.ini file (${configFile}), try fixing it by recreating an emulator.`);
}

config['skin.name'] = `${width}x${height}`;
fs.writeFileSync(configFile, ini.stringify(config));
}
return config;
}

async boot(deviceId) {
await this.emulator.boot(deviceId);
await this.adb.waitForBootComplete(deviceId);
}

async acquireFreeDevice(name) {
Expand All @@ -34,11 +51,28 @@ class EmulatorDriver extends AndroidDriver {
make sure you choose one of the available emulators: ${avds.toString()}`);
}

await this._fixEmulatorConfigIniSkinName(name);
await this.emulator.boot(name);

const deviceId = await this.findDeviceId({type: 'emulator', name: name});
await this.adb.unlockScreen(deviceId);
return deviceId;
const adbDevices = await this.adb.devices();
const filteredDevices = _.filter(adbDevices, {type: 'emulator', name: name});

let adbName;
switch (filteredDevices.length) {
case 0:
throw new Error(`Could not find '${name}' on the currently ADB attached devices,
try restarting adb 'adb kill-server && adb start-server'`);
case 1:
const adbDevice = filteredDevices[0];
adbName = adbDevice.adbName;
break;
default:
throw new Error(`Got more than one device corresponding to the name: ${name}`);
}

await this.adb.waitForBootComplete(adbName);
await this.adb.unlockScreen(adbName);
return adbName;
}

async shutdown(deviceId) {
Expand Down
2 changes: 1 addition & 1 deletion detox/src/devices/android/AAPT.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class AAPT {
async getPackageName(apkPath) {
await this._prepare();
const process = await exec(`${this.aaptBin} dump badging "${apkPath}"`);
let packageName = new RegExp(/package: name='([^']+)'/g).exec(process.stdout);
const packageName = new RegExp(/package: name='([^']+)'/g).exec(process.stdout);
return packageName[1];
}
}
Expand Down
27 changes: 23 additions & 4 deletions detox/src/devices/android/ADB.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class ADB {
async parseAdbDevicesConsoleOutput(input) {
const outputToList = input.trim().split('\n');
const devicesList = _.takeRight(outputToList, outputToList.length - 1);
let devices = [];
for (let deviceString of devicesList) {
const devices = [];
for (const deviceString of devicesList) {
const deviceParams = deviceString.split('\t');
const deviceAdbName = deviceParams[0];
let device;
Expand All @@ -29,13 +29,13 @@ class ADB {
await telnet.connect(port);
const name = await telnet.avdName();
device = {type: 'emulator', name: name, adbName: deviceAdbName, port: port};
await telnet.quit();
} else if (this.isGenymotion(deviceAdbName)) {
device = {type: 'genymotion', name: deviceAdbName, adbName: deviceAdbName};
} else {
device = {type: 'device', name: deviceAdbName, adbName: deviceAdbName};
}
devices.push(device);

}
return devices;
}
Expand Down Expand Up @@ -65,14 +65,33 @@ class ADB {
}

async shell(deviceId, cmd) {
await this.adbCmd(deviceId, `shell ${cmd}`)
return (await this.adbCmd(deviceId, `shell ${cmd}`)).stdout.trim();
}

async waitForBootComplete(deviceId) {
try {
const bootComplete = await this.shell(deviceId, `getprop dev.bootcomplete`);
if (bootComplete === '1') {
return true;
} else {
await this.sleep(2000);
return await this.waitForBootComplete(deviceId);
}
} catch (ex) {
await this.sleep(2000);
return await this.waitForBootComplete(deviceId);
}
}

async adbCmd(deviceId, params) {
const serial = `${deviceId ? `-s ${deviceId}` : ''}`;
const cmd = `${this.adbBin} ${serial} ${params}`;
return await exec(cmd, undefined, undefined, 1);
}

async sleep(ms = 0) {
return new Promise((resolve, reject) => setTimeout(resolve, ms));
}
}

module.exports = ADB;
45 changes: 29 additions & 16 deletions detox/src/devices/android/Emulator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,15 @@ const exec = require('../../utils/exec').execWithRetriesAndLogs;
const spawn = require('child-process-promise').spawn;
const _ = require('lodash');
const log = require('npmlog');
const fs = require('fs');
const Environment = require('../../utils/environment');
const Tail = require('tail').Tail;

class Emulator {

constructor() {
this.emulatorBin = path.join(Environment.getAndroidSDKPath(), 'tools', 'emulator');
}

async boot(emulatorName) {
await this.spawnEmulator(`-verbose -gpu host -no-audio @${emulatorName}`);
}

async listAvds() {
const output = await this.exec(`-list-avds --verbose`);
const avds = output.trim().split('\n');
Expand All @@ -25,28 +22,44 @@ class Emulator {
return (await exec(`${this.emulatorBin} ${cmd}`)).stdout;
}

async spawnEmulator(cmd) {
const promise = spawn(this.emulatorBin, _.split(cmd, ' '));
async boot(emulatorName) {
const cmd = `-verbose -gpu host -no-audio @${emulatorName}`;
log.verbose(this.emulatorBin, cmd);
const tempLog = `./${emulatorName}.log`;
const stdout = fs.openSync(tempLog, 'a');
const stderr = fs.openSync(tempLog, 'a');
const tail = new Tail(tempLog);
const promise = spawn(this.emulatorBin, _.split(cmd, ' '), {detached: true, stdio: ['ignore', stdout, stderr]});

const childProcess = promise.childProcess;
childProcess.stdout.on('data', function(data) {
log.verbose('Emulator stdout: ', data.toString());
const output = data.toString();
if (output.includes('Adb connected, start proxing data')) {
promise._cpResolve();
childProcess.unref();

tail.on("line", function(data) {
if (data.includes('Adb connected, start proxing data')) {
detach();
}
if (output.includes(`There's another emulator instance running with the current AVD`)) {
promise._cpResolve();
if (data.includes(`There's another emulator instance running with the current AVD`)) {
detach();
}
});
childProcess.stderr.on('data', function(data) {
log.verbose('Emulator stderr: ', data.toString());

tail.on("error", function(error) {
detach();
log.verbose('Emulator stderr: ', error);
});

promise.catch(function(err) {
log.error('Emulator ERROR: ', err);
});

function detach() {
tail.unwatch();
fs.closeSync(stdout);
fs.closeSync(stderr);
fs.unlink(tempLog);
promise._cpResolve();
}

return promise;
}
}
Expand Down
14 changes: 6 additions & 8 deletions detox/src/devices/android/EmulatorTelnet.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ const os = require('os');
const fs = require('fs-extra');

class EmulatorTelnet {

constructor() {
this.connection = new Telnet();
}

async connect(port) {
let params = {
const params = {
host: 'localhost',
port: port,
shellPrompt: /^OK$/m,
Expand All @@ -24,7 +23,6 @@ class EmulatorTelnet {
await this.connection.connect(params);
const auth = await fs.readFile(path.join(os.homedir(), '.emulator_console_auth_token'), 'utf8');
await this.exec(`auth ${auth}`);

}

async exec(command) {
Expand All @@ -38,11 +36,11 @@ class EmulatorTelnet {
this.connection.shell((error, stream) => {
stream.write(`${command}\n`);
stream.on('data', (data) => {
const result = data.toString();
if (result.includes('\n')) {
resolve(result);
}
const result = data.toString();
if (result.includes('\n')) {
resolve(result);
}
}
);
});
});
Expand All @@ -67,4 +65,4 @@ class EmulatorTelnet {
}
}

module.exports = EmulatorTelnet;
module.exports = EmulatorTelnet;
5 changes: 4 additions & 1 deletion detox/src/utils/exec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ const exec = require('child-process-promise').exec;

let _operationCounter = 0;

/**

*/
async function execWithRetriesAndLogs(bin, options, statusLogs, retries = 10, interval = 1000) {
_operationCounter++;

Expand All @@ -17,7 +20,7 @@ async function execWithRetriesAndLogs(bin, options, statusLogs, retries = 10, in
log.verbose(`${_operationCounter}: ${cmd}`);

let result;
await retry({ retries, interval }, async () => {
await retry({retries, interval}, async () => {
if (statusLogs && statusLogs.trying) {
log.info(`${_operationCounter}: ${statusLogs.trying}`);
}
Expand Down