UNPKG

15.8 kBJavaScriptView Raw
1/**
2 * Detects Windows Phone emulators.
3 *
4 * @module emulator
5 *
6 * @copyright
7 * Copyright (c) 2014-2015 by Appcelerator, Inc. All Rights Reserved.
8 *
9 * @license
10 * Licensed under the terms of the Apache Public License.
11 * Please see the LICENSE included with this distribution for details.
12 */
13
14const
15 appc = require('node-appc'),
16 async = require('async'),
17 env = require('./env'),
18 fs = require('fs'),
19 magik = require('./utilities').magik,
20 path = require('path'),
21 proc = require('./process'),
22 spawn = require('child_process').spawn,
23 windowsphone = require('./windowsphone'),
24 wptool = require('./wptool'),
25 __ = appc.i18n(__dirname).__;
26
27var cache;
28
29exports.detect = detect;
30exports.isRunning = isRunning;
31exports.launch = launch;
32exports.install = install;
33exports.stop = stop;
34exports.status = status;
35
36/**
37 * Detects connected Windows Phone emulators.
38 *
39 * @param {Object} [options] - An object containing various settings.
40 * @param {Boolean} [options.bypassCache=false] - When true, re-detects all Windows Phone emulators.
41 * @param {Function} [callback(err, results)] - A function to call with the emulator information.
42 *
43 * @emits module:emulator#detected
44 * @emits module:emulator#error
45 *
46 * @returns {EventEmitter}
47 */
48function detect(options, callback) {
49 return magik(options, callback, function (emitter, options, callback) {
50 if (cache && !options.bypassCache) {
51 emitter.emit('detected', cache);
52 return callback(null, cache);
53 }
54
55 wptool.enumerate(options, function (err, results) {
56 if (err) {
57 emitter.emit('error', err);
58 return callback(err);
59 }
60
61 var result = {
62 emulators: {},
63 issues: []
64 };
65
66 if (!err) {
67 Object.keys(results).forEach(function (wpsdk) {
68 result.emulators[wpsdk] = results[wpsdk].emulators;
69 });
70
71 cache = result;
72 }
73
74 emitter.emit('detected', result);
75 callback(null, result);
76 });
77 });
78}
79
80/**
81 * Detects if the specified emulator is running.
82 *
83 * @param {String} udid - The UDID of the Windows Phone emulator to check if running.
84 * @param {Object} [options] - An object containing various settings.
85 * @param {Boolean} [options.bypassCache=false] - When true, re-detects all Windows Phone emulators.
86 * @param {String} [options.powershell] - Path to the 'powershell' executable.
87 * @param {Function} [callback(err, results)] - A function to call with the emulator information.
88 *
89 * @emits module:emulator#detected
90 * @emits module:emulator#error
91 *
92 * @returns {EventEmitter}
93 */
94function isRunning(udid, options, callback) {
95 return magik(options, callback, function (emitter, options, callback) {
96 env.detect(options)
97 .on('error', function (err) {
98 emitter.emit('error', err);
99 callback(err);
100 })
101 .on('detected', function (results) {
102
103 detect(options)
104 .on('error', function (err) {
105 emitter.emit('error', err);
106 callback(err);
107 })
108 .on('detected', function (results) {
109 var emu;
110 // find the emulator
111 function testDev(d) {
112 if (d.udid == udid) { // this MUST be == because the udid might be a number and not a string
113 emu = d;
114 return true;
115 }
116 };
117 Object.keys(results.emulators).some(function (wpsdk) {
118 return results.emulators[wpsdk].some(testDev);
119 });
120
121 if (!emu) {
122 var err = new Error(__('Invalid udid "%s"', udid));
123 emitter.emit('error', err);
124 return callback(err);
125 }
126
127 appc.subprocess.getRealName(path.resolve(__dirname, '..', 'bin', 'wp_emulator_status.ps1'), function (err, script) {
128 if (err) {
129 emitter.emit('error', err);
130 return callback(err);
131 }
132
133 appc.subprocess.run(options.powershell || 'powershell', [
134 '-ExecutionPolicy', 'Bypass', '-NoLogo', '-NonInteractive', '-NoProfile',
135 '-File',
136 script
137 ], function (code, out, err) {
138 if (code) {
139 var err = new Error(__('Failed to detect running emulators'));
140 emitter.emit('error', err);
141 return callback(err);
142 }
143
144 var re = new RegExp('^' + emu.name + '\\..*(\\d+)(?:\\s+\\d+)$', 'i'),
145 match = out.trim().split(/\r\n|\n/).map(function (line) {
146 line = line.trim();
147 return line && line.match(re);
148 }).filter(function (m) { return !!m; }).shift(),
149 running = match && ~~match[1] === 2 || false;
150
151 emitter.emit('running', running);
152 callback(null, running);
153 });
154 });
155 });
156 });
157 });
158}
159
160/**
161 * Launches the specified Windows Phone emulator or picks one automatically.
162 *
163 * @param {String} udid - The UDID of the Windows Phone emulator to launch or null if you want windowslib to pick one.
164 * @param {Object} [options] - An object containing various settings.
165 * @param {String} [options.appPath] - The path to the Windows Phone app to install after launching the Windows Phone Emulator.
166 * @param {String} [options.assemblyPath=%WINDIR%\Microsoft.NET\assembly\GAC_MSIL] - Path to .NET global assembly cache.
167 * @param {Boolean} [options.bypassCache=false] - When true, re-detects all emulators.
168 * @param {Boolean} [options.killIfRunning=false] - Kill the Windows Phone emulator if already running.
169 * @param {Object} [options.requiredAssemblies] - An object containing assemblies to check for in addition to the required windowslib dependencies.
170 * @param {Boolean} [options.skipLaunch] - Whether we should just install without launching.
171 * @param {Boolean} [options.tasklist] - The path to the 'tasklist' executable.
172 * @param {Number} [options.timeout] - Number of milliseconds to wait for the emulator to launch and launch the app before timing out. Must be at least 1 millisecond.
173 * @param {String} options.wpsdk - A specific Windows Phone version to query.
174 * @param {Function} [callback(err, emuHandle)] - A function to call when the emulator has launched.
175 *
176 * @emits module:emulator#error
177 * @emits module:emulator#launched
178 * @emits module:emulator#timeout
179 *
180 * @returns {EventEmitter}
181 */
182function launch(udid, options, callback) {
183 return magik(options, callback, function (emitter, options, callback) {
184 // detect emulators
185 detect(options, function (err, emuInfo) {
186 if (err) {
187 emitter.emit('error', err);
188 return callback(err);
189 }
190
191 var emuHandle;
192
193 if (udid) {
194 // validate the udid
195 Object.keys(emuInfo.emulators).filter(function (wpsdk) {
196 return !options.wpsdk || wpsdk === options.wpsdk;
197 }).some(function (wpsdk) {
198 return emuInfo.emulators[wpsdk].some(function (emu) {
199 if (emu.udid === udid) {
200 emuHandle = appc.util.mix({}, emu);
201 return true;
202 }
203 });
204 });
205
206 if (!emuHandle) {
207 err = new Error(__('Unable to find an Windows Phone emulator with the UDID "%s"', udid));
208 }
209 } else {
210 Object.keys(emuInfo.emulators).filter(function (wpsdk) {
211 return !options.wpsdk || wpsdk === options.wpsdk;
212 }).some(function (wpsdk) {
213 if (emuInfo.emulators[wpsdk].length) {
214 emuHandle = appc.util.mix({}, emuInfo.emulators[wpsdk][0]);
215 return true;
216 }
217 });
218
219 if (!emuHandle) {
220 // user experience!
221 if (options.wpsdk) {
222 err = new Error(__('Unable to find an Windows Phone %s emulator.', options.wpsdk));
223 } else {
224 err = new Error(__('Unable to find an Windows Phone emulator.'));
225 }
226 }
227 }
228
229 if (err) {
230 emitter.emit('error', err);
231 return callback(err);
232 }
233
234 if (options.appPath && !fs.existsSync(options.appPath)) {
235 err = new Error(__('App path does not exist: ' + options.appPath));
236 emitter.emit('error', err);
237 return callback(err);
238 }
239
240 emuHandle.startTime = Date.now();
241 emuHandle.running = false;
242
243 function launchEmulator() {
244 var timeout = options.timeout !== void 0 && Math.max(~~options.timeout, 1); // minimum of 1 millisecond
245 if (options.appPath) {
246 var mixedOptions = appc.util.mix({timeout: timeout}, options);
247 wptool.install(emuHandle, options.appPath, mixedOptions)
248 .on('error', function (err) {
249 emitter.emit('error', err);
250 callback(err);
251 }).on('installed', function () {
252 emuHandle.running = true;
253 emitter.emit('installed', emuHandle);
254 // If we're not going to launch, this is the end of the lifecycle here.
255 if (mixedOptions.skipLaunch) {
256 callback(null, emuHandle);
257 }
258 }).on('launched', function () {
259 emitter.emit('launched', emuHandle);
260 // Don't do callback until we launch if we're supposed to be launching
261 if (!mixedOptions.skipLaunch) {
262 callback(null, emuHandle);
263 }
264 }).on('timeout', function (err) {
265 err || (err = new Error(__('Timed out after %d milliseconds waiting to launch the emulator.', timeout)));
266 emitter.emit('timeout', err);
267 callback(err);
268 });
269 } else {
270 // not installing an app, just launch the emulator
271 wptool.connect(emuHandle.udid, {
272 timeout: timeout
273 }).on('error', function (err) {
274 emitter.emit('error', err);
275 callback(err);
276 }).on('connected', function (emu) {
277 emuHandle.ip = emu.ip; // copy IP address we got from connecting
278 emuHandle.running = emu.running || true;
279 emitter.emit('launched', emuHandle);
280 callback(null, emuHandle);
281 }).on('timeout', function (err) {
282 err || (err = new Error(__('Timed out after %d milliseconds waiting to launch the emulator.', timeout)));
283 emitter.emit('timeout', err);
284 callback(err);
285 });
286 }
287 }
288
289 if (options.killIfRunning) {
290 stop(emuHandle, options, launchEmulator);
291 } else {
292 launchEmulator();
293 }
294 });
295 });
296}
297
298/**
299 * Installs the specified app to an Windows Phone emulator. If the emulator is not running, it will launch it.
300 *
301 * @param {String} udid - The UDID of the emulator to install the app to or null if you want windowslib to pick one.
302 * @param {String} appPath - The path to the Windows Phone app to install.
303 * @param {Object} [options] - An object containing various settings.
304 * @param {Boolean} [options.bypassCache=false] - When true, re-detects the environment configuration.
305 * @param {Boolean} [options.skipLaunch=false] - When true, only installs the app, does not attempt to launch it.
306 * @param {Number} [options.timeout] - Number of milliseconds to wait before timing out.
307 * @param {Function} [callback(err)] - A function to call when the app is installed. To know when the app gets launched, hook an event listener for 'launched' event
308 *
309 * @emits module:emulator#error
310 * @emits module:emulator#installed
311 * @emits module:emulator#launched
312 *
313 * @returns {EventEmitter}
314 */
315function install(udid, appPath, options, callback) {
316 return magik(options, callback, function (emitter, options, callback) {
317 if (!appPath) {
318 var err = new Error(__('Missing app path argument'));
319 emitter.emit('error', err);
320 return callback(err);
321 }
322
323 var launchEmitter = launch(udid, appc.util.mix({
324 appPath: appPath
325 }, options), callback),
326 oldEmit = launchEmitter.emit;
327
328 launchEmitter.emit = function () {
329 oldEmit.apply(launchEmitter, arguments);
330 emitter.emit.apply(emitter, arguments);
331 };
332 });
333}
334
335/**
336 * Stops the specified Windows Phone emulator.
337 *
338 * @param {Object} emuHandle - The emulator handle.
339 * @param {Object} [options] - An object containing various settings.
340 * @param {String} [options.powershell] - Path to the 'powershell' executable.
341 * @param {Boolean} [options.tasklist] - The path to the 'tasklist' executable.
342 * @param {Function} [callback(err)] - A function to call when the emulator has quit.
343 *
344 * @emits module:emulator#error
345 * @emits module:emulator#stopped
346 *
347 * @returns {EventEmitter}
348 */
349function stop(emuHandle, options, callback) {
350 return magik(options, callback, function (emitter, options, callback) {
351 if (!emuHandle || typeof emuHandle !== 'object') {
352 var err = new Error(__('Invalid emulator handle argument'));
353 emitter.emit('error', err);
354 return callback(err);
355 }
356
357 // make sure the Windows Phone emulator has had some time to launch the emulator
358 setTimeout(function () {
359 proc.list(options, function (err, processes) {
360 if (err) {
361 emitter.emit('error', err);
362 return callback(err);
363 }
364
365 appc.subprocess.getRealName(path.resolve(__dirname, '..', 'bin', 'wp_stop_emulator.ps1'), function (err, psScript) {
366 if (err) {
367 emitter.emit('error', err);
368 return callback(err);
369 }
370
371 var xdeRegExp = /^xde\.exe$/i,
372 name = emuHandle.name.toLowerCase();
373
374 async.eachSeries(processes, function (p, next) {
375 if (!xdeRegExp.test(p.name) || p.title.toLowerCase() !== name) {
376 return next();
377 }
378 // first kill the emulator
379 process.kill(p.pid, 'SIGKILL'); // FIXME If we get ESRCH, can we assume the process died on it's own?
380
381 setTimeout(function () {
382 // next get hyper-v to think the emulator is not running
383 appc.subprocess.run(options.powershell || 'powershell', [
384 '-ExecutionPolicy', 'Bypass', '-NoLogo', '-NonInteractive', '-NoProfile',
385 '-File',
386 psScript,
387 '"' + emuHandle.name + '"'
388 ], function (code, out, err) {
389 try {
390 if (!code) {
391 var r = JSON.parse(out);
392 if (r.success) {
393 return next();
394 }
395 }
396 } catch (e) {
397 return next(e);
398 }
399 next(new Error('Failed to stop emulator.\r\nstdout: ' + out + '\r\nstderr: ' + err));
400 });
401 }, 1000);
402 }, function (err) {
403 if (err) {
404 emitter.emit('error', err);
405 return callback(err);
406 }
407
408 emuHandle.running = false;
409 emitter.emit('stopped');
410 callback();
411 });
412 });
413 });
414 }, Date.now() - emuHandle.startTime < 250 ? 250 : 0);
415 });
416}
417
418/**
419 * Retrieves the status of the specified Windows Phone emulator.
420 *
421 * @param {Object} emuHandle - The emulator handle.
422 * @param {Object} [options] - An object containing various settings.
423 * @param {String} [options.powershell] - Path to the 'powershell' executable.
424 * @param {Boolean} [options.tasklist] - The path to the 'tasklist' executable.
425 * @param {Function} [callback(err)] - A function to call when the emulator has quit.
426 *
427 * @returns {EventEmitter}
428 */
429function status(emuHandle, options, callback) {
430 return magik(options, callback, function (emitter, options, callback) {
431 if (!emuHandle || typeof emuHandle !== 'object') {
432 var err = new Error(__('Invalid emulator handle argument'));
433 emitter.emit('error', err);
434 return callback(err);
435 }
436
437 appc.subprocess.getRealName(path.resolve(__dirname, '..', 'bin', 'wp_emulator_enabledstate.ps1'), function (err, psScript) {
438 if (err) {
439 emitter.emit('error', err);
440 return callback(err);
441 }
442
443 // next get hyper-v to think the emulator is not running
444 appc.subprocess.run(options.powershell || 'powershell', [
445 '-ExecutionPolicy', 'Bypass', '-NoLogo', '-NonInteractive', '-NoProfile',
446 '-File',
447 psScript,
448 emuHandle.name
449 ], function (code, out, err) {
450 try {
451 if (!code) {
452 var result = JSON.parse(out);
453 if (result.success) {
454 return callback(null, result.state);
455 } else {
456 return callback(result.message);
457 }
458 }
459 } catch (e) {}
460 callback(new Error('Failed to retrieve emulator status'));
461 });
462 });
463 });
464}