UNPKG

36.3 kBJavaScriptView Raw
1/**
2 * Wrapper around the wptool command line tool for enumerating and connecting
3 * to Windows Phone devices and emulators.
4 *
5 * @module wptool
6 *
7 * @copyright
8 * Copyright (c) 2014-2016 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
15const
16 appc = require('node-appc'),
17 async = require('async'),
18 assemblies = require('./assemblies'),
19 DOMParser = require('xmldom').DOMParser,
20 fs = require('fs'),
21 magik = require('./utilities').magik,
22 checkOutdated = require('./utilities').checkOutdated,
23 emulator = require('./emulator'),
24 path = require('path'),
25 spawn = require('child_process').spawn,
26 visualstudio = require('./visualstudio'),
27 windowsphone = require('./windowsphone'),
28 wrench = require('wrench'),
29 wptool = path.resolve(__dirname, '..', 'bin', 'wptool.exe'),
30 __ = appc.i18n(__dirname).__,
31 os = require('os'),
32
33 // Temp build directory to build the wptool in if the path is too long
34 // to copy the .dll files into
35 tmpBuildDir = path.join(os.tmpdir(), 'appcelerator', 'wptool'),
36
37 // this is a hard coded list of emulators to detect.
38 // when the next windows phone is released, the enumerate()
39 // function will need to detect the new emulators.
40 wpsdks = ['8.0', '8.1', '10.0'],
41 PREFERRED_SDK = '10.0'; // ultimate fallback sdk version to use by default
42
43var cache;
44
45exports.enumerate = enumerate;
46exports.connect = connect;
47exports.install = install;
48exports.detect = detect;
49// expose some methods for unit testing
50exports.test = {
51 parseWinAppDeployCmdListing: parseWinAppDeployCmdListing,
52 parseAppDeployCmdListing: parseAppDeployCmdListing
53};
54
55
56/**
57 * Detects all Windows 10 Mobile devices using WinAppDeployCmd.exe
58 *
59 * @param {String} [deployCmd] - The full path to WinAppDeployCmd.exe
60 * @param {Function} [next(err, results)] - A function to call with the device information.
61 */
62function winAppDeployCmdEnumerate(deployCmd, next) {
63 var cmd = deployCmd,
64 args = ['devices', '2'], // TODO What timeout should we use here? Using 2 seconds for now, since I think wptool takes that long anyways
65 child = spawn(cmd, args),
66 out = '',
67 result;
68
69 child.stdout.on('data', function (data) {
70 out += data.toString();
71 });
72
73 child.stderr.on('data', function (data) {
74 out += data.toString();
75 });
76
77 child.on('close', function (code) {
78 if (code) {
79 var errmsg = out.trim().split(/\r\n|\n/).shift(),
80 ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to enumerate devices for WP SDK 10.0 (code %s)', code));
81 next(ex, null);
82 } else {
83 var devices = parseWinAppDeployCmdListing(out);
84 next(null, {
85 devices: devices,
86 emulators: []
87 });
88 }
89 });
90}
91
92/**
93 * @param {String} [deployCmd] - Device listing output from WinAppDeployCmd.exe
94 * @return {Array[Object]} - Array of devices
95 **/
96function parseWinAppDeployCmdListing(out) {
97 var deviceListingRE = /^((\d{1,3}\.){3}\d{1,3})\s+([0-9A-F]{8}[-]?([0-9A-F]{4}[-]?){3}[0-9A-F]{12})\s+(.+?)$/igm;
98 var devices = [];
99 var match,
100 i = 0;
101 while ((match = deviceListingRE.exec(out)) !== null)
102 {
103 // TODO How can we know what SDK is on the phone? My win 8.1U1 phone shows up in listings when connected via USB
104 devices.push({name: match[5], udid: match[3], index: i, wpsdk: null, ip: match[1], type: 'device'});
105 i++;
106 }
107
108 return devices;
109}
110
111/**
112 * Detects all Windows Phone devices and emulators using our custom tooling (wptool.exe)
113 *
114 * @param {String} [wpsdk] - The windows phone sdk version ('8.0', '8.1', '10.0').
115 * @param {Object} [options] - An object containing various settings.
116 * @param {Function} [next(err, results)] - A function to call with the device information.
117 */
118function wptoolEnumerate(wpsdk, options, next) {
119 function run(wpsdk, next) {
120 var child = spawn(wptool, ['enumerate', '--wpsdk', wpsdk]),
121 out = '',
122 result;
123
124 child.stdout.on('data', function (data) {
125 out += data.toString();
126 });
127
128 child.stderr.on('data', function (data) {
129 out += data.toString();
130 });
131
132 child.on('close', function (code) {
133 if (code) {
134 var errmsg = out.trim().split(/\r\n|\n/).shift(),
135 ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to enumerate devices/emulators for WP SDK %s (code %s)', wpsdk, code));
136 next(ex, null);
137 } else {
138 try {
139 next(null, JSON.parse(out));
140 } catch (E) {
141 next(null, {});
142 }
143 }
144 });
145 }
146
147 return windowsphone.detect(options, function (err, phoneResults) {
148 if (err) {
149 return next(err);
150 }
151
152 if (!phoneResults.windowsphone[wpsdk]) {
153 // Just move on if we have no results for a given version
154 return next(null, {devices:[],emulators:[]});
155 }
156
157 // device discovery is slower, do it in parallel with emulator discovery/listing
158 async.parallel([
159 // discover windows 10 devices in network using WinAppDeployCmd
160 function (cb) {
161 if (phoneResults.windowsphone[wpsdk].deployCmd) {
162 winAppDeployCmdEnumerate(phoneResults.windowsphone[wpsdk].deployCmd, cb);
163 } else {
164 cb(null, {devices: [],emulators: []});
165 }
166 },
167 // Use our custom wptool binary to gather Windows 10 emulators
168 function (cb) {
169 // TODO Handle when we don't have permissions to the folders in SDK and need to offload build to user HOME
170 var wpToolCs = path.resolve(__dirname, '..', 'wptool', 'wptool.cs');
171 checkOutdated(wpToolCs, wptool, function (err, outdated) {
172 if (err) {
173 return cb(err);
174 }
175 if (outdated) {
176 return buildWpTool(options, function (err, path) {
177 if (err) {
178 return cb(err);
179 }
180 run(wpsdk, cb);
181 });
182 }
183
184 run(wpsdk, cb);
185 });
186 }
187 ], function (err, results) {
188 if (err) {
189 return next(err);
190 }
191 // Combine devices and emulators listings!
192 var combined = results[1];
193 combined.devices = results[0].devices.concat(combined.devices);
194 return next(null, combined);
195 });
196 });
197}
198
199/**
200 * Parses the emulator listing from AppDeployCmd.exe
201 *
202 * @param {String} [out] - The raw string output from AppDeployCmd.exe
203 * @param {String} [wpsdk] - The windows phone sdk version ('8.0', '8.1', '10.0').
204 * @return {Array[Object]} - An array of the emulators detected
205 */
206function parseAppDeployCmdListing(out, wpsdk) {
207 // Parse the output! Hope this regex is OK!
208 var deviceListingRE = /^\s*(\d+)\s+([\w \.]+)/mg;
209 deviceListingRE.exec(out); // skip device
210 var emulators = [];
211 var match;
212 while ((match = deviceListingRE.exec(out)) !== null)
213 {
214 emulators.push({name: match[2], udid: wpsdk.replace('.', '-') + "-" + match[1], index: parseInt(match[1]), wpsdk: wpsdk, type: 'emulator'});
215 }
216
217 // TIMOB-19576
218 // Windows 10 Mobile Emulators are detected by 8.1 sdk,
219 // which can be used for both 8.1 and 10 project.
220 if (wpsdk != '8.0') {
221 // limit 8.1 or 10.0 emulators to those SDKs only
222 emulators = emulators.filter(function (e) {
223 return new RegExp("Emulator\ " + wpsdk).test(e.name);
224 });
225 // FIXME change the udids back if they don't start at 1? (If we have 8.1 and 10, the 8.1 emulators udids start at 8-1-7)
226 }
227 return emulators;
228}
229
230/**
231 * Detects all Windows Phone devices and emulators using the native tooling (AppDeployCmd.exe)
232 *
233 * @param {String} [wpsdk] - The windows phone sdk version ('8.0', '8.1', '10.0').
234 * @param {Object} [options] - An object containing various settings.
235 * @param {Function} [next(err, results)] - A function to call with the device information.
236 */
237function nativeEnumerate(wpsdk, options, next) {
238 return windowsphone.detect(options, function (err, phoneResults) {
239 if (err) {
240 return next(err, null);
241 }
242
243 if (!phoneResults.windowsphone[wpsdk]) {
244 // Just move on if we have no results for a given version
245 return next(null, {devices:[],emulators:[]});
246 }
247
248 if (!phoneResults.windowsphone[wpsdk].deployCmd) {
249 var ex = new Error(__('No deploy command found for WP SDK %s. Cannot enumerate devices.', wpsdk));
250 return next(ex, null);
251 }
252
253 var cmd = phoneResults.windowsphone[wpsdk].deployCmd,
254 args = ['/EnumerateDevices'],
255 child = spawn(cmd, args),
256 out = '',
257 result;
258
259 child.stdout.on('data', function (data) {
260 out += data.toString();
261 });
262
263 child.stderr.on('data', function (data) {
264 out += data.toString();
265 });
266
267 child.on('close', function (code) {
268 if (code) {
269 var errmsg = out.trim().split(/\r\n|\n/).shift(),
270 ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to enumerate devices/emulators for WP SDK %s (code %s)', wpsdk, code));
271 next(ex, null);
272 } else {
273 var emulators = parseAppDeployCmdListing(out, wpsdk);
274 next(null, {
275 devices: [{name: 'Device', udid: 0, index: 0, wpsdk: null, type: 'device'}],
276 emulators: emulators
277 });
278 }
279 });
280 });
281}
282
283/**
284 * Detects Windows Phone devices and emulators.
285 *
286 * @param {Object} [options] - An object containing various settings.
287 * @param {Boolean} [options.bypassCache=false] - When true, re-detects all Windows Phone devices.
288 * @param {Function} [callback(err, results)] - A function to call with the device/emulator information.
289 */
290function detect(options, callback) {
291 return enumerate(options, function (err, results) {
292 var result = {
293 emulators: {},
294 devices: [],
295 issues: []
296 },
297 tmp = {};
298
299 if (err && !results) {
300 // detected an error with no results
301 callback(err);
302 } else {
303 Object.keys(results).forEach(function (wpsdk) {
304 result.emulators[wpsdk] = results[wpsdk].emulators;
305 results[wpsdk].devices.forEach(function (dev) {
306 if (!tmp[dev.udid]) {
307 tmp[dev.udid] = result.devices.length+1;
308 result.devices.push(dev);
309 } else if (dev.wpsdk) {
310 result.devices[tmp[dev.udid]-1] = dev;
311 }
312 });
313 });
314 // If we have a device with udid of 0 and non-null wpsdk _and_
315 // we have a device with real udid we got from WinAppDeployCmd, combine the listings!
316 var wpsdkIndex = -1,
317 realDeviceIndex = -1;
318 for (var i = 0; i < result.devices.length; i++) {
319 var dev = result.devices[i];
320 if (dev.udid == 0 && dev.wpsdk) {
321 wpsdkIndex = i;
322 } else if (dev.udid != 0 && !dev.wpsdk) {
323 // now find with "real" device
324 realDeviceIndex = i;
325 }
326 if (wpsdkIndex != -1 && realDeviceIndex != -1) {
327 break;
328 }
329 };
330 if (wpsdkIndex != -1 && realDeviceIndex != -1) {
331 // set 'real' device wpsdk to the value we got from wptool binary
332 result.devices[realDeviceIndex].wpsdk = result.devices[wpsdkIndex].wpsdk;
333 // remove the wptool binary entry
334 result.devices.splice(wpsdkIndex, 1);
335 }
336 }
337
338 callback(null, result);
339 });
340}
341
342/**
343 * Detects all Windows Phone devices and emulators.
344 *
345 * @param {Object} [options] - An object containing various settings.
346 * @param {Boolean} [options.bypassCache=false] - When true, re-detects devices and emulators.
347 * @param {Function} [callback(err, results)] - A function to call with the device information.
348 *
349 * @emits module:wptool#detected
350 * @emits module:wptool#error
351 *
352 * @returns {EventEmitter}
353 */
354function enumerate(options, callback) {
355 return magik(options, callback, function (emitter, options, callback) {
356 if (cache && !options.bypassCache) {
357 emitter.emit('detected', cache);
358 return callback(null, cache);
359 }
360
361 function runTool() {
362 var results = {},
363 errors = [],
364 toDetect;
365 if (options.supportedWindowsPhoneSDKVersions) {
366 toDetect = wpsdks.filter(ver => appc.version.satisfies(ver, options.supportedWindowsPhoneSDKVersions, false));
367 } else {
368 toDetect = wpsdks;
369 }
370 // wpsdks is a constant above that contains all supported Windows Phone SDK versions
371 async.eachSeries(toDetect, function (wpsdk, next) {
372 // Use custom wptool for 10.0 and 8.1, use native tooling for 8.0
373 var funcToCall = (wpsdk == '10.0') ? wptoolEnumerate : nativeEnumerate;
374 funcToCall(wpsdk, options, function (err, result) {
375 if (err) {
376 // If there was an error, move on, but record error.
377 // Then later if we have no results for any version, we propagate the error
378 if (!results[wpsdk]) {
379 results[wpsdk] = {
380 devices: [],
381 emulators: [],
382 };
383 }
384 errors.push(err);
385 next();
386 } else {
387 results[wpsdk] = result;
388 next();
389 }
390 });
391 }, function (err) {
392 if (err) {
393 emitter.emit('error', err);
394 return callback(err);
395 }
396 // If there are no emulators for either version, surface the first error
397 if (errors.length > 0 && !Object.keys(results).some(function (wpsdk) {
398 return results[wpsdk].emulators.length > 0;
399 })) {
400 emitter.emit('error', errors[0]);
401 return callback(errors[0], results);
402 }
403
404 // add a helper function to get a device by udid
405 Object.defineProperty(results, 'getByUdid', {
406 value: function (udid) {
407 var dev = null;
408
409 function testDev(d) {
410 if (d.udid == udid) { // this MUST be == because the udid might be a number and not a string
411 dev = d;
412 return true;
413 }
414 }
415
416 Object.keys(results).some(function (wpsdk) {
417 return results[wpsdk].devices.some(testDev) || results[wpsdk].emulators.some(testDev);
418 });
419
420 return dev;
421 }
422 });
423
424 cache = results;
425 emitter.emit('detected', cache);
426 callback(null, cache);
427 });
428 }
429
430 runTool();
431 });
432}
433
434/**
435 * Connects to a Windows Phone device or launches a Windows Phone emulator.
436 *
437 * @param {String} udid - The device or emulator udid.
438 * @param {Object} [options] - An object containing various settings.
439 * @param {Boolean} [options.bypassCache=false] - When true, re-detects devices and emulators.
440 * @param {String} [options.assemblyPath=%WINDIR%\Microsoft.NET\assembly\GAC_MSIL] - Path to .NET global assembly cache.
441 * @param {Object} [options.requiredAssemblies] - An object containing assemblies to check for in addition to the required windowslib dependencies.
442 * @param {Number} [options.timeout] - The number of milliseconds to wait before timing out.
443 * @param {Function} [callback(err, handle)] - A function to call after attempting to connnect to the device/emulator.
444 *
445 * @emits module:wptool#connected
446 * @emits module:wptool#error
447 * @emits module:wptool#timeout
448 *
449 * @returns {EventEmitter}
450 */
451function connect(udid, options, callback) {
452 return magik(options, callback, function (emitter, options, callback) {
453 if (udid === null || udid === void 0) {
454 var ex = new Error(__('Missing required "%s" argument', 'udid'));
455 emitter.emit('error', ex);
456 return callback(ex);
457 }
458
459 enumerate(options)
460 .on('error', function (err) {
461 emitter.emit('error', err);
462 callback(err);
463 })
464 .on('detected', function (results) {
465 // validate the udid
466 var dev = results.getByUdid(udid);
467
468 if (!dev) {
469 var err = new Error(__('Invalid udid "%s"', udid));
470 emitter.emit('error', err);
471 return callback(err);
472 }
473 // TODO if we have win 10 use it's deploy tool to push to devices?
474 var wpsdk = dev.wpsdk || options.wpsdk || options.preferredWindowsPhoneSDK || PREFERRED_SDK,
475 done = function (err, result) {
476 if (err) {
477 emitter.emit(result || 'error', err);
478 return callback(err);
479 }
480 emitter.emit('connected', result); // send along device info we have
481 callback(null, result);
482 };
483
484 if (wpsdk == '10.0') {
485 // if win10, just call connect on our native tool!
486 wpToolConnect(dev, options, done);
487 } else {
488 // If win 8.x, launch bogus app
489 nativeLaunch(dev, 'f8ce6878-0aeb-497f-bcf4-65be961d4bba', options, done);
490 }
491 });
492 });
493}
494
495/**
496 * Builds our own custom tool to interact with emulators and devices.
497 *
498 * @param {Object} [options] - An object containing various settings.
499 * @param {String} [options.assemblyPath=%WINDIR%\Microsoft.NET\assembly\GAC_MSIL] - Path to .NET global assembly cache.
500 * @param {Object} [options.requiredAssemblies] - An object containing assemblies to check for in addition to the required windowslib dependencies.
501 * @param {Function} [callback(err, path)] - A function to call after building the executable.
502 */
503function buildWpTool(options, callback) {
504 // FIXME Handle when we don't have permission to edit the existing csproj or copy to the bin dir!
505 // We should move to a writable directory under HOME and return path to that
506
507 // find required assemblies
508 return assemblies.detect(options, function (err, results) {
509 if (err) {
510 return callback(err);
511 }
512
513 // check that we have the assemblies we need
514 var requiredAssemblies = {
515 'Microsoft.SmartDevice.Connectivity.Interface': null,
516 'Microsoft.SmartDevice.MultiTargeting.Connectivity': null
517 },
518 missing = Object.keys(requiredAssemblies).filter(function (assembly) {
519 var r = results.assemblies[assembly];
520 if (!r) return true;
521 requiredAssemblies[assembly] = r[Object.keys(r).sort().pop()];
522 });
523
524 if (missing.length) {
525 var ex = new Error(__('Missing one or more required Microsoft .NET assemblies: %s', missing.join(', ')));
526 return callback(ex);
527 }
528
529 // update visual studio references
530 var project = path.resolve(__dirname, '..', 'wptool', 'wptool.csproj'),
531 parser = new DOMParser({ errorHandler: function () {} }),
532 dom = parser.parseFromString(fs.readFileSync(project).toString(), 'text/xml');
533
534 (function updateRefs(node) {
535 while (node) {
536 if (node.nodeType === appc.xml.ELEMENT_NODE) {
537 switch (node.tagName) {
538 case 'Reference':
539 var inc = node.getAttribute('Include');
540 if (inc) {
541 var name = inc.split(',').shift();
542
543 if (requiredAssemblies[name]) {
544 node.setAttribute('Include', name + ', Version=' + requiredAssemblies[name].assemblyVersion + ', Culture=neutral, PublicKeyToken=' + requiredAssemblies[name].publicKeyToken + ', processorArchitecture=MSIL');
545
546 var child = node.firstChild,
547 found = false;
548
549 while (child) {
550 if (child.nodeType === appc.xml.ELEMENT_NODE && child.tagName === 'HintPath') {
551 while (child.firstChild) {
552 child.removeChild(child.firstChild);
553 }
554 child.appendChild(dom.createTextNode(requiredAssemblies[name].assemblyFile));
555 found = true;
556 break;
557 }
558 child = child.nextSibling;
559 }
560
561 if (!found) {
562 child = dom.createElement('HintPath');
563 child.appendChild(dom.createTextNode(requiredAssemblies[name].assemblyFile));
564 node.appendChild(child);
565 }
566 }
567 }
568 break;
569 default:
570 updateRefs(node.firstChild);
571 }
572 }
573 node = node.nextSibling;
574 }
575 }(dom.documentElement.firstChild));
576
577 fs.writeFileSync(project, '<?xml version="1.0" encoding="UTF-8"?>\n' + dom.documentElement.toString());
578
579 // remove the bin and obj folders
580 var d;
581 fs.existsSync(d = path.resolve(__dirname, '..', 'wptool', 'bin')) && wrench.rmdirSyncRecursive(d);
582 fs.existsSync(d = path.resolve(__dirname, '..', 'wptool', 'obj')) && wrench.rmdirSyncRecursive(d);
583
584 var pathLength = 0;
585 for(var assembly of Object.keys(requiredAssemblies)) {
586 var assemblyPath = path.join(__dirname,'wptool', 'bin', 'Release' , assembly);
587 if (assemblyPath.length > pathLength) {
588 pathLength = assemblyPath.length;
589 }
590 }
591 if (pathLength >= 260) {
592 if (!fs.existsSync(tmpBuildDir)) {
593 wrench.mkdirSyncRecursive(tmpBuildDir);
594 }
595 wrench.copyDirSyncRecursive(path.join(__dirname, '..', 'wptool'), tmpBuildDir, {
596 forceDelete: true
597 });
598 project = path.join(tmpBuildDir, 'wptool.csproj');
599 }
600 // build the wptool
601 visualstudio.build(appc.util.mix({
602 buildConfiguration: 'Release',
603 project: project
604 }, options), function (err, result) {
605 if (err) {
606 return callback(err);
607 }
608
609 var src = path.resolve(path.dirname(project), 'bin', 'Release', 'wptool.exe');
610
611 if (!fs.existsSync(src)) {
612 var ex = new Error(__('Failed to build the wptool executable.'));
613 return callback(ex);
614 }
615
616 // Always copy wptool.exe whenever we build it
617 fs.writeFileSync(wptool, fs.readFileSync(src));
618
619 // Make sure to copy all dependencies
620 var srcdir = path.resolve(path.dirname(project), 'bin', 'Release');
621 fs.readdirSync(srcdir).forEach(function(filename) {
622 if (path.extname(filename) == '.dll') {
623 var dest = path.resolve(__dirname, '..', 'bin', filename);
624 fs.writeFileSync(dest, fs.readFileSync(path.resolve(srcdir, filename)));
625 }
626 });
627
628 return callback(null, wptool);
629 });
630 });
631}
632
633function wpToolConnect(device, options, callback) {
634 var args = [
635 'connect',
636 device.index
637 ],
638 child = spawn(wptool, args),
639 out = '',
640 abortTimer,
641 timedOut = false;
642
643 child.stdout.on('data', function (data) {
644 out += data.toString();
645 });
646
647 child.stderr.on('data', function (data) {
648 out += data.toString();
649 });
650
651 child.on('close', function (code) {
652 clearTimeout(abortTimer);
653
654 try {
655 var result = JSON.parse(out),
656 pollEmulator;
657
658 if (!result.success) {
659 clearTimeout(abortTimer);
660 return callback(new Error(__('Failed to connect to %s', device.name)));
661 }
662 device.ip = result.ip;
663 // If this is an emulator we should poll the status and wait until it's 'Running' before moving on
664 // I'm seeing consistent failures to install an app on Windows 10 emulators in our builds here.
665 if (device.type == 'emulator') {
666 pollEmulator = function() {
667 if (timedOut) {
668 return;
669 }
670
671 // check if emulator is running...
672 emulator.status(device, function(err, status) {
673 if (err) {
674 clearTimeout(abortTimer);
675 return callback(err);
676 }
677
678 if (status == 2) { // running state
679 clearTimeout(abortTimer);
680 device.running = true; // mark it as running so we don't try and launch it again via connect
681 callback(null, device);
682 } else {
683 // try again in 500ms
684 setTimeout(pollEmulator, 500);
685 }
686 });
687 };
688 // wait 250ms and check status of emulator
689 setTimeout(pollEmulator, 250);
690 } else {
691 // It's a device, just assume we're ok
692 clearTimeout(abortTimer);
693 device.running = true; // mark it as running so we don't try and launch it again via connect
694 callback(null, device);
695 }
696 } catch (e) {
697 clearTimeout(abortTimer);
698 callback(new Error(__('Failed to connect to %s', device.name)));
699 }
700 });
701 if (options.timeout) {
702 abortTimer = setTimeout(function () {
703 timedOut = true; // set flag so we don't poll on emulator state change
704 child.kill();
705
706 var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.name));
707 callback(ex, 'timeout');
708 }, options.timeout);
709 }
710}
711
712/**
713 * Launches an app on a given emulator/device.
714 **/
715function wpToolLaunch(device, productGuid, options, callback) {
716 var args = [
717 'launch',
718 device.index,
719 productGuid
720 ],
721 child = spawn(wptool, args),
722 out = '',
723 abortTimer;
724
725 child.stdout.on('data', function (data) {
726 out += data.toString();
727 });
728
729 child.stderr.on('data', function (data) {
730 out += data.toString();
731 });
732
733 child.on('close', function (code) {
734 clearTimeout(abortTimer);
735
736 try {
737 var result = JSON.parse(out);
738 if (result.success) {
739 return callback(null, device);
740 }
741 var ex = new Error(__('Failed to launch app: %s', result.message));
742 callback(ex);
743 } catch (e) {
744 var ex = new Error(__('Failed to connect to emulator: %s', out));
745 callback(ex);
746 }
747 });
748 if (options.timeout) {
749 abortTimer = setTimeout(function () {
750 child.kill();
751
752 var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.type));
753 callback(ex, 'timeout');
754 }, options.timeout);
755 }
756}
757
758function nativeLaunch(device, appid, options, callback) {
759 windowsphone.detect(options, function (err, phoneResults) {
760 if (err) {
761 return callback(err);
762 }
763
764 var wpsdk = device.wpsdk || options.wpsdk || options.preferredWindowsPhoneSDK || PREFERRED_SDK,
765 deployCmd = phoneResults.windowsphone[wpsdk].deployCmd,
766 args = [
767 '/launch',
768 appid,
769 '/targetdevice:' + device.index
770 ],
771 child,
772 out = '',
773 abortTimer;
774 if (!deployCmd) {
775 var ex = new Error(__('Windows Phone SDK v%s does not appear to have an App deploy tool.', wpsdk));
776 return callback(ex);
777 }
778
779 child = spawn(deployCmd, args);
780
781 child.stdout.on('data', function (data) {
782 out += data.toString();
783 });
784
785 child.stderr.on('data', function (data) {
786 out += data.toString();
787 });
788
789 child.on('close', function (code) {
790 clearTimeout(abortTimer);
791
792 var errmsg = out.trim().split(/\r\n|\n/).shift(),
793 ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to start %s (code %s)', device.name, code));
794 // Here's where we expect the failure that the app is not installed, which is right.
795 // We're explicitly telling to launch a bogus app, so we expect a very specific failure as "success" here...
796 // if (code == -2146233088 || code == 2148734208)
797 if (errmsg == '' || errmsg.indexOf('The application is not installed.') != -1) {
798 // we must be successful, right?
799 callback(null, device);
800 } else {
801 // we sometimes get the same code, but different error message
802 callback(ex);
803 }
804 });
805 if (options.timeout) {
806 abortTimer = setTimeout(function () {
807 child.kill();
808
809 var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.type));
810 callback(ex, 'timeout');
811 }, options.timeout);
812 }
813 });
814}
815
816function nativeInstall(deployCmd, device, appPath, options, callback) {
817 // We're explicitly telling to launch a bogus app, so we expect a very specific failure as "success" here...
818 var args = [
819 options.skipLaunch ? '/install' : '/installlaunch',
820 appPath,
821 '/targetdevice:' + device.index
822 ],
823 child = spawn(deployCmd, args),
824 out = '',
825 abortTimer;
826
827 child.stdout.on('data', function (data) {
828 out += data.toString();
829 });
830
831 child.stderr.on('data', function (data) {
832 out += data.toString();
833 });
834
835 child.on('close', function (code) {
836 clearTimeout(abortTimer);
837
838 if (out.trim() != '' && code) {
839 var errmsg = out.trim().split(/\r\n|\n/).shift(),
840 ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to install app (code %s)', code));
841 callback(ex);
842 } else {
843 device.running = true;
844 callback(null, device);
845 }
846 });
847 if (options.timeout) {
848 abortTimer = setTimeout(function () {
849 child.kill();
850
851 var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.type));
852 callback(ex, 'timeout');
853 }, options.timeout);
854 }
855}
856
857/**
858 * Installs an app on a device/emulator, via WinAppDeployCmd. If necessary we'll
859 * first launch the emulator and grab the IP address before installing.
860 *
861 * @param {String} [deployCmd] - Path to WinAppDeployCmd.exe
862 * @param {Object} [device] - The windows phone device or emulator.
863 * @param {String} [appPath] - Path to the appx, xap or appxbundle to install.
864 * @param {Object} [options] - An object containing various settings.
865 * @param {Function} [callback(err, device)] - A function to call with the device information.
866 */
867function wpToolInstall(deployCmd, device, appPath, options, callback) {
868 if (!device.ip || !device.running) {
869 // Launch the emulator, grab the IP and mark as running then install the app
870 return wpToolConnect(device, options, function(err, dev) {
871 if (err) {
872 return callback(err);
873 }
874
875 wpToolInstall(deployCmd, dev, appPath, options, callback);
876 });
877 }
878
879 var args = [
880 options.forceUnInstall ? 'uninstall' : 'install',
881 '-file',
882 appPath,
883 '-ip',
884 device.ip
885 ],
886 child = spawn(deployCmd, args),
887 out = '',
888 abortTimer;
889
890 child.stdout.on('data', function (data) {
891 out += data.toString();
892 });
893
894 child.stderr.on('data', function (data) {
895 out += data.toString();
896 });
897
898 // TODO If the app install fails because it needs a pin, we should help guide the user. Prompt for pin, or spit out a message
899 // telling them how to install manually and pair once with pin?
900 child.on('close', function (code) {
901 clearTimeout(abortTimer);
902
903 if (code) {
904 // handle duplicate package identity error code from Windows 10.0.14393 tooling and above
905 if (code == '2148734208') {
906 if (out.indexOf('0x80131500 - Failed to install or update package: Unspecified error') != -1) {
907 // handle duplicate package identity error code from Windows 10.0.14393 tooling and above
908 callback(new Error('A debug application is already installed, please remove existing debug application.'));
909 } else if (out.indexOf('because the current user does not have that package installed') == -1) {
910 if (options.forceUnInstall) {
911 wpToolInstall(deployCmd, device, appPath, options, callback);
912 } else {
913 callback(new Error('A debug application is already installed. Please increment the version number of the application, or use forceUnInstall option to explicitly delete existing app.'));
914 }
915 } else {
916 // Windows cannot remove the app because the current user does not have that package installed.
917 options.forceUnInstall = false;
918 wpToolInstall(deployCmd, device, appPath, options, callback);
919 }
920 } else {
921 var errmsg = out.trim().split(/\r\n|\n/).shift(),
922 ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to install app (code %s): %s', code, out));
923 callback(ex);
924 }
925 } else {
926 var errmsg = /failed\. (\w*)\r?\n(.*)/.exec(out);
927 if (errmsg) {
928 var err = errmsg[1],
929 msg = errmsg[2];
930
931 if (err == '0x80073CF9') {
932 callback(new Error('A debug application is already installed, please remove existing debug application'));
933 } else if (err == '0x80073CFB') {
934 if (options.forceUnInstall) {
935 // Provided package has the same identity as an already-installed package. Proceed uninstalling.
936 wpToolInstall(deployCmd, device, appPath, options, callback);
937 } else {
938 callback(new Error('A debug application is already installed. Please increment the version number of the application, or use forceUnInstall option to explicitly delete existing app.'));
939 }
940 } else {
941 callback(new Error(__('Failed to install app (code %s): %s', err, msg)));
942 }
943 } else {
944 // Provided package is uninstalled...proceed re-installing.
945 if (options.forceUnInstall) {
946 options.forceUnInstall = false;
947 wpToolInstall(deployCmd, device, appPath, options, callback);
948 } else {
949 callback(null, device);
950 }
951 }
952 }
953 });
954 if (options.timeout) {
955 abortTimer = setTimeout(function () {
956 child.kill();
957
958 var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.type));
959 callback(ex, 'timeout');
960 }, options.timeout);
961 }
962
963}
964
965/**
966 * Installs an app on a device/emulator, defaults to launching it as well.
967 *
968 * @param {Object} [device] - The windows phonedevice or emulator.
969 * @param {String} [appPath] - Path to the appx, xap or appxbundle to install.
970 * @param {Object} [options] - An object containing various settings.
971 * @param {Object} [options.skipLaunch] - Just install the app, don't launch it too.
972 * @param {Object} [options.appGuid] - The generated app guid. May be null/empty, if so we'll try to detect it
973 * @param {String} [options.powershell] - Path to the 'powershell' executable.
974 * @param {Function} [callback(err, results)] - A function to call with the device information once the app is installed. To know when the app gets launched, hook an event listener for 'launched' event
975 *
976 * @emits module:wptool#error
977 * @emits module:wptool#timeout
978 * @emits module:wptool#installed
979 * @emits module:wptool#launched
980 *
981 * @returns {EventEmitter}
982 */
983function install(device, appPath, options, callback) {
984 return magik(options, callback, function (emitter, options, callback) {
985 windowsphone.detect(options, function (err, phoneResults) {
986 if (err) {
987 emitter.emit('error', err);
988 return callback(err);
989 }
990
991 var wpsdk = device.wpsdk || options.wpsdk || options.preferredWindowsPhoneSDK || PREFERRED_SDK,
992 cmd = phoneResults.windowsphone[wpsdk].deployCmd;
993 if (!cmd) {
994 var ex = new Error(__('Windows Phone SDK v%s does not appear to have an App deploy tool.', wpsdk));
995 return callback(ex);
996 }
997
998 if (wpsdk == '10.0') {
999 if (!options.skipLaunch) {
1000 var guid;
1001 // we need the appid to launch, so install the app and get the app id in parallel
1002 async.parallel([
1003 function (next) {
1004 wpToolInstall(cmd, device, appPath, options, function (err, result) {
1005 if (err) {
1006 emitter.emit(result || 'error', err);
1007 return next(err);
1008 }
1009 emitter.emit('installed', device);
1010 next();
1011 });
1012 },
1013 function (next) {
1014 if (options.appGuid) {
1015 guid = options.appGuid;
1016 next();
1017 } else {
1018 getProductGUID(appPath, options, function(err, productGuid) {
1019 if (err) {
1020 emitter.emit('error', err);
1021 return next(err);
1022 }
1023
1024 guid = productGuid;
1025 next();
1026 });
1027 }
1028 }
1029 ], function (err, results) {
1030 if (err) {
1031 return callback(err);
1032 }
1033 // now launch it!
1034 wpToolLaunch(device, guid, options, function (err, result) {
1035 if (err) {
1036 emitter.emit(result || 'error', err);
1037 return callback(err);
1038 }
1039 emitter.emit('launched', device);
1040 callback(null, result);
1041 });
1042 });
1043 } else {
1044 // We're just installing. No need to grab appid or launch the app
1045 wpToolInstall(cmd, device, appPath, options, function (err, result) {
1046 if (err) {
1047 emitter.emit(result || 'error', err);
1048 return callback(err);
1049 }
1050
1051 emitter.emit('installed', device);
1052 return callback(null, result);
1053 });
1054 }
1055 } else {
1056 nativeInstall(cmd, device, appPath, options, function (err, result) {
1057 if (err) {
1058 emitter.emit(result || 'error', err);
1059 return callback(err);
1060 }
1061 emitter.emit('installed', device);
1062 if (!options.skipLaunch) {
1063 emitter.emit('launched', device);
1064 }
1065 callback(null, result);
1066 });
1067 }
1068 });
1069 });
1070}
1071
1072/**
1073 * Unzips an appx file to read the AppxManifest.xml and grab the product guid
1074 * out (so we know the guid we need to launch it)
1075 *
1076 * @param {String} [appxFile] - Path to the appx, xap or appxbundle to inspect.
1077 * @param {Object} [options] - An object containing various settings.
1078 * @param {String} [options.powershell] - Path to the 'powershell' executable.
1079 * @param {Function} [callback(err, results)] - A function to call with the GUID
1080 */
1081function getProductGUID(appxFile, options, callback) {
1082 appc.subprocess.getRealName(path.resolve(__dirname, '..', 'bin', 'wp_get_appx_metadata.ps1'), function (err, script) {
1083 if (err) {
1084 return callback(err);
1085 }
1086
1087 appc.subprocess.run(options.powershell || 'powershell', [
1088 '-ExecutionPolicy', 'Bypass', '-NoLogo', '-NonInteractive', '-NoProfile',
1089 '-File',
1090 script,
1091 appxFile
1092 ], function (code, out, err) {
1093 if (code) {
1094 var ex = new Error(__('Failed to detect product id of appx: %s', out));
1095 return callback(ex);
1096 }
1097
1098 callback(null, out.trim());
1099 });
1100 });
1101}