Skip to content

Commit

Permalink
feat(start android): extend the --detach flag to wait for appium/andr…
Browse files Browse the repository at this point in the history
…oid (#141)
  • Loading branch information
sjelin authored Nov 16, 2016
1 parent deead0f commit d533b03
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 44 deletions.
6 changes: 4 additions & 2 deletions lib/cmds/opts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ var opts: Options = {};
opts[OUT_DIR] = new Option(OUT_DIR, 'Location to output/expect', 'string', Config.getSeleniumDir());
opts[SELENIUM_PORT] =
new Option(SELENIUM_PORT, 'Optional port for the selenium standalone server', 'string', '4444');
opts[APPIUM_PORT] = new Option(APPIUM_PORT, 'Optional port for the appium server', 'string');
opts[APPIUM_PORT] =
new Option(APPIUM_PORT, 'Optional port for the appium server', 'string', '4723');
opts[AVD_PORT] = new Option(
AVD_PORT, 'Optional port for android virtual devices. See mobile.md for details', 'string');
AVD_PORT, 'Optional port for android virtual devices. See mobile.md for details', 'number',
5554);
opts[IGNORE_SSL] = new Option(IGNORE_SSL, 'Ignore SSL certificates', 'boolean', false);
opts[PROXY] = new Option(PROXY, 'Proxy to use for the install or update command', 'string');
opts[ALTERNATE_CDN] = new Option(ALTERNATE_CDN, 'Alternate CDN to binaries', 'string');
Expand Down
164 changes: 123 additions & 41 deletions lib/cmds/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ if (argv._[0] === 'start-run') {
prog.printHelp();
}

// Manage processes used in android emulation
let androidProcesses: ChildProcess[] = [];

/**
* Parses the options and starts the selenium standalone server.
* @param options
Expand All @@ -71,6 +74,9 @@ function start(options: Options) {
let osType = os.type();
let binaries = FileManager.setupBinaries();
let seleniumPort = options[Opt.SELENIUM_PORT].getString();
let appiumPort = options[Opt.APPIUM_PORT].getString();
let avdPort = options[Opt.AVD_PORT].getNumber();
let android = options[Opt.ANDROID].getBoolean();
let outputDir = Config.getSeleniumDir();
if (options[Opt.OUT_DIR].getString()) {
if (path.isAbsolute(options[Opt.OUT_DIR].getString())) {
Expand Down Expand Up @@ -165,20 +171,18 @@ function start(options: Options) {
// driver does not exist.
}
}
if (options[Opt.ANDROID].getBoolean()) {
if (android) {
if (downloadedBinaries[AndroidSDK.id] != null) {
let avds = options[Opt.AVDS].getString();
startAndroid(
outputDir, binaries[AndroidSDK.id], avds.split(','),
options[Opt.AVD_USE_SNAPSHOTS].getBoolean(), options[Opt.AVD_PORT].getString());
options[Opt.AVD_USE_SNAPSHOTS].getBoolean(), avdPort);
} else {
logger.warn('Not starting android because it is not installed');
}
}
if (downloadedBinaries[Appium.id] != null) {
startAppium(
outputDir, binaries[Appium.id], binaries[AndroidSDK.id],
options[Opt.APPIUM_PORT].getString());
startAppium(outputDir, binaries[Appium.id], binaries[AndroidSDK.id], appiumPort);
}

// log the command to launch selenium server
Expand All @@ -198,10 +202,10 @@ function start(options: Options) {

let seleniumProcess = spawn('java', args, 'inherit');
if (options[Opt.STARTED_SIGNIFIER].getString()) {
// TODO(sjelin): check android too once it's working signalWhenReady(
signalWhenReady(
options[Opt.STARTED_SIGNIFIER].getString(), options[Opt.SIGNAL_VIA_IPC].getBoolean(),
seleniumPort);
outputDir, seleniumPort, downloadedBinaries[Appium.id] ? appiumPort : '',
binaries[AndroidSDK.id], avdPort, androidProcesses.length);
}
logger.info('seleniumProcess.pid: ' + seleniumProcess.pid);
seleniumProcess.on('exit', (code: number) => {
Expand All @@ -220,17 +224,21 @@ function start(options: Options) {
});
}

// Manage processes used in android emulation
let androidProcesses: ChildProcess[] = [];

function startAndroid(
outputDir: string, sdk: Binary, avds: string[], useSnapshots: boolean, port: string): void {
outputDir: string, sdk: Binary, avds: string[], useSnapshots: boolean, port: number): void {
let sdkPath = path.join(outputDir, sdk.executableFilename(os.type()));
if (avds[0] == 'all') {
avds = <string[]>require(path.join(sdkPath, 'available_avds.json'));
} else if (avds[0] == 'none') {
avds.length = 0;
}
const minAVDPort = 5554;
const maxAVDPort = 5586 - 2 * avds.length;
if (avds.length && ((port < minAVDPort) || (port > maxAVDPort))) {
throw new RangeError(
'AVD Port must be between ' + minAVDPort + ' and ' + maxAVDPort + ' to emulate ' +
avds.length + ' android devices');
}
avds.forEach((avd: string, i: number) => {
logger.info('Booting up AVD ' + avd);
// Credit to appium-ci, which this code was adapted from
Expand All @@ -244,7 +252,7 @@ function startAndroid(
emuArgs = emuArgs.concat(['-no-snapshot-load', '-no-snapshot-save']);
}
if (port) {
emuArgs = emuArgs.concat(['-ports', (port + 2 * i) + ',' + (port + 2 * i + 1)]);
emuArgs = emuArgs.concat(['-port', '' + (port + 2 * i)]);
}
if (emuBin !== 'emulator') {
emuArgs = emuArgs.concat(['-qemu', '-enable-kvm']);
Expand All @@ -263,10 +271,10 @@ function killAndroid() {
// Manage appium process
let appiumProcess: ChildProcess;

function startAppium(outputDir: string, binary: Binary, android_sdk: Binary, port: string) {
function startAppium(outputDir: string, binary: Binary, androidSDK: Binary, port: string) {
logger.info('Starting appium server');
if (android_sdk) {
process.env.ANDROID_HOME = path.join(outputDir, android_sdk.executableFilename(os.type()));
if (androidSDK) {
process.env.ANDROID_HOME = path.join(outputDir, androidSDK.executableFilename(os.type()));
}
appiumProcess = spawn(
path.join(outputDir, binary.filename(), 'node_modules', '.bin', 'appium'),
Expand All @@ -280,38 +288,112 @@ function killAppium() {
}
}

function signalWhenReady(signal: string, viaIPC: boolean, seleniumPort: string) {
function check(callback: (ready: boolean) => void) {
http.get(
'http://localhost:' + seleniumPort + '/selenium-server/driver/?cmd=getLogMessages',
(res) => {

function signalWhenReady(
signal: string, viaIPC: boolean, outputDir: string, seleniumPort: string, appiumPort: string,
androidSDK: Binary, avdPort: number, avdCount: number) {
const checkInterval = 100;
const maxWait = 10000;
function waitFor(isReady: () => Promise<void>) {
return new Promise<void>((resolve, reject) => {
let waited = 0;
(function recursiveCheck() {
setTimeout(() => {
isReady().then(
() => {
resolve();
},
(reason) => {
waited += checkInterval;
if (waited < maxWait) {
recursiveCheck();
} else {
reject('Timed out. Final rejection reason: ' + reason);
}
});
}, checkInterval);
})();
});
};
function serverChecker(url: string, test: (data: string) => boolean): () => Promise<void> {
return () => {
return new Promise<void>((resolve, reject) => {
http.get(url, (res) => {
if (res.statusCode !== 200) {
return callback(false);
reject(
'Could not check ' + url + ' for server status (' + res.statusCode + ': ' +
res.statusMessage + ')');
} else {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (test(data)) {
resolve();
} else {
reject('Bad server status: ' + data);
}
});
}
var logs = '';
res.on('data', (chunk) => {
logs += chunk;
});
res.on('end', () => {
callback(logs.toUpperCase().indexOf('OK') != -1);
});
})
.on('error', () => {
callback(false);
}).on('error', () => {
reject();
});
});
};
}

(function recursiveCheck(triesRemaining: number) {
setTimeout(() => {
check((ready: boolean) => {
if (ready) {
sendStartedSignal(signal, viaIPC);
} else if (triesRemaining) {
recursiveCheck(triesRemaining--);
function waitForAndroid(port: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
let child = spawn(
path.join(outputDir, androidSDK.executableFilename(os.type()), 'platform-tools', 'adb'),
['-s', 'emulator-' + port, 'wait-for-device'], 'ignore');
let done = false;
child.on('error', (err: Error) => {
if (!done) {
done = true;
reject('Error while waiting for for emulator-' + port + ': ' + err);
}
});
}, 100);
})(100);
child.on('exit', (code: number, signal: string) => {
if (!done) {
done = true;
resolve();
}
});
setTimeout(() => {
if (!done) {
done = true;
child.kill();
reject('Timed out waiting for emulator-' + port);
}
}, maxWait);
});
}
let pending = [waitFor(serverChecker(
'http://localhost:' + seleniumPort + '/selenium-server/driver/?cmd=getLogMessages',
(logs) => {
return logs.toUpperCase().indexOf('OK') != -1;
}))];
if (appiumPort) {
pending.push(
waitFor(serverChecker('http://localhost:' + appiumPort + '/wd/hub/status', (status) => {
return JSON.parse(status).status == 0;
})));
}
if (androidSDK && avdPort && avdCount) {
for (let i = 0; i < avdCount; i++) {
pending.push(waitForAndroid(avdPort + 2 * i));
}
}
Promise.all(pending).then(
() => {
sendStartedSignal(signal, viaIPC);
},
(error) => {
logger.error(error);
shutdownEverything(seleniumPort);
process.exitCode = 1;
});
}

function sendStartedSignal(signal: string, viaIPC: boolean) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"author": "Craig Nishina <[email protected]>",
"license": "MIT",
"dependencies": {
"@types/es6-promise": "0.0.32",
"adm-zip": "^0.4.7",
"chalk": "^1.1.1",
"del": "^2.2.0",
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"outDir": "built/",
"types": [
"adm-zip", "chalk", "glob", "jasmine", "minimist",
"node", "q", "request", "rimraf", "semver"
"node", "q", "request", "rimraf", "semver", "es6-promise"
]
},
"exclude": [
Expand Down

0 comments on commit d533b03

Please sign in to comment.