From c0312f9b502c5b947040244b0f4acfb05741e6c5 Mon Sep 17 00:00:00 2001 From: Dmitry Blotsky Date: Mon, 8 Jun 2015 17:17:12 -0700 Subject: [PATCH] CB-9119 Adding lib/retry.js for retrying promise-returning functions. Retrying 'adb install' in emulator.js because it sometimes hangs. --- bin/templates/cordova/lib/emulator.js | 98 +++++++++++++++++++-------- bin/templates/cordova/lib/exec.js | 39 +++++++++-- bin/templates/cordova/lib/retry.js | 66 ++++++++++++++++++ 3 files changed, 167 insertions(+), 36 deletions(-) create mode 100644 bin/templates/cordova/lib/retry.js diff --git a/bin/templates/cordova/lib/emulator.js b/bin/templates/cordova/lib/emulator.js index 5e79152f..e81dd679 100644 --- a/bin/templates/cordova/lib/emulator.js +++ b/bin/templates/cordova/lib/emulator.js @@ -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); }); }; diff --git a/bin/templates/cordova/lib/exec.js b/bin/templates/cordova/lib/exec.js index 342b6fbb..798a93ba 100644 --- a/bin/templates/cordova/lib/exec.js +++ b/bin/templates/cordova/lib/exec.js @@ -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; }; diff --git a/bin/templates/cordova/lib/retry.js b/bin/templates/cordova/lib/retry.js new file mode 100644 index 00000000..dc52a7d2 --- /dev/null +++ b/bin/templates/cordova/lib/retry.js @@ -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); + } + ); +};