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};