CB-9119 Adding lib/retry.js for retrying promise-returning functions. Retrying 'adb install' in emulator.js because it sometimes hangs.

This commit is contained in:
Dmitry Blotsky 2015-06-08 17:17:12 -07:00
parent eb70f05168
commit c0312f9b50
3 changed files with 167 additions and 36 deletions

View File

@ -21,14 +21,22 @@
/* jshint sub:true */
var exec = require('./exec'),
Q = require('q'),
os = require('os'),
appinfo = require('./appinfo'),
build = require('./build'),
child_process = require('child_process');
var exec = require('./exec');
var appinfo = require('./appinfo');
var retry = require('./retry');
var build = require('./build');
var check_reqs = require('./check_reqs');
var Q = require('q');
var os = require('os');
var child_process = require('child_process');
// constants
var ONE_SECOND = 1000; // in milliseconds
var INSTALL_COMMAND_TIMEOUT = 120 * ONE_SECOND; // in milliseconds
var NUM_INSTALL_RETRIES = 3;
var EXEC_KILL_SIGNAL = 'SIGKILL';
/**
* Returns a Promise for a list of emulator images in the form of objects
* {
@ -298,37 +306,67 @@ module.exports.resolveTarget = function(target) {
* If no started emulators are found, error out.
* Returns a promise.
*/
module.exports.install = function(target, buildResults) {
return Q().then(function() {
if (target && typeof target == 'object') {
return target;
module.exports.install = function(givenTarget, buildResults) {
var target;
// resolve the target emulator
return Q().then(function () {
if (givenTarget && typeof givenTarget == 'object') {
return givenTarget;
} else {
return module.exports.resolveTarget(givenTarget);
}
return module.exports.resolveTarget(target);
}).then(function(resolvedTarget) {
var apk_path = build.findBestApkForArchitecture(buildResults, resolvedTarget.arch);
// set the resolved target
}).then(function (resolvedTarget) {
target = resolvedTarget;
// install the app
}).then(function () {
var apk_path = build.findBestApkForArchitecture(buildResults, target.arch);
var execOptions = {
timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds
killSignal: EXEC_KILL_SIGNAL
};
console.log('Installing app on emulator...');
console.log('Using apk: ' + apk_path);
return exec('adb -s ' + resolvedTarget.target + ' install -r -d "' + apk_path + '"', os.tmpdir())
.then(function(output) {
var retriedInstall = retry.retryPromise(
NUM_INSTALL_RETRIES,
exec, 'adb -s ' + target.target + ' install -r -d "' + apk_path + '"', os.tmpdir(), execOptions
);
return retriedInstall.then(function (output) {
if (output.match(/Failure/)) {
return Q.reject('Failed to install apk to emulator: ' + output);
} else {
console.log('INSTALL SUCCESS');
}
return Q();
}, function(err) {
}, function (err) {
return Q.reject('Failed to install apk to emulator: ' + err);
}).then(function() {
//unlock screen
return exec('adb -s ' + resolvedTarget.target + ' shell input keyevent 82', os.tmpdir());
}).then(function() {
// launch the application
console.log('Launching application...');
var launchName = appinfo.getActivityName();
var cmd = 'adb -s ' + resolvedTarget.target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName;
return exec(cmd, os.tmpdir());
}).then(function(output) {
console.log('LAUNCH SUCCESS');
}, function(err) {
return Q.reject('Failed to launch app on emulator: ' + err);
});
// unlock screen
}).then(function () {
console.log('Unlocking screen...');
return exec('adb -s ' + target.target + ' shell input keyevent 82', os.tmpdir());
// launch the application
}).then(function () {
console.log('Launching application...');
var launchName = appinfo.getActivityName();
var cmd = 'adb -s ' + target.target + ' shell am start -W -a android.intent.action.MAIN -n ' + launchName;
return exec(cmd, os.tmpdir());
// report success or failure
}).then(function (output) {
console.log('LAUNCH SUCCESS');
}, function (err) {
return Q.reject('Failed to launch app on emulator: ' + err);
});
};

View File

@ -19,23 +19,50 @@
under the License.
*/
var child_process = require('child_process'),
Q = require('q');
var child_process = require("child_process");
var Q = require("q");
// constants
var DEFAULT_MAX_BUFFER = 1024000;
// Takes a command and optional current working directory.
// Returns a promise that either resolves with the stdout, or
// rejects with an error message and the stderr.
module.exports = function(cmd, opt_cwd) {
//
// WARNING:
// opt_cwd is an artifact of an old design, and must
// be removed in the future; the correct solution is
// to pass the options object the same way that
// child_process.exec expects
//
// NOTE:
// exec documented here - https://nodejs.org/api/child_process.html#child_process_child_process_exec_command_options_callback
module.exports = function(cmd, opt_cwd, options) {
var d = Q.defer();
if (typeof options === "undefined") {
options = {};
}
// override cwd to preserve old opt_cwd behavior
options.cwd = opt_cwd;
// set maxBuffer
if (typeof options.maxBuffer === "undefined") {
options.maxBuffer = DEFAULT_MAX_BUFFER;
}
try {
child_process.exec(cmd, {cwd: opt_cwd, maxBuffer: 1024000}, function(err, stdout, stderr) {
if (err) d.reject('Error executing "' + cmd + '": ' + stderr);
child_process.exec(cmd, options, function(err, stdout, stderr) {
if (err) d.reject("Error executing \"" + cmd + "\": " + stderr);
else d.resolve(stdout);
});
} catch(e) {
console.error('error caught: ' + e);
console.error("error caught: " + e);
d.reject(e);
}
return d.promise;
};

66
bin/templates/cordova/lib/retry.js vendored Normal file
View File

@ -0,0 +1,66 @@
#!/usr/bin/env node
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
/* jshint node: true */
"use strict";
/*
* Retry a promise-returning function a number of times, propagating its
* results on success or throwing its error on a failed final attempt.
*
* @arg {Number} attemts_left - The number of times to retry the passed call.
* @arg {Function} promiseFunction - A function that returns a promise.
* @arg {...} - Arguments to pass to promiseFunction.
*
* @returns {Promise}
*/
module.exports.retryPromise = function (attemts_left, promiseFunction) {
// NOTE:
// get all trailing arguments, by skipping the first two (attemts_left and
// promiseFunction) because they shouldn't get passed to promiseFunction
var promiseFunctionArguments = Array.prototype.slice.call(arguments, 2);
return promiseFunction.apply(undefined, promiseFunctionArguments).then(
// on success pass results through
function onFulfilled(value) {
return value;
},
// on rejection either retry, or throw the error
function onRejected(error) {
attemts_left -= 1;
if (attemts_left < 1) {
throw error;
}
console.log("A retried call failed. Retrying " + attemts_left + " more time(s).");
// retry call self again with the same arguments, except attemts_left is now lower
var fullArguments = [attemts_left, promiseFunction].concat(promiseFunctionArguments);
return module.exports.retryPromise.apply(undefined, fullArguments);
}
);
};