UNPKG

14.6 kBJavaScriptView Raw
1/**
2 * @overview
3 * Library for controlling an Android Emulator.
4 *
5 * @module lib/emulator
6 *
7 * @copyright
8 * Copyright (c) 2009-2014 by Appcelerator, Inc. All Rights Reserved.
9 *
10 * @license
11 * Licensed under the terms of the Apache Public License
12 * Please see the LICENSE included with this distribution for details.
13 */
14'use strict';
15
16const android = require('./android'),
17 appc = require('node-appc'),
18 __ = appc.i18n(__dirname).__,
19 ADB = require('./adb'),
20 async = require('async'),
21 events = require('events'),
22 fs = require('fs'),
23 path = require('path'),
24 util = require('util');
25require('colors');
26
27module.exports = EmulatorManager;
28
29/**
30 * Creates an Emulator instace.
31 * @class
32 * @extends EventEmitter
33 * @classdesc Simple object that contains the avd settings and exposes event
34 * methods.
35 * @constructor
36 */
37function Emulator() {}
38util.inherits(EmulatorManager.Emulator = Emulator, events.EventEmitter);
39
40/**
41 * Creates an EmulatorManager instance.
42 * @class
43 * @classdesc Manages emulator implementations and responsible for launching and
44 * killing emulators.
45 * @constructor
46 * @param {Object} config - The CLI config object
47 */
48function EmulatorManager(config) {
49 this.config = config;
50}
51
52/**
53 * Loads emulator implementation modules and detects all available emulators.
54 * @param {Object} [opts] - Detection options
55 * @param {String} [opts.type] - The type of emulator to load (avd, genymotion); defaults to all
56 * @param {Function} callback - A function to call when the detection has completed
57 */
58EmulatorManager.prototype.detect = function detect(opts, callback) {
59 if (opts && typeof opts === 'function') {
60 callback = opts;
61 opts = {};
62 }
63
64 var files = opts && opts.type ? [ opts.type + '.js' ] : fs.readdirSync(path.join(__dirname, 'emulators')),
65 re = /\.js$/,
66 config = this.config;
67
68 async.parallel(files.map(function (filename) {
69 return function (next) {
70 var file = path.join(__dirname, 'emulators', filename);
71 if (re.test(filename) && fs.existsSync(file)) {
72 var module = require(file);
73 if (typeof module.detect === 'function') {
74 module.detect(config, opts, next);
75 return;
76 }
77 }
78 next();
79 };
80 }), function (err, results) {
81 if (err) {
82 return callback(err);
83 }
84
85 android.detect(this.config, opts, function (androidEnv) {
86 var ver2api = {},
87 emus = [];
88
89 Object.keys(androidEnv.targets).forEach(function (id) {
90 if (androidEnv.targets[id].type === 'platform') {
91 ver2api[androidEnv.targets[id].version] = androidEnv.targets[id].sdk;
92 }
93 });
94
95 results.forEach(function (r) {
96 if (r && Array.isArray(r.avds)) {
97 r.avds.forEach(function (avd) {
98 if (!avd['api-level']) {
99 avd['api-level'] = ver2api[avd['sdk-version']] || null;
100 }
101 if (!avd.id) {
102 avd.id = avd.name;
103 }
104 emus.push(avd);
105 });
106 }
107 });
108
109 opts.logger && opts.logger.trace(__('Found %s emulators', String(emus.length).cyan));
110 callback(null, emus);
111 });
112 }.bind(this));
113};
114
115/**
116 * Detects if a specific Android emulator is running.
117 * @param {String} id - The id of the emulator
118 * @param {Object} [opts] - Detection options
119 * @param {String} [opts.type] - The type of emulator to load (avd, genymotion); defaults to all
120 * @param {Function} callback - A function to call when the detection has completed
121 */
122EmulatorManager.prototype.isRunning = function isRunning(id, opts, callback) {
123 if (opts && typeof opts === 'function') {
124 callback = opts;
125 opts = {};
126 }
127
128 opts.logger && opts.logger.trace(__('Detecting if %s exists...', id.cyan));
129
130 this.detect(opts, function (err, emus) {
131 if (err) {
132 return callback(err);
133 }
134
135 const emu = emus.filter(e => e && e.id == id).shift(); // eslint-disable-line eqeqeq
136
137 if (!emu) {
138 return callback(new Error(__('Invalid emulator "%s"', id)), null);
139 }
140
141 opts.logger && opts.logger.trace(__('Emulator exists, detecting all running emulators and connected devices...'));
142
143 // need to see if the emulator is running
144 const adb = new ADB(this.config);
145 adb.devices(function (err, devices) {
146 if (err) {
147 return callback(err);
148 }
149
150 opts.logger && opts.logger.trace(__('Detected %s running emulators and connected devices', String(devices.length).cyan));
151
152 // if there are no devices, then it can't possibly be running
153 if (!devices.length) {
154 return callback(null, null);
155 }
156
157 opts.logger && opts.logger.trace(__('Checking %s devices to see if it\'s the emulator we want', String(devices.length).cyan));
158
159 require(path.join(__dirname, 'emulators', emu.type + '.js')).isRunning(this.config, emu, devices, function (err, device) {
160 if (err) {
161 opts.logger && opts.logger.trace(__('Failed to check if the emulator was running: %s', err));
162 } else if (device) {
163 opts.logger && opts.logger.trace(__('The emulator is running'));
164 } else {
165 opts.logger && opts.logger.trace(__('The emulator is NOT running'));
166 }
167 callback(err, device);
168 });
169 }.bind(this));
170 }.bind(this));
171};
172
173/**
174 * Determines if the specified "device name" is an emulator or a device.
175 * @param {String} device - The name of the device returned from 'adb devices'
176 * @param {Object} [opts] - Detection options
177 * @param {String} [opts.type] - The type of emulator to load (avd, genymotion); defaults to all
178 * @param {Function} callback - A function to call when the detection has completed
179 */
180EmulatorManager.prototype.isEmulator = function isEmulator(device, opts, callback) {
181 if (opts && typeof opts === 'function') {
182 callback = opts;
183 opts = {};
184 }
185
186 var files = opts && opts.type ? [ opts.type + '.js' ] : fs.readdirSync(path.join(__dirname, 'emulators')),
187 re = /\.js$/,
188 config = this.config;
189
190 async.parallel(files.map(function (filename) {
191 return function (next) {
192 var file = path.join(__dirname, 'emulators', filename);
193 if (re.test(filename) && fs.existsSync(file)) {
194 var module = require(file);
195 if (typeof module.isEmulator === 'function') {
196 module.isEmulator(config, device, next);
197 return;
198 }
199 }
200 next();
201 };
202 }), function (err, results) {
203 if (err) {
204 callback(new Error(__('Unable to find device "%s"', device)));
205 } else {
206 callback(null, results.filter(n => n).shift());
207 }
208 });
209};
210
211function checkedBooted(config, opts, emulator) {
212 // we need to get the id of emulator
213 var adb = new ADB(config),
214 retryTimeout = 2000, // if an adb call fails, how long before we retry
215 bootTimeout = opts.bootTimeout || 240000, // 4 minutes to boot before timeout
216 // if a timeout is set and the emulator doesn't boot quick enough, fire the timeout event,
217 // however if the timeout is zero, still listen for the timeout to kill the whilst loop above
218 bootTimer = setTimeout(function () {
219 opts.logger && opts.logger.trace(__('Timed out while waiting for the emulator to boot; waited %s ms', bootTimeout));
220 conn && conn.end();
221 bootTimeout && emulator.emit('timeout', { type: 'emulator', waited: bootTimeout });
222 }, bootTimeout),
223 sdcardTimeout = opts.sdcardTimeout || 60000, // 1 minute to boot before timeout
224 sdcardTimer,
225 conn,
226 deviceId,
227 emu = emulator.emulator,
228 emulib = require(path.join(__dirname, 'emulators', emu.type + '.js'));
229
230 opts.logger && opts.logger.trace(__('Checking the boot state for the next %s ms', bootTimeout));
231 opts.logger && opts.logger.trace(__('Waiting for emulator to register with ADB'));
232
233 conn = adb.trackDevices(function (err, devices) {
234 if (err) {
235 opts.logger && opts.logger.trace(__('Error tracking devices: %s', err.message));
236 return;
237 } else if (!devices.length) {
238 opts.logger && opts.logger.trace(__('No devices found, continuing to wait'));
239 return;
240 }
241
242 // just in case we get any extra events but we already have the deviceId, just return
243 if (deviceId) {
244 return;
245 }
246
247 opts.logger && opts.logger.trace(__('Found %s devices, checking if any of them are the emulator...', devices.length));
248
249 emulib.isRunning(config, emu, devices, function (err, running) {
250 if (err) {
251 // TODO: this could be bad... maybe we should emit an error event?
252 opts.logger && opts.logger.trace(__('Error checking if emulator is running: %s', err));
253 } else if (!running) {
254 // try again
255 opts.logger && opts.logger.trace(__('Emulator not running yet, continuing to wait'));
256 } else {
257 // running!
258 opts.logger && opts.logger.trace(__('Emulator is running!'));
259 appc.util.mix(emulator, running);
260 deviceId = running.id;
261 conn.end(); // no need to track devices anymore
262
263 // keep polling until the boot animation has finished
264 opts.logger && opts.logger.trace(__('Checking if boot animation has finished...'));
265 (function checkBootAnim() {
266 // emulator is running, now shell into it and check if it has booted
267 adb.shell(deviceId, 'getprop init.svc.bootanim', function (err, output) {
268 if (!err && output.toString().split('\n').shift().trim() === 'stopped') {
269 clearTimeout(bootTimer);
270 opts.logger && opts.logger.trace(__('Emulator is booted, emitting booted event'));
271 emulator.emit('booted', emulator);
272 } else {
273 opts.logger && opts.logger.trace(__('Emulator is not booted yet; checking again in %s ms', retryTimeout));
274 setTimeout(checkBootAnim, retryTimeout);
275 }
276 });
277 }());
278 }
279 });
280 });
281
282 emulator.on('booted', function () {
283 var done = false;
284
285 opts.logger && opts.logger.info(__('Emulator is booted'));
286
287 if (!opts.checkMounts || !emu.sdcard) {
288 // nothing to do, fire ready event
289 opts.logger && opts.logger.info(__('SD card not required, skipping mount check'));
290 emulator.emit('ready', emulator);
291 return;
292 }
293
294 opts.logger && opts.logger.info(__('Checking if SD card is mounted'));
295
296 // keep polling /sdcard until it's mounted
297 async.whilst(
298 function () { return !done; },
299
300 function (cb) {
301 // emulator is running, now shell into it and check if it has booted
302 adb.shell(deviceId, 'cd /sdcard && echo "SDCARD READY"', function (err, output) {
303 if (!err && output.toString().split('\n').shift().trim() === 'SDCARD READY') {
304 done = true;
305 cb();
306 } else {
307 setTimeout(cb, retryTimeout);
308 }
309 });
310 },
311
312 function () {
313 var mounted = false,
314 mountPoints = [ '/sdcard', '/mnt/sdcard' ];
315
316 adb.shell(deviceId, 'ls -l /sdcard', function (err, output) {
317 if (!err) {
318 var m = output.toString().trim().split('\n').shift().trim().match(/-> (\S+)/);
319 if (m && mountPoints.indexOf(m[1]) === -1) {
320 mountPoints.unshift(m[1]);
321 }
322 }
323
324 opts.logger && opts.logger.debug(__('Checking mount points: %s', mountPoints.join(', ').cyan));
325
326 // wait for the sd card to be mounted
327 async.whilst(
328 function () { return !mounted; },
329
330 function (cb) {
331 adb.shell(deviceId, 'mount', function (err, output) {
332 if (!err && output.toString().trim().split('\n').some(function (line) {
333 var parts = line.trim().split(' ');
334 return parts.length > 1 && mountPoints.indexOf(parts[1]) !== -1;
335 })) {
336 mounted = true;
337 clearTimeout(sdcardTimer);
338 opts.logger && opts.logger.debug(__('SD card is mounted'));
339 cb();
340 } else {
341 setTimeout(cb, retryTimeout);
342 }
343 });
344 },
345
346 function () {
347 // requery the devices since device state may have changed
348 adb.devices(function (err, devices) {
349 emulib.isRunning(config, emu, devices.filter(d => d.id = emulator.id), function (err, running) {
350 if (!err && running) {
351 appc.util.mix(emulator, running);
352 }
353 emulator.emit('ready', emulator);
354 });
355 });
356 }
357 );
358 });
359 }
360 );
361
362 sdcardTimer = setTimeout(function () {
363 sdcardTimeout && emulator.emit('timeout', { type: 'sdcard', waited: sdcardTimeout });
364 done = true;
365 }, sdcardTimeout || 30000);
366 });
367}
368
369/**
370 * Starts the specified emulator, if not already running.
371 * @param {String} id - The id of the emulator
372 * @param {Object} [opts] - Options for detection and launching the emulator
373 * @param {Function} callback - A function to call when the emulator as launched
374 */
375EmulatorManager.prototype.start = function start(id, opts, callback) {
376 if (opts && typeof opts === 'function') {
377 callback = opts;
378 opts = {};
379 }
380
381 opts.logger && opts.logger.trace(__('Checking if emulator %s is running...', id.cyan));
382
383 this.isRunning(id, opts, function (err, running) {
384 if (err) {
385 // something went boom
386 return callback(err);
387 }
388
389 if (running) {
390 // already running
391 var emulator = new Emulator();
392 appc.util.mix(emulator, running);
393 opts.logger && opts.logger.info(__('Emulator already running'));
394 checkedBooted(this.config, opts, emulator);
395 callback(null, emulator);
396 return;
397 }
398
399 opts.logger && opts.logger.trace(__('Emulator not running, detecting emulator info'));
400
401 // not running, start the emulator
402 this.detect(opts, function (err, emus) {
403 if (err) {
404 return callback(err);
405 }
406
407 var emu = emus.filter(e => e && e.id == id).shift(); // eslint-disable-line eqeqeq
408
409 // this should never happen because it would have happened already thanks to isRunning()
410 if (!emu) {
411 return callback(new Error(__('Invalid emulator "%s"', id)), null);
412 }
413
414 opts.logger && opts.logger.trace(__('Starting the emulator...'));
415
416 var emulib = require(path.join(__dirname, 'emulators', emu.type + '.js'));
417 emulib.start(this.config, emu, opts, function (err, emulator) {
418 if (err) {
419 callback(err);
420 } else {
421 // give the emulator a second to get started before we start beating up adb
422 opts.logger && opts.logger.trace(__('Emulator is starting, monitoring boot state...'));
423 checkedBooted(this.config, opts, emulator);
424 callback(null, emulator);
425 }
426 }.bind(this));
427 }.bind(this));
428 }.bind(this));
429};
430
431/**
432 * Stops the specified emulator, if running.
433 * @param {String} id - The id of the emulator
434 * @param {Object} [opts] - Options for detection and killing the emulator
435 * @param {Function} callback - A function to call when the emulator as been killed
436 */
437EmulatorManager.prototype.stop = function stop(id, opts, callback) {
438 if (opts && typeof opts === 'function') {
439 callback = opts;
440 opts = {};
441 }
442
443 this.isRunning(id, opts, function (err, running) {
444 if (err) {
445 // something went boom
446 callback(err);
447 } else if (!running) {
448 // already stopped
449 callback(new Error(__('Emulator "%s" not running', id)));
450 } else {
451 require(path.join(__dirname, 'emulators', running.emulator.type + '.js')).stop(this.config, running.emulator.name, running, opts, callback);
452 }
453 }.bind(this));
454};