arnaucube
arnaucube/commonroutesServerPublic Self-hosted repository
commonroutesServer/www/platforms/android/cordova/lib/emulator.js
www/platforms/android/cordova/lib/emulator.jsRaw
  1#!/usr/bin/env node
  2
  3/*
  4       Licensed to the Apache Software Foundation (ASF) under one
  5       or more contributor license agreements.  See the NOTICE file
  6       distributed with this work for additional information
  7       regarding copyright ownership.  The ASF licenses this file
  8       to you under the Apache License, Version 2.0 (the
  9       "License"); you may not use this file except in compliance
 10       with the License.  You may obtain a copy of the License at
 11
 12         http://www.apache.org/licenses/LICENSE-2.0
 13
 14       Unless required by applicable law or agreed to in writing,
 15       software distributed under the License is distributed on an
 16       "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 17       KIND, either express or implied.  See the License for the
 18       specific language governing permissions and limitations
 19       under the License.
 20*/
 21
 22/* jshint sub:true */
 23
 24var retry      = require('./retry');
 25var build      = require('./build');
 26var path = require('path');
 27var Adb = require('./Adb');
 28var AndroidManifest = require('./AndroidManifest');
 29var events = require('cordova-common').events;
 30var spawn = require('cordova-common').superspawn.spawn;
 31var CordovaError = require('cordova-common').CordovaError;
 32
 33var Q             = require('q');
 34var os            = require('os');
 35var child_process = require('child_process');
 36
 37// constants
 38var ONE_SECOND              = 1000; // in milliseconds
 39var ONE_MINUTE              = 60 * ONE_SECOND; // in milliseconds
 40var INSTALL_COMMAND_TIMEOUT = 5 * ONE_MINUTE; // in milliseconds
 41var NUM_INSTALL_RETRIES     = 3;
 42var CHECK_BOOTED_INTERVAL   = 3 * ONE_SECOND; // in milliseconds
 43var EXEC_KILL_SIGNAL        = 'SIGKILL';
 44
 45/**
 46 * Returns a Promise for a list of emulator images in the form of objects
 47 * {
 48       name   : <emulator_name>,
 49       path   : <path_to_emulator_image>,
 50       target : <api_target>,
 51       abi    : <cpu>,
 52       skin   : <skin>
 53   }
 54 */
 55module.exports.list_images = function() {
 56    return spawn('android', ['list', 'avds'])
 57    .then(function(output) {
 58        var response = output.split('\n');
 59        var emulator_list = [];
 60        for (var i = 1; i < response.length; i++) {
 61            // To return more detailed information use img_obj
 62            var img_obj = {};
 63            if (response[i].match(/Name:\s/)) {
 64                img_obj['name'] = response[i].split('Name: ')[1].replace('\r', '');
 65                if (response[i + 1].match(/Device:\s/)) {
 66                    i++;
 67                    img_obj['device'] = response[i].split('Device: ')[1].replace('\r', '');
 68                }
 69                if (response[i + 1].match(/Path:\s/)) {
 70                    i++;
 71                    img_obj['path'] = response[i].split('Path: ')[1].replace('\r', '');
 72                }
 73                if (response[i + 1].match(/\(API\slevel\s/) || (response[i + 2] && response[i + 2].match(/\(API\slevel\s/))) {
 74                    i++;
 75                    var secondLine = response[i + 1].match(/\(API\slevel\s/) ? response[i + 1] : '';
 76                    img_obj['target'] = (response[i] + secondLine).split('Target: ')[1].replace('\r', '');
 77                }
 78                if (response[i + 1].match(/ABI:\s/)) {
 79                    i++;
 80                    img_obj['abi'] = response[i].split('ABI: ')[1].replace('\r', '');
 81                }
 82                if (response[i + 1].match(/Skin:\s/)) {
 83                    i++;
 84                    img_obj['skin'] = response[i].split('Skin: ')[1].replace('\r', '');
 85                }
 86
 87                emulator_list.push(img_obj);
 88            }
 89            /* To just return a list of names use this
 90            if (response[i].match(/Name:\s/)) {
 91                emulator_list.push(response[i].split('Name: ')[1].replace('\r', '');
 92            }*/
 93
 94        }
 95        return emulator_list;
 96    });
 97};
 98
 99/**
100 * Will return the closest avd to the projects target
101 * or undefined if no avds exist.
102 * Returns a promise.
103 */
104module.exports.best_image = function() {
105    return this.list_images()
106    .then(function(images) {
107        // Just return undefined if there is no images
108        if (images.length === 0) return;
109
110        var closest = 9999;
111        var best = images[0];
112        // Loading check_reqs at run-time to avoid test-time vs run-time directory structure difference issue
113        var project_target = require('./check_reqs').get_target().replace('android-', '');
114        for (var i in images) {
115            var target = images[i].target;
116            if(target) {
117                var num = target.split('(API level ')[1].replace(')', '');
118                if (num == project_target) {
119                    return images[i];
120                } else if (project_target - num < closest && project_target > num) {
121                    closest = project_target - num;
122                    best = images[i];
123                }
124            }
125        }
126        return best;
127    });
128};
129
130// Returns a promise.
131module.exports.list_started = function() {
132    return Adb.devices({emulators: true});
133};
134
135// Returns a promise.
136module.exports.list_targets = function() {
137    return spawn('android', ['list', 'targets'], {cwd: os.tmpdir()})
138    .then(function(output) {
139        var target_out = output.split('\n');
140        var targets = [];
141        for (var i = target_out.length; i >= 0; i--) {
142            if(target_out[i].match(/id:/)) {
143                targets.push(targets[i].split(' ')[1]);
144            }
145        }
146        return targets;
147    });
148};
149
150/*
151 * Gets unused port for android emulator, between 5554 and 5584
152 * Returns a promise.
153 */
154module.exports.get_available_port = function () {
155    var self = this;
156
157    return self.list_started()
158    .then(function (emulators) {
159        for (var p = 5584; p >= 5554; p-=2) {
160            if (emulators.indexOf('emulator-' + p) === -1) {
161                events.emit('verbose', 'Found available port: ' + p);
162                return p;
163            }
164        }
165        throw new CordovaError('Could not find an available avd port');
166    });
167};
168
169/*
170 * Starts an emulator with the given ID,
171 * and returns the started ID of that emulator.
172 * If no ID is given it will use the first image available,
173 * if no image is available it will error out (maybe create one?).
174 * If no boot timeout is given or the value is negative it will wait forever for
175 * the emulator to boot
176 *
177 * Returns a promise.
178 */
179module.exports.start = function(emulator_ID, boot_timeout) {
180    var self = this;
181
182    return Q().then(function() {
183        if (emulator_ID) return Q(emulator_ID);
184
185        return self.best_image()
186        .then(function(best) {
187            if (best && best.name) {
188                events.emit('warn', 'No emulator specified, defaulting to ' + best.name);
189                return best.name;
190            }
191
192            // Loading check_reqs at run-time to avoid test-time vs run-time directory structure difference issue
193            var androidCmd = require('./check_reqs').getAbsoluteAndroidCmd();
194            return Q.reject(new CordovaError('No emulator images (avds) found.\n' +
195                '1. Download desired System Image by running: ' + androidCmd + ' sdk\n' +
196                '2. Create an AVD by running: ' + androidCmd + ' avd\n' +
197                'HINT: For a faster emulator, use an Intel System Image and install the HAXM device driver\n'));
198        });
199    }).then(function(emulatorId) {
200        return self.get_available_port()
201        .then(function (port) {
202            var args = ['-avd', emulatorId, '-port', port];
203            // Don't wait for it to finish, since the emulator will probably keep running for a long time.
204            child_process
205                .spawn('emulator', args, { stdio: 'inherit', detached: true })
206                .unref();
207
208            // wait for emulator to start
209            events.emit('log', 'Waiting for emulator to start...');
210            return self.wait_for_emulator(port);
211        });
212    }).then(function(emulatorId) {
213        if (!emulatorId)
214            return Q.reject(new CordovaError('Failed to start emulator'));
215
216        //wait for emulator to boot up
217        process.stdout.write('Waiting for emulator to boot (this may take a while)...');
218        return self.wait_for_boot(emulatorId, boot_timeout)
219        .then(function(success) {
220            if (success) {
221                events.emit('log','BOOT COMPLETE');
222                //unlock screen
223                return Adb.shell(emulatorId, 'input keyevent 82')
224                .then(function() {
225                    //return the new emulator id for the started emulators
226                    return emulatorId;
227                });
228            } else {
229                // We timed out waiting for the boot to happen
230                return null;
231            }
232        });
233    });
234};
235
236/*
237 * Waits for an emulator to boot on a given port.
238 * Returns this emulator's ID in a promise.
239 */
240module.exports.wait_for_emulator = function(port) {
241    var self = this;
242    return Q().then(function() {
243        var emulator_id = 'emulator-' + port;
244        return Adb.shell(emulator_id, 'getprop dev.bootcomplete')
245        .then(function (output) {
246            if (output.indexOf('1') >= 0) {
247                return emulator_id;
248            }
249            return self.wait_for_emulator(port);
250        }, function (error) {
251            if (error && error.message &&
252            (error.message.indexOf('not found') > -1) ||
253            error.message.indexOf('device offline') > -1) {
254                // emulator not yet started, continue waiting
255                return self.wait_for_emulator(port);
256            } else {
257                // something unexpected has happened
258                throw error;
259            }
260        });
261     });
262};
263
264/*
265 * Waits for the core android process of the emulator to start. Returns a
266 * promise that resolves to a boolean indicating success. Not specifying a
267 * time_remaining or passing a negative value will cause it to wait forever
268 */
269module.exports.wait_for_boot = function(emulator_id, time_remaining) {
270    var self = this;
271    return Adb.shell(emulator_id, 'ps')
272    .then(function(output) {
273        if (output.match(/android\.process\.acore/)) {
274            return true;
275        } else if (time_remaining === 0) {
276            return false;
277        } else {
278            process.stdout.write('.');
279
280            // Check at regular intervals
281            return Q.delay(time_remaining < CHECK_BOOTED_INTERVAL ? time_remaining : CHECK_BOOTED_INTERVAL).then(function() {
282                var updated_time = time_remaining >= 0 ? Math.max(time_remaining - CHECK_BOOTED_INTERVAL, 0) : time_remaining;
283                return self.wait_for_boot(emulator_id, updated_time);
284            });
285        }
286    });
287};
288
289/*
290 * Create avd
291 * TODO : Enter the stdin input required to complete the creation of an avd.
292 * Returns a promise.
293 */
294module.exports.create_image = function(name, target) {
295    console.log('Creating new avd named ' + name);
296    if (target) {
297        return spawn('android', ['create', 'avd', '--name', name, '--target', target])
298        .then(null, function(error) {
299            console.error('ERROR : Failed to create emulator image : ');
300            console.error(' Do you have the latest android targets including ' + target + '?');
301            console.error(error);
302        });
303    } else {
304        console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.');
305        return spawn('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]])
306        .then(function() {
307            // TODO: This seems like another error case, even though it always happens.
308            console.error('ERROR : Unable to create an avd emulator, no targets found.');
309            console.error('Ensure you have targets available by running the "android" command');
310            return Q.reject();
311        }, function(error) {
312            console.error('ERROR : Failed to create emulator image : ');
313            console.error(error);
314        });
315    }
316};
317
318module.exports.resolveTarget = function(target) {
319    return this.list_started()
320    .then(function(emulator_list) {
321        if (emulator_list.length < 1) {
322            return Q.reject('No running Android emulators found, please start an emulator before deploying your project.');
323        }
324
325        // default emulator
326        target = target || emulator_list[0];
327        if (emulator_list.indexOf(target) < 0) {
328            return Q.reject('Unable to find target \'' + target + '\'. Failed to deploy to emulator.');
329        }
330
331        return build.detectArchitecture(target)
332        .then(function(arch) {
333            return {target:target, arch:arch, isEmulator:true};
334        });
335    });
336};
337
338/*
339 * Installs a previously built application on the emulator and launches it.
340 * If no target is specified, then it picks one.
341 * If no started emulators are found, error out.
342 * Returns a promise.
343 */
344module.exports.install = function(givenTarget, buildResults) {
345
346    var target;
347    var manifest = new AndroidManifest(path.join(__dirname, '../../AndroidManifest.xml'));
348    var pkgName = manifest.getPackageId();
349
350    // resolve the target emulator
351    return Q().then(function () {
352        if (givenTarget && typeof givenTarget == 'object') {
353            return givenTarget;
354        } else {
355            return module.exports.resolveTarget(givenTarget);
356        }
357
358    // set the resolved target
359    }).then(function (resolvedTarget) {
360        target = resolvedTarget;
361
362    // install the app
363    }).then(function () {
364        // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
365        // or the app doesn't installed at all, so no error catching needed.
366        return Q.when()
367        .then(function() {
368
369            var apk_path = build.findBestApkForArchitecture(buildResults, target.arch);
370            var execOptions = {
371                cwd: os.tmpdir(),
372                timeout:    INSTALL_COMMAND_TIMEOUT, // in milliseconds
373                killSignal: EXEC_KILL_SIGNAL
374            };
375
376            events.emit('log', 'Using apk: ' + apk_path);
377            events.emit('log', 'Package name: ' + pkgName);
378            events.emit('verbose', 'Installing app on emulator...');
379
380            // A special function to call adb install in specific environment w/ specific options.
381            // Introduced as a part of fix for http://issues.apache.org/jira/browse/CB-9119
382            // to workaround sporadic emulator hangs
383            function adbInstallWithOptions(target, apk, opts) {
384                events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...');
385
386                var command = 'adb -s ' + target + ' install -r "' + apk + '"';
387                return Q.promise(function (resolve, reject) {
388                    child_process.exec(command, opts, function(err, stdout, stderr) {
389                        if (err) reject(new CordovaError('Error executing "' + command + '": ' + stderr));
390                        // adb does not return an error code even if installation fails. Instead it puts a specific
391                        // message to stdout, so we have to use RegExp matching to detect installation failure.
392                        else if (/Failure/.test(stdout)) {
393                            if (stdout.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
394                                stdout += 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
395                                    ' or sign and deploy the unsigned apk manually using Android tools.';
396                            } else if (stdout.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) {
397                                stdout += 'You\'re trying to install apk with a lower versionCode that is already installed.' +
398                                    '\nEither uninstall an app or increment the versionCode.';
399                            }
400
401                            reject(new CordovaError('Failed to install apk to emulator: ' + stdout));
402                        } else resolve(stdout);
403                    });
404                });
405            }
406
407            function installPromise () {
408                return adbInstallWithOptions(target.target, apk_path, execOptions)
409                .catch(function (error) {
410                    // CB-9557 CB-10157 only uninstall and reinstall app if the one that
411                    // is already installed on device was signed w/different certificate
412                    if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString()))
413                        throw error;
414
415                    events.emit('warn', 'Uninstalling app from device and reinstalling it because the ' +
416                        'currently installed app was signed with different key');
417
418                    // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
419                    // or the app doesn't installed at all, so no error catching needed.
420                    return Adb.uninstall(target.target, pkgName)
421                    .then(function() {
422                        return adbInstallWithOptions(target.target, apk_path, execOptions);
423                    });
424                });
425            }
426
427            return retry.retryPromise(NUM_INSTALL_RETRIES, installPromise)
428            .then(function (output) {
429                events.emit('log', 'INSTALL SUCCESS');
430            });
431        });
432    // unlock screen
433    }).then(function () {
434
435        events.emit('verbose', 'Unlocking screen...');
436        return Adb.shell(target.target, 'input keyevent 82');
437    }).then(function () {
438        Adb.start(target.target, pkgName + '/.' + manifest.getActivity().getName());
439    // report success or failure
440    }).then(function (output) {
441        events.emit('log', 'LAUNCH SUCCESS');
442    });
443};