123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530 |
- #!/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.
- */
-
- var android_versions = require('android-versions');
- var retry = require('./retry');
- var build = require('./build');
- var path = require('path');
- var Adb = require('./Adb');
- var AndroidManifest = require('./AndroidManifest');
- var events = require('cordova-common').events;
- var superspawn = require('cordova-common').superspawn;
- var CordovaError = require('cordova-common').CordovaError;
- var shelljs = require('shelljs');
- var android_sdk = require('./android_sdk');
- var check_reqs = require('./check_reqs');
-
- var os = require('os');
- var fs = require('fs');
- var child_process = require('child_process');
-
- // constants
- var ONE_SECOND = 1000; // in milliseconds
- var ONE_MINUTE = 60 * ONE_SECOND; // in milliseconds
- var INSTALL_COMMAND_TIMEOUT = 5 * ONE_MINUTE; // in milliseconds
- var NUM_INSTALL_RETRIES = 3;
- var CHECK_BOOTED_INTERVAL = 3 * ONE_SECOND; // in milliseconds
- var EXEC_KILL_SIGNAL = 'SIGKILL';
-
- function forgivingWhichSync (cmd) {
- try {
- return fs.realpathSync(shelljs.which(cmd));
- } catch (e) {
- return '';
- }
- }
-
- module.exports.list_images_using_avdmanager = function () {
- return superspawn.spawn('avdmanager', ['list', 'avd']).then(function (output) {
- var response = output.split('\n');
- var emulator_list = [];
- for (var i = 1; i < response.length; i++) {
- // To return more detailed information use img_obj
- var img_obj = {};
- if (response[i].match(/Name:\s/)) {
- img_obj['name'] = response[i].split('Name: ')[1].replace('\r', '');
- if (response[i + 1].match(/Device:\s/)) {
- i++;
- img_obj['device'] = response[i].split('Device: ')[1].replace('\r', '');
- }
- if (response[i + 1].match(/Path:\s/)) {
- i++;
- img_obj['path'] = response[i].split('Path: ')[1].replace('\r', '');
- }
- if (response[i + 1].match(/Target:\s/)) {
- i++;
- if (response[i + 1].match(/ABI:\s/)) {
- img_obj['abi'] = response[i + 1].split('ABI: ')[1].replace('\r', '');
- }
- // This next conditional just aims to match the old output of `android list avd`
- // We do so so that we don't have to change the logic when parsing for the
- // best emulator target to spawn (see below in `best_image`)
- // This allows us to transitionally support both `android` and `avdmanager` binaries,
- // depending on what SDK version the user has
- if (response[i + 1].match(/Based\son:\s/)) {
- img_obj['target'] = response[i + 1].split('Based on:')[1];
- if (img_obj['target'].match(/Tag\/ABI:\s/)) {
- img_obj['target'] = img_obj['target'].split('Tag/ABI:')[0].replace('\r', '').trim();
- if (img_obj['target'].indexOf('(') > -1) {
- img_obj['target'] = img_obj['target'].substr(0, img_obj['target'].indexOf('(') - 1).trim();
- }
- }
- var version_string = img_obj['target'].replace(/Android\s+/, '');
-
- var api_level = android_sdk.version_string_to_api_level[version_string];
- if (api_level) {
- img_obj['target'] += ' (API level ' + api_level + ')';
- }
- }
- }
- if (response[i + 1].match(/Skin:\s/)) {
- i++;
- img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', '');
- }
-
- emulator_list.push(img_obj);
- }
- /* To just return a list of names use this
- if (response[i].match(/Name:\s/)) {
- emulator_list.push(response[i].split('Name: ')[1].replace('\r', '');
- } */
-
- }
- return emulator_list;
- });
- };
-
- module.exports.list_images_using_android = function () {
- return superspawn.spawn('android', ['list', 'avd']).then(function (output) {
- var response = output.split('\n');
- var emulator_list = [];
- for (var i = 1; i < response.length; i++) {
- // To return more detailed information use img_obj
- var img_obj = {};
- if (response[i].match(/Name:\s/)) {
- img_obj['name'] = response[i].split('Name: ')[1].replace('\r', '');
- if (response[i + 1].match(/Device:\s/)) {
- i++;
- img_obj['device'] = response[i].split('Device: ')[1].replace('\r', '');
- }
- if (response[i + 1].match(/Path:\s/)) {
- i++;
- img_obj['path'] = response[i].split('Path: ')[1].replace('\r', '');
- }
- if (response[i + 1].match(/\(API\slevel\s/) || (response[i + 2] && response[i + 2].match(/\(API\slevel\s/))) {
- i++;
- var secondLine = response[i + 1].match(/\(API\slevel\s/) ? response[i + 1] : '';
- img_obj['target'] = (response[i] + secondLine).split('Target: ')[1].replace('\r', '');
- }
- if (response[i + 1].match(/ABI:\s/)) {
- i++;
- img_obj['abi'] = response[i].split('ABI: ')[1].replace('\r', '');
- }
- if (response[i + 1].match(/Skin:\s/)) {
- i++;
- img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', '');
- }
-
- emulator_list.push(img_obj);
- }
- /* To just return a list of names use this
- if (response[i].match(/Name:\s/)) {
- emulator_list.push(response[i].split('Name: ')[1].replace('\r', '');
- } */
-
- }
- return emulator_list;
- });
- };
-
- /**
- * Returns a Promise for a list of emulator images in the form of objects
- * {
- name : <emulator_name>,
- device : <device>,
- path : <path_to_emulator_image>,
- target : <api_target>,
- abi : <cpu>,
- skin : <skin>
- }
- */
- module.exports.list_images = function () {
- return Promise.resolve().then(function () {
- if (forgivingWhichSync('avdmanager')) {
- return module.exports.list_images_using_avdmanager();
- } else if (forgivingWhichSync('android')) {
- return module.exports.list_images_using_android();
- } else {
- return Promise.reject(new CordovaError('Could not find either `android` or `avdmanager` on your $PATH! Are you sure the Android SDK is installed and available?'));
- }
- }).then(function (avds) {
- // In case we're missing the Android OS version string from the target description, add it.
- return avds.map(function (avd) {
- if (avd.target && avd.target.indexOf('Android API') > -1 && avd.target.indexOf('API level') < 0) {
- var api_level = avd.target.match(/\d+/);
- if (api_level) {
- var level = android_versions.get(api_level);
- if (level) {
- avd.target = 'Android ' + level.semver + ' (API level ' + api_level + ')';
- }
- }
- }
- return avd;
- });
- });
- };
-
- /**
- * Will return the closest avd to the projects target
- * or undefined if no avds exist.
- * Returns a promise.
- */
- module.exports.best_image = function () {
- return this.list_images().then(function (images) {
- // Just return undefined if there is no images
- if (images.length === 0) return;
-
- var closest = 9999;
- var best = images[0];
- var project_target = parseInt(check_reqs.get_target().replace('android-', ''));
- for (var i in images) {
- var target = images[i].target;
- if (target && target.indexOf('API level') > -1) {
- var num = parseInt(target.split('(API level ')[1].replace(')', ''));
- if (num === project_target) {
- return images[i];
- } else if (project_target - num < closest && project_target > num) {
- closest = project_target - num;
- best = images[i];
- }
- }
- }
- return best;
- });
- };
-
- // Returns a promise.
- module.exports.list_started = function () {
- return Adb.devices({ emulators: true });
- };
-
- // Returns a promise.
- // TODO: we should remove this, there's a more robust method under android_sdk.js
- module.exports.list_targets = function () {
- return superspawn.spawn('android', ['list', 'targets'], { cwd: os.tmpdir() }).then(function (output) {
- var target_out = output.split('\n');
- var targets = [];
- for (var i = target_out.length; i >= 0; i--) {
- if (target_out[i].match(/id:/)) {
- targets.push(targets[i].split(' ')[1]);
- }
- }
- return targets;
- });
- };
-
- /*
- * Gets unused port for android emulator, between 5554 and 5584
- * Returns a promise.
- */
- module.exports.get_available_port = function () {
- var self = this;
-
- return self.list_started().then(function (emulators) {
- for (var p = 5584; p >= 5554; p -= 2) {
- if (emulators.indexOf('emulator-' + p) === -1) {
- events.emit('verbose', 'Found available port: ' + p);
- return p;
- }
- }
- throw new CordovaError('Could not find an available avd port');
- });
- };
-
- /*
- * Starts an emulator with the given ID,
- * and returns the started ID of that emulator.
- * If no ID is given it will use the first image available,
- * if no image is available it will error out (maybe create one?).
- * If no boot timeout is given or the value is negative it will wait forever for
- * the emulator to boot
- *
- * Returns a promise.
- */
- module.exports.start = function (emulator_ID, boot_timeout) {
- var self = this;
-
- return Promise.resolve().then(function () {
- if (emulator_ID) return Promise.resolve(emulator_ID);
-
- return self.best_image().then(function (best) {
- if (best && best.name) {
- events.emit('warn', 'No emulator specified, defaulting to ' + best.name);
- return best.name;
- }
-
- var androidCmd = check_reqs.getAbsoluteAndroidCmd();
- return Promise.reject(new CordovaError('No emulator images (avds) found.\n' +
- '1. Download desired System Image by running: ' + androidCmd + ' sdk\n' +
- '2. Create an AVD by running: ' + androidCmd + ' avd\n' +
- 'HINT: For a faster emulator, use an Intel System Image and install the HAXM device driver\n'));
- });
- }).then(function (emulatorId) {
- return self.get_available_port().then(function (port) {
- // Figure out the directory the emulator binary runs in, and set the cwd to that directory.
- // Workaround for https://code.google.com/p/android/issues/detail?id=235461
- var emulator_dir = path.dirname(shelljs.which('emulator'));
- var args = ['-avd', emulatorId, '-port', port];
- // Don't wait for it to finish, since the emulator will probably keep running for a long time.
- child_process
- .spawn('emulator', args, { stdio: 'inherit', detached: true, cwd: emulator_dir })
- .unref();
-
- // wait for emulator to start
- events.emit('log', 'Waiting for emulator to start...');
- return self.wait_for_emulator(port);
- });
- }).then(function (emulatorId) {
- if (!emulatorId) { return Promise.reject(new CordovaError('Failed to start emulator')); }
-
- // wait for emulator to boot up
- process.stdout.write('Waiting for emulator to boot (this may take a while)...');
- return self.wait_for_boot(emulatorId, boot_timeout).then(function (success) {
- if (success) {
- events.emit('log', 'BOOT COMPLETE');
- // unlock screen
- return Adb.shell(emulatorId, 'input keyevent 82').then(function () {
- // return the new emulator id for the started emulators
- return emulatorId;
- });
- } else {
- // We timed out waiting for the boot to happen
- return null;
- }
- });
- });
- };
-
- /*
- * Waits for an emulator to boot on a given port.
- * Returns this emulator's ID in a promise.
- */
- module.exports.wait_for_emulator = function (port) {
- var self = this;
- return Promise.resolve().then(function () {
- var emulator_id = 'emulator-' + port;
- return Adb.shell(emulator_id, 'getprop dev.bootcomplete').then(function (output) {
- if (output.indexOf('1') >= 0) {
- return emulator_id;
- }
- return self.wait_for_emulator(port);
- }, function (error) {
- if ((error && error.message &&
- (error.message.indexOf('not found') > -1)) ||
- (error.message.indexOf('device offline') > -1) ||
- (error.message.indexOf('device still connecting') > -1) ||
- (error.message.indexOf('device still authorizing') > -1)) {
- // emulator not yet started, continue waiting
- return self.wait_for_emulator(port);
- } else {
- // something unexpected has happened
- throw error;
- }
- });
- });
- };
-
- /*
- * Waits for the core android process of the emulator to start. Returns a
- * promise that resolves to a boolean indicating success. Not specifying a
- * time_remaining or passing a negative value will cause it to wait forever
- */
- module.exports.wait_for_boot = function (emulator_id, time_remaining) {
- var self = this;
- return Adb.shell(emulator_id, 'ps').then(function (output) {
- if (output.match(/android\.process\.acore/)) {
- return true;
- } else if (time_remaining === 0) {
- return false;
- } else {
- process.stdout.write('.');
-
- return new Promise(resolve => {
- const delay = time_remaining < CHECK_BOOTED_INTERVAL ? time_remaining : CHECK_BOOTED_INTERVAL;
-
- setTimeout(() => {
- const updated_time = time_remaining >= 0 ? Math.max(time_remaining - CHECK_BOOTED_INTERVAL, 0) : time_remaining;
- resolve(self.wait_for_boot(emulator_id, updated_time));
- }, delay);
- });
- }
- });
- };
-
- /*
- * Create avd
- * TODO : Enter the stdin input required to complete the creation of an avd.
- * Returns a promise.
- */
- module.exports.create_image = function (name, target) {
- console.log('Creating new avd named ' + name);
- if (target) {
- return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', target]).then(null, function (error) {
- console.error('ERROR : Failed to create emulator image : ');
- console.error(' Do you have the latest android targets including ' + target + '?');
- console.error(error);
- });
- } else {
- console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.');
- // TODO: there's a more robust method for finding targets in android_sdk.js
- return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]).then(function () {
- // TODO: This seems like another error case, even though it always happens.
- console.error('ERROR : Unable to create an avd emulator, no targets found.');
- console.error('Ensure you have targets available by running the "android" command');
- return Promise.reject(new CordovaError());
- }, function (error) {
- console.error('ERROR : Failed to create emulator image : ');
- console.error(error);
- });
- }
- };
-
- module.exports.resolveTarget = function (target) {
- return this.list_started().then(function (emulator_list) {
- if (emulator_list.length < 1) {
- return Promise.reject(new CordovaError('No running Android emulators found, please start an emulator before deploying your project.'));
- }
-
- // default emulator
- target = target || emulator_list[0];
- if (emulator_list.indexOf(target) < 0) {
- return Promise.reject(new CordovaError('Unable to find target \'' + target + '\'. Failed to deploy to emulator.'));
- }
-
- return build.detectArchitecture(target).then(function (arch) {
- return { target: target, arch: arch, isEmulator: true };
- });
- });
- };
-
- /*
- * Installs a previously built application on the emulator and launches it.
- * If no target is specified, then it picks one.
- * If no started emulators are found, error out.
- * Returns a promise.
- */
- module.exports.install = function (givenTarget, buildResults) {
-
- var target;
- // We need to find the proper path to the Android Manifest
- const manifestPath = path.join(__dirname, '..', '..', 'app', 'src', 'main', 'AndroidManifest.xml');
- const manifest = new AndroidManifest(manifestPath);
- const pkgName = manifest.getPackageId();
-
- // resolve the target emulator
- return Promise.resolve().then(function () {
- if (givenTarget && typeof givenTarget === 'object') {
- return givenTarget;
- } else {
- return module.exports.resolveTarget(givenTarget);
- }
-
- // set the resolved target
- }).then(function (resolvedTarget) {
- target = resolvedTarget;
-
- // install the app
- }).then(function () {
- // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
- // or the app doesn't installed at all, so no error catching needed.
- return Promise.resolve().then(function () {
-
- var apk_path = build.findBestApkForArchitecture(buildResults, target.arch);
- var execOptions = {
- cwd: os.tmpdir(),
- timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds
- killSignal: EXEC_KILL_SIGNAL
- };
-
- events.emit('log', 'Using apk: ' + apk_path);
- events.emit('log', 'Package name: ' + pkgName);
- events.emit('verbose', 'Installing app on emulator...');
-
- // A special function to call adb install in specific environment w/ specific options.
- // Introduced as a part of fix for http://issues.apache.org/jira/browse/CB-9119
- // to workaround sporadic emulator hangs
- function adbInstallWithOptions (target, apk, opts) {
- events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...');
-
- var command = 'adb -s ' + target + ' install -r "' + apk + '"';
- return new Promise(function (resolve, reject) {
- child_process.exec(command, opts, function (err, stdout, stderr) {
- if (err) reject(new CordovaError('Error executing "' + command + '": ' + stderr));
- // adb does not return an error code even if installation fails. Instead it puts a specific
- // message to stdout, so we have to use RegExp matching to detect installation failure.
- else if (/Failure/.test(stdout)) {
- if (stdout.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
- stdout += 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
- ' or sign and deploy the unsigned apk manually using Android tools.';
- } else if (stdout.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) {
- stdout += 'You\'re trying to install apk with a lower versionCode that is already installed.' +
- '\nEither uninstall an app or increment the versionCode.';
- }
-
- reject(new CordovaError('Failed to install apk to emulator: ' + stdout));
- } else resolve(stdout);
- });
- });
- }
-
- function installPromise () {
- return adbInstallWithOptions(target.target, apk_path, execOptions).catch(function (error) {
- // CB-9557 CB-10157 only uninstall and reinstall app if the one that
- // is already installed on device was signed w/different certificate
- if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString())) { throw error; }
-
- events.emit('warn', 'Uninstalling app from device and reinstalling it because the ' +
- 'currently installed app was signed with different key');
-
- // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
- // or the app doesn't installed at all, so no error catching needed.
- return Adb.uninstall(target.target, pkgName).then(function () {
- return adbInstallWithOptions(target.target, apk_path, execOptions);
- });
- });
- }
-
- return retry.retryPromise(NUM_INSTALL_RETRIES, installPromise).then(function (output) {
- events.emit('log', 'INSTALL SUCCESS');
- });
- });
- // unlock screen
- }).then(function () {
-
- events.emit('verbose', 'Unlocking screen...');
- return Adb.shell(target.target, 'input keyevent 82');
- }).then(function () {
- Adb.start(target.target, pkgName + '/.' + manifest.getActivity().getName());
- // report success or failure
- }).then(function (output) {
- events.emit('log', 'LAUNCH SUCCESS');
- });
- };
|