windowslib
Version:
Windows Phone Utility Library
1,105 lines (995 loc) • 36.5 kB
JavaScript
/**
* Wrapper around the wptool command line tool for enumerating and connecting
* to Windows Phone devices and emulators.
*
* @module wptool
*
* @copyright
* Copyright (c) 2014-2016 by Appcelerator, Inc. All Rights Reserved.
*
* @license
* Licensed under the terms of the Apache Public License.
* Please see the LICENSE included with this distribution for details.
*/
const
appc = require('node-appc'),
async = require('async'),
assemblies = require('./assemblies'),
DOMParser = require('xmldom').DOMParser,
fs = require('fs'),
magik = require('./utilities').magik,
checkOutdated = require('./utilities').checkOutdated,
emulator = require('./emulator'),
path = require('path'),
spawn = require('child_process').spawn,
visualstudio = require('./visualstudio'),
windowsphone = require('./windowsphone'),
wrench = require('wrench'),
wptool = path.resolve(__dirname, '..', 'bin', 'wptool.exe'),
__ = appc.i18n(__dirname).__,
os = require('os'),
// Temp build directory to build the wptool in if the path is too long
// to copy the .dll files into
tmpBuildDir = path.join(os.tmpdir(), 'appcelerator', 'wptool'),
// this is a hard coded list of emulators to detect.
// when the next windows phone is released, the enumerate()
// function will need to detect the new emulators.
wpsdks = ['8.0', '8.1', '10.0'],
PREFERRED_SDK = '10.0'; // ultimate fallback sdk version to use by default
var cache;
exports.enumerate = enumerate;
exports.connect = connect;
exports.install = install;
exports.detect = detect;
// expose some methods for unit testing
exports.test = {
parseWinAppDeployCmdListing: parseWinAppDeployCmdListing,
parseAppDeployCmdListing: parseAppDeployCmdListing
};
/**
* Detects all Windows 10 Mobile devices using WinAppDeployCmd.exe
*
* @param {String} [deployCmd] - The full path to WinAppDeployCmd.exe
* @param {Function} [next(err, results)] - A function to call with the device information.
*/
function winAppDeployCmdEnumerate(deployCmd, next) {
var cmd = deployCmd,
args = ['devices', '2'], // TODO What timeout should we use here? Using 2 seconds for now, since I think wptool takes that long anyways
child = spawn(cmd, args),
out = '',
result;
child.stdout.on('data', function (data) {
out += data.toString();
});
child.stderr.on('data', function (data) {
out += data.toString();
});
child.on('close', function (code) {
if (code) {
var errmsg = out.trim().split(/\r\n|\n/).shift(),
ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to enumerate devices for WP SDK 10.0 (code %s)', code));
next(ex, null);
} else {
var devices = parseWinAppDeployCmdListing(out);
next(null, {
devices: devices,
emulators: []
});
}
});
}
/**
* @param {String} [deployCmd] - Device listing output from WinAppDeployCmd.exe
* @return {Array[Object]} - Array of devices
**/
function parseWinAppDeployCmdListing(out) {
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;
var devices = [];
var match,
i = 0;
while ((match = deviceListingRE.exec(out)) !== null)
{
// TODO How can we know what SDK is on the phone? My win 8.1U1 phone shows up in listings when connected via USB
devices.push({name: match[5], udid: match[3], index: i, wpsdk: null, ip: match[1], type: 'device'});
i++;
}
return devices;
}
/**
* Detects all Windows Phone devices and emulators using our custom tooling (wptool.exe)
*
* @param {String} [wpsdk] - The windows phone sdk version ('8.0', '8.1', '10.0').
* @param {Object} [options] - An object containing various settings.
* @param {Function} [next(err, results)] - A function to call with the device information.
*/
function wptoolEnumerate(wpsdk, options, next) {
function run(wpsdk, next) {
var child = spawn(wptool, ['enumerate', '--wpsdk', wpsdk]),
out = '',
result;
child.stdout.on('data', function (data) {
out += data.toString();
});
child.stderr.on('data', function (data) {
out += data.toString();
});
child.on('close', function (code) {
if (code) {
var errmsg = out.trim().split(/\r\n|\n/).shift(),
ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to enumerate devices/emulators for WP SDK %s (code %s)', wpsdk, code));
next(ex, null);
} else {
try {
next(null, JSON.parse(out));
} catch (E) {
next(null, {});
}
}
});
}
return windowsphone.detect(options, function (err, phoneResults) {
if (err) {
return next(err);
}
if (!phoneResults.windowsphone[wpsdk]) {
// Just move on if we have no results for a given version
return next(null, {devices:[],emulators:[]});
}
// device discovery is slower, do it in parallel with emulator discovery/listing
async.parallel([
// discover windows 10 devices in network using WinAppDeployCmd
function (cb) {
if (phoneResults.windowsphone[wpsdk].deployCmd) {
winAppDeployCmdEnumerate(phoneResults.windowsphone[wpsdk].deployCmd, cb);
} else {
cb(null, {devices: [],emulators: []});
}
},
// Use our custom wptool binary to gather Windows 10 emulators
function (cb) {
// TODO Handle when we don't have permissions to the folders in SDK and need to offload build to user HOME
var wpToolCs = path.resolve(__dirname, '..', 'wptool', 'wptool.cs');
checkOutdated(wpToolCs, wptool, function (err, outdated) {
if (err) {
return cb(err);
}
if (outdated) {
return buildWpTool(options, function (err, path) {
if (err) {
return cb(err);
}
run(wpsdk, cb);
});
}
run(wpsdk, cb);
});
}
], function (err, results) {
if (err) {
return next(err);
}
// Combine devices and emulators listings!
var combined = results[1];
combined.devices = results[0].devices.concat(combined.devices);
return next(null, combined);
});
});
}
/**
* Parses the emulator listing from AppDeployCmd.exe
*
* @param {String} [out] - The raw string output from AppDeployCmd.exe
* @param {String} [wpsdk] - The windows phone sdk version ('8.0', '8.1', '10.0').
* @return {Array[Object]} - An array of the emulators detected
*/
function parseAppDeployCmdListing(out, wpsdk) {
// Parse the output! Hope this regex is OK!
var deviceListingRE = /^\s*(\d+)\s+([\w \.]+)/mg;
deviceListingRE.exec(out); // skip device
var emulators = [];
var match;
while ((match = deviceListingRE.exec(out)) !== null)
{
emulators.push({name: match[2], udid: wpsdk.replace('.', '-') + "-" + match[1], index: parseInt(match[1]), wpsdk: wpsdk, type: 'emulator'});
}
// TIMOB-19576
// Windows 10 Mobile Emulators are detected by 8.1 sdk,
// which can be used for both 8.1 and 10 project.
if (wpsdk != '8.0') {
// limit 8.1 or 10.0 emulators to those SDKs only
emulators = emulators.filter(function (e) {
return new RegExp("Emulator\ " + wpsdk).test(e.name);
});
// 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)
}
return emulators;
}
/**
* Detects all Windows Phone devices and emulators using the native tooling (AppDeployCmd.exe)
*
* @param {String} [wpsdk] - The windows phone sdk version ('8.0', '8.1', '10.0').
* @param {Object} [options] - An object containing various settings.
* @param {Function} [next(err, results)] - A function to call with the device information.
*/
function nativeEnumerate(wpsdk, options, next) {
return windowsphone.detect(options, function (err, phoneResults) {
if (err) {
return next(err, null);
}
if (!phoneResults.windowsphone[wpsdk]) {
// Just move on if we have no results for a given version
return next(null, {devices:[],emulators:[]});
}
if (!phoneResults.windowsphone[wpsdk].deployCmd) {
var ex = new Error(__('No deploy command found for WP SDK %s. Cannot enumerate devices.', wpsdk));
return next(ex, null);
}
var cmd = phoneResults.windowsphone[wpsdk].deployCmd,
args = ['/EnumerateDevices'],
child = spawn(cmd, args),
out = '',
result;
child.stdout.on('data', function (data) {
out += data.toString();
});
child.stderr.on('data', function (data) {
out += data.toString();
});
child.on('close', function (code) {
if (code) {
var errmsg = out.trim().split(/\r\n|\n/).shift(),
ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to enumerate devices/emulators for WP SDK %s (code %s)', wpsdk, code));
next(ex, null);
} else {
var emulators = parseAppDeployCmdListing(out, wpsdk);
next(null, {
devices: [{name: 'Device', udid: 0, index: 0, wpsdk: null, type: 'device'}],
emulators: emulators
});
}
});
});
}
/**
* Detects Windows Phone devices and emulators.
*
* @param {Object} [options] - An object containing various settings.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects all Windows Phone devices.
* @param {Function} [callback(err, results)] - A function to call with the device/emulator information.
*/
function detect(options, callback) {
return enumerate(options, function (err, results) {
var result = {
emulators: {},
devices: [],
issues: []
},
tmp = {};
if (err && !results) {
// detected an error with no results
callback(err);
} else {
Object.keys(results).forEach(function (wpsdk) {
result.emulators[wpsdk] = results[wpsdk].emulators;
results[wpsdk].devices.forEach(function (dev) {
if (!tmp[dev.udid]) {
tmp[dev.udid] = result.devices.length+1;
result.devices.push(dev);
} else if (dev.wpsdk) {
result.devices[tmp[dev.udid]-1] = dev;
}
});
});
// If we have a device with udid of 0 and non-null wpsdk _and_
// we have a device with real udid we got from WinAppDeployCmd, combine the listings!
var wpsdkIndex = -1,
realDeviceIndex = -1;
for (var i = 0; i < result.devices.length; i++) {
var dev = result.devices[i];
if (dev.udid == 0 && dev.wpsdk) {
wpsdkIndex = i;
} else if (dev.udid != 0 && !dev.wpsdk) {
// now find with "real" device
realDeviceIndex = i;
}
if (wpsdkIndex != -1 && realDeviceIndex != -1) {
break;
}
};
if (wpsdkIndex != -1 && realDeviceIndex != -1) {
// set 'real' device wpsdk to the value we got from wptool binary
result.devices[realDeviceIndex].wpsdk = result.devices[wpsdkIndex].wpsdk;
// remove the wptool binary entry
result.devices.splice(wpsdkIndex, 1);
}
}
callback(null, result);
});
}
/**
* Detects all Windows Phone devices and emulators.
*
* @param {Object} [options] - An object containing various settings.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects devices and emulators.
* @param {Function} [callback(err, results)] - A function to call with the device information.
*
* @emits module:wptool#detected
* @emits module:wptool#error
*
* @returns {EventEmitter}
*/
function enumerate(options, callback) {
return magik(options, callback, function (emitter, options, callback) {
if (cache && !options.bypassCache) {
emitter.emit('detected', cache);
return callback(null, cache);
}
function runTool() {
var results = {},
errors = [],
toDetect;
if (options.supportedWindowsPhoneSDKVersions) {
toDetect = wpsdks.filter(ver => appc.version.satisfies(ver, options.supportedWindowsPhoneSDKVersions, false));
} else {
toDetect = wpsdks;
}
// wpsdks is a constant above that contains all supported Windows Phone SDK versions
async.eachSeries(toDetect, function (wpsdk, next) {
// Use custom wptool for 10.0 and 8.1, use native tooling for 8.0
var funcToCall = (wpsdk == '10.0') ? wptoolEnumerate : nativeEnumerate;
funcToCall(wpsdk, options, function (err, result) {
if (err) {
// If there was an error, move on, but record error.
// Then later if we have no results for any version, we propagate the error
if (!results[wpsdk]) {
results[wpsdk] = {
devices: [],
emulators: [],
};
}
errors.push(err);
next();
} else {
results[wpsdk] = result;
next();
}
});
}, function (err) {
if (err) {
emitter.emit('error', err);
return callback(err);
}
// If there are no emulators for either version, surface the first error
if (errors.length > 0 && !Object.keys(results).some(function (wpsdk) {
return results[wpsdk].emulators.length > 0;
})) {
emitter.emit('error', errors[0]);
return callback(errors[0], results);
}
// add a helper function to get a device by udid
Object.defineProperty(results, 'getByUdid', {
value: function (udid) {
var dev = null;
function testDev(d) {
if (d.udid == udid) { // this MUST be == because the udid might be a number and not a string
dev = d;
return true;
}
}
Object.keys(results).some(function (wpsdk) {
return results[wpsdk].devices.some(testDev) || results[wpsdk].emulators.some(testDev);
});
return dev;
}
});
cache = results;
emitter.emit('detected', cache);
callback(null, cache);
});
}
runTool();
});
}
/**
* Connects to a Windows Phone device or launches a Windows Phone emulator.
*
* @param {String} udid - The device or emulator udid.
* @param {Object} [options] - An object containing various settings.
* @param {Boolean} [options.bypassCache=false] - When true, re-detects devices and emulators.
* @param {String} [options.assemblyPath=%WINDIR%\Microsoft.NET\assembly\GAC_MSIL] - Path to .NET global assembly cache.
* @param {Object} [options.requiredAssemblies] - An object containing assemblies to check for in addition to the required windowslib dependencies.
* @param {Number} [options.timeout] - The number of milliseconds to wait before timing out.
* @param {Function} [callback(err, handle)] - A function to call after attempting to connnect to the device/emulator.
*
* @emits module:wptool#connected
* @emits module:wptool#error
* @emits module:wptool#timeout
*
* @returns {EventEmitter}
*/
function connect(udid, options, callback) {
return magik(options, callback, function (emitter, options, callback) {
if (udid === null || udid === void 0) {
var ex = new Error(__('Missing required "%s" argument', 'udid'));
emitter.emit('error', ex);
return callback(ex);
}
enumerate(options)
.on('error', function (err) {
emitter.emit('error', err);
callback(err);
})
.on('detected', function (results) {
// validate the udid
var dev = results.getByUdid(udid);
if (!dev) {
var err = new Error(__('Invalid udid "%s"', udid));
emitter.emit('error', err);
return callback(err);
}
// TODO if we have win 10 use it's deploy tool to push to devices?
var wpsdk = dev.wpsdk || options.wpsdk || options.preferredWindowsPhoneSDK || PREFERRED_SDK,
done = function (err, result) {
if (err) {
emitter.emit(result || 'error', err);
return callback(err);
}
emitter.emit('connected', result); // send along device info we have
callback(null, result);
};
if (wpsdk == '10.0') {
// if win10, just call connect on our native tool!
wpToolConnect(dev, options, done);
} else {
// If win 8.x, launch bogus app
nativeLaunch(dev, 'f8ce6878-0aeb-497f-bcf4-65be961d4bba', options, done);
}
});
});
}
/**
* Builds our own custom tool to interact with emulators and devices.
*
* @param {Object} [options] - An object containing various settings.
* @param {String} [options.assemblyPath=%WINDIR%\Microsoft.NET\assembly\GAC_MSIL] - Path to .NET global assembly cache.
* @param {Object} [options.requiredAssemblies] - An object containing assemblies to check for in addition to the required windowslib dependencies.
* @param {Function} [callback(err, path)] - A function to call after building the executable.
*/
function buildWpTool(options, callback) {
// FIXME Handle when we don't have permission to edit the existing csproj or copy to the bin dir!
// We should move to a writable directory under HOME and return path to that
// find required assemblies
return assemblies.detect(options, function (err, results) {
if (err) {
return callback(err);
}
// check that we have the assemblies we need
var requiredAssemblies = {
'Microsoft.SmartDevice.Connectivity.Interface': null,
'Microsoft.SmartDevice.MultiTargeting.Connectivity': null
},
missing = Object.keys(requiredAssemblies).filter(function (assembly) {
var r = results.assemblies[assembly];
if (!r) return true;
requiredAssemblies[assembly] = r[Object.keys(r).sort().pop()];
});
if (missing.length) {
var ex = new Error(__('Missing one or more required Microsoft .NET assemblies: %s', missing.join(', ')));
return callback(ex);
}
// update visual studio references
var project = path.resolve(__dirname, '..', 'wptool', 'wptool.csproj'),
parser = new DOMParser({ errorHandler: function () {} }),
dom = parser.parseFromString(fs.readFileSync(project).toString(), 'text/xml');
(function updateRefs(node) {
while (node) {
if (node.nodeType === appc.xml.ELEMENT_NODE) {
switch (node.tagName) {
case 'Reference':
var inc = node.getAttribute('Include');
if (inc) {
var name = inc.split(',').shift();
if (requiredAssemblies[name]) {
node.setAttribute('Include', name + ', Version=' + requiredAssemblies[name].assemblyVersion + ', Culture=neutral, PublicKeyToken=' + requiredAssemblies[name].publicKeyToken + ', processorArchitecture=MSIL');
var child = node.firstChild,
found = false;
while (child) {
if (child.nodeType === appc.xml.ELEMENT_NODE && child.tagName === 'HintPath') {
while (child.firstChild) {
child.removeChild(child.firstChild);
}
child.appendChild(dom.createTextNode(requiredAssemblies[name].assemblyFile));
found = true;
break;
}
child = child.nextSibling;
}
if (!found) {
child = dom.createElement('HintPath');
child.appendChild(dom.createTextNode(requiredAssemblies[name].assemblyFile));
node.appendChild(child);
}
}
}
break;
default:
updateRefs(node.firstChild);
}
}
node = node.nextSibling;
}
}(dom.documentElement.firstChild));
fs.writeFileSync(project, '<?xml version="1.0" encoding="UTF-8"?>\n' + dom.documentElement.toString());
// remove the bin and obj folders
var d;
fs.existsSync(d = path.resolve(__dirname, '..', 'wptool', 'bin')) && wrench.rmdirSyncRecursive(d);
fs.existsSync(d = path.resolve(__dirname, '..', 'wptool', 'obj')) && wrench.rmdirSyncRecursive(d);
var pathLength = 0;
for(var assembly of Object.keys(requiredAssemblies)) {
var assemblyPath = path.join(__dirname,'wptool', 'bin', 'Release' , assembly);
if (assemblyPath.length > pathLength) {
pathLength = assemblyPath.length;
}
}
if (pathLength >= 260) {
if (!fs.existsSync(tmpBuildDir)) {
wrench.mkdirSyncRecursive(tmpBuildDir);
}
wrench.copyDirSyncRecursive(path.join(__dirname, '..', 'wptool'), tmpBuildDir, {
forceDelete: true
});
project = path.join(tmpBuildDir, 'wptool.csproj');
}
// build the wptool
visualstudio.build(appc.util.mix({
buildConfiguration: 'Release',
project: project
}, options), function (err, result) {
if (err) {
return callback(err);
}
var src = path.resolve(path.dirname(project), 'bin', 'Release', 'wptool.exe');
if (!fs.existsSync(src)) {
var ex = new Error(__('Failed to build the wptool executable.'));
return callback(ex);
}
// Always copy wptool.exe whenever we build it
fs.writeFileSync(wptool, fs.readFileSync(src));
// Make sure to copy all dependencies
var srcdir = path.resolve(path.dirname(project), 'bin', 'Release');
fs.readdirSync(srcdir).forEach(function(filename) {
if (path.extname(filename) == '.dll') {
var dest = path.resolve(__dirname, '..', 'bin', filename);
fs.writeFileSync(dest, fs.readFileSync(path.resolve(srcdir, filename)));
}
});
return callback(null, wptool);
});
});
}
function wpToolConnect(device, options, callback) {
var args = [
'connect',
device.index
],
child = spawn(wptool, args),
out = '',
abortTimer,
timedOut = false;
child.stdout.on('data', function (data) {
out += data.toString();
});
child.stderr.on('data', function (data) {
out += data.toString();
});
child.on('close', function (code) {
clearTimeout(abortTimer);
try {
var result = JSON.parse(out),
pollEmulator;
if (!result.success) {
clearTimeout(abortTimer);
return callback(new Error(__('Failed to connect to %s', device.name)));
}
device.ip = result.ip;
// If this is an emulator we should poll the status and wait until it's 'Running' before moving on
// I'm seeing consistent failures to install an app on Windows 10 emulators in our builds here.
if (device.type == 'emulator') {
pollEmulator = function() {
if (timedOut) {
return;
}
// check if emulator is running...
emulator.status(device, function(err, status) {
if (err) {
clearTimeout(abortTimer);
return callback(err);
}
if (status == 2) { // running state
clearTimeout(abortTimer);
device.running = true; // mark it as running so we don't try and launch it again via connect
callback(null, device);
} else {
// try again in 500ms
setTimeout(pollEmulator, 500);
}
});
};
// wait 250ms and check status of emulator
setTimeout(pollEmulator, 250);
} else {
// It's a device, just assume we're ok
clearTimeout(abortTimer);
device.running = true; // mark it as running so we don't try and launch it again via connect
callback(null, device);
}
} catch (e) {
clearTimeout(abortTimer);
callback(new Error(__('Failed to connect to %s', device.name)));
}
});
if (options.timeout) {
abortTimer = setTimeout(function () {
timedOut = true; // set flag so we don't poll on emulator state change
child.kill();
var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.name));
callback(ex, 'timeout');
}, options.timeout);
}
}
/**
* Launches an app on a given emulator/device.
**/
function wpToolLaunch(device, productGuid, options, callback) {
var args = [
'launch',
device.index,
productGuid
],
child = spawn(wptool, args),
out = '',
abortTimer;
child.stdout.on('data', function (data) {
out += data.toString();
});
child.stderr.on('data', function (data) {
out += data.toString();
});
child.on('close', function (code) {
clearTimeout(abortTimer);
try {
var result = JSON.parse(out);
if (result.success) {
return callback(null, device);
}
var ex = new Error(__('Failed to launch app: %s', result.message));
callback(ex);
} catch (e) {
var ex = new Error(__('Failed to connect to emulator: %s', out));
callback(ex);
}
});
if (options.timeout) {
abortTimer = setTimeout(function () {
child.kill();
var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.type));
callback(ex, 'timeout');
}, options.timeout);
}
}
function nativeLaunch(device, appid, options, callback) {
windowsphone.detect(options, function (err, phoneResults) {
if (err) {
return callback(err);
}
var wpsdk = device.wpsdk || options.wpsdk || options.preferredWindowsPhoneSDK || PREFERRED_SDK,
deployCmd = phoneResults.windowsphone[wpsdk].deployCmd,
args = [
'/launch',
appid,
'/targetdevice:' + device.index
],
child,
out = '',
abortTimer;
if (!deployCmd) {
var ex = new Error(__('Windows Phone SDK v%s does not appear to have an App deploy tool.', wpsdk));
return callback(ex);
}
child = spawn(deployCmd, args);
child.stdout.on('data', function (data) {
out += data.toString();
});
child.stderr.on('data', function (data) {
out += data.toString();
});
child.on('close', function (code) {
clearTimeout(abortTimer);
var errmsg = out.trim().split(/\r\n|\n/).shift(),
ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to start %s (code %s)', device.name, code));
// Here's where we expect the failure that the app is not installed, which is right.
// We're explicitly telling to launch a bogus app, so we expect a very specific failure as "success" here...
// if (code == -2146233088 || code == 2148734208)
if (errmsg == '' || errmsg.indexOf('The application is not installed.') != -1) {
// we must be successful, right?
callback(null, device);
} else {
// we sometimes get the same code, but different error message
callback(ex);
}
});
if (options.timeout) {
abortTimer = setTimeout(function () {
child.kill();
var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.type));
callback(ex, 'timeout');
}, options.timeout);
}
});
}
function nativeInstall(deployCmd, device, appPath, options, callback) {
// We're explicitly telling to launch a bogus app, so we expect a very specific failure as "success" here...
var args = [
options.skipLaunch ? '/install' : '/installlaunch',
appPath,
'/targetdevice:' + device.index
],
child = spawn(deployCmd, args),
out = '',
abortTimer;
child.stdout.on('data', function (data) {
out += data.toString();
});
child.stderr.on('data', function (data) {
out += data.toString();
});
child.on('close', function (code) {
clearTimeout(abortTimer);
if (out.trim() != '' && code) {
var errmsg = out.trim().split(/\r\n|\n/).shift(),
ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to install app (code %s)', code));
callback(ex);
} else {
device.running = true;
callback(null, device);
}
});
if (options.timeout) {
abortTimer = setTimeout(function () {
child.kill();
var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.type));
callback(ex, 'timeout');
}, options.timeout);
}
}
/**
* Installs an app on a device/emulator, via WinAppDeployCmd. If necessary we'll
* first launch the emulator and grab the IP address before installing.
*
* @param {String} [deployCmd] - Path to WinAppDeployCmd.exe
* @param {Object} [device] - The windows phone device or emulator.
* @param {String} [appPath] - Path to the appx, xap or appxbundle to install.
* @param {Object} [options] - An object containing various settings.
* @param {Function} [callback(err, device)] - A function to call with the device information.
*/
function wpToolInstall(deployCmd, device, appPath, options, callback) {
if (!device.ip || !device.running) {
// Launch the emulator, grab the IP and mark as running then install the app
return wpToolConnect(device, options, function(err, dev) {
if (err) {
return callback(err);
}
wpToolInstall(deployCmd, dev, appPath, options, callback);
});
}
var args = [
options.forceUnInstall ? 'uninstall' : 'install',
'-file',
appPath,
'-ip',
device.ip
],
child = spawn(deployCmd, args),
out = '',
abortTimer;
child.stdout.on('data', function (data) {
out += data.toString();
});
child.stderr.on('data', function (data) {
out += data.toString();
});
// 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
// telling them how to install manually and pair once with pin?
child.on('close', function (code) {
clearTimeout(abortTimer);
if (code) {
// handle duplicate package identity error code from Windows 10.0.14393 tooling and above
if (code == '2148734208') {
if (out.indexOf('0x80131500 - Failed to install or update package: Unspecified error') != -1) {
// handle duplicate package identity error code from Windows 10.0.14393 tooling and above
callback(new Error('A debug application is already installed, please remove existing debug application.'));
} else if (out.indexOf('because the current user does not have that package installed') == -1) {
if (options.forceUnInstall) {
wpToolInstall(deployCmd, device, appPath, options, callback);
} else {
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.'));
}
} else {
// Windows cannot remove the app because the current user does not have that package installed.
options.forceUnInstall = false;
wpToolInstall(deployCmd, device, appPath, options, callback);
}
} else if (code == '2147943860' && out.indexOf('violates pattern constraint of') != -1) {
// WinAppDeployCmd says deployment failed but we saw app is actually installed in this case...
callback(null, device);
} else {
var errmsg = out.trim().split(/\r\n|\n/).shift(),
ex = new Error(/^Error: /.test(errmsg) ? errmsg.substring(7) : __('Failed to install app (code %s): %s', code, out));
callback(ex);
}
} else {
var errmsg = /failed\. (\w*)\r?\n(.*)/.exec(out);
if (errmsg) {
var err = errmsg[1],
msg = errmsg[2];
if (err == '0x80073CF9') {
callback(new Error('A debug application is already installed, please remove existing debug application'));
} else if (err == '0x80073CFB') {
if (options.forceUnInstall) {
// Provided package has the same identity as an already-installed package. Proceed uninstalling.
wpToolInstall(deployCmd, device, appPath, options, callback);
} else {
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.'));
}
} else {
callback(new Error(__('Failed to install app (code %s): %s', err, msg)));
}
} else {
// Provided package is uninstalled...proceed re-installing.
if (options.forceUnInstall) {
options.forceUnInstall = false;
wpToolInstall(deployCmd, device, appPath, options, callback);
} else {
callback(null, device);
}
}
}
});
if (options.timeout) {
abortTimer = setTimeout(function () {
child.kill();
var ex = new Error(__('Timed out after %d milliseconds trying to connect to %s', options.timeout, device.type));
callback(ex, 'timeout');
}, options.timeout);
}
}
/**
* Installs an app on a device/emulator, defaults to launching it as well.
*
* @param {Object} [device] - The windows phonedevice or emulator.
* @param {String} [appPath] - Path to the appx, xap or appxbundle to install.
* @param {Object} [options] - An object containing various settings.
* @param {Object} [options.skipLaunch] - Just install the app, don't launch it too.
* @param {Object} [options.appGuid] - The generated app guid. May be null/empty, if so we'll try to detect it
* @param {String} [options.powershell] - Path to the 'powershell' executable.
* @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
*
* @emits module:wptool#error
* @emits module:wptool#timeout
* @emits module:wptool#installed
* @emits module:wptool#launched
*
* @returns {EventEmitter}
*/
function install(device, appPath, options, callback) {
return magik(options, callback, function (emitter, options, callback) {
windowsphone.detect(options, function (err, phoneResults) {
if (err) {
emitter.emit('error', err);
return callback(err);
}
var wpsdk = device.wpsdk || options.wpsdk || options.preferredWindowsPhoneSDK || PREFERRED_SDK,
cmd = phoneResults.windowsphone[wpsdk].deployCmd;
if (!cmd) {
var ex = new Error(__('Windows Phone SDK v%s does not appear to have an App deploy tool.', wpsdk));
return callback(ex);
}
if (wpsdk == '10.0') {
if (!options.skipLaunch) {
var guid;
// we need the appid to launch, so install the app and get the app id in parallel
async.parallel([
function (next) {
wpToolInstall(cmd, device, appPath, options, function (err, result) {
if (err) {
emitter.emit(result || 'error', err);
return next(err);
}
emitter.emit('installed', device);
next();
});
},
function (next) {
if (options.appGuid) {
guid = options.appGuid;
next();
} else {
getProductGUID(appPath, options, function(err, productGuid) {
if (err) {
emitter.emit('error', err);
return next(err);
}
guid = productGuid;
next();
});
}
}
], function (err, results) {
if (err) {
return callback(err);
}
// now launch it!
wpToolLaunch(device, guid, options, function (err, result) {
if (err) {
emitter.emit(result || 'error', err);
return callback(err);
}
emitter.emit('launched', device);
callback(null, result);
});
});
} else {
// We're just installing. No need to grab appid or launch the app
wpToolInstall(cmd, device, appPath, options, function (err, result) {
if (err) {
emitter.emit(result || 'error', err);
return callback(err);
}
emitter.emit('installed', device);
return callback(null, result);
});
}
} else {
nativeInstall(cmd, device, appPath, options, function (err, result) {
if (err) {
emitter.emit(result || 'error', err);
return callback(err);
}
emitter.emit('installed', device);
if (!options.skipLaunch) {
emitter.emit('launched', device);
}
callback(null, result);
});
}
});
});
}
/**
* Unzips an appx file to read the AppxManifest.xml and grab the product guid
* out (so we know the guid we need to launch it)
*
* @param {String} [appxFile] - Path to the appx, xap or appxbundle to inspect.
* @param {Object} [options] - An object containing various settings.
* @param {String} [options.powershell] - Path to the 'powershell' executable.
* @param {Function} [callback(err, results)] - A function to call with the GUID
*/
function getProductGUID(appxFile, options, callback) {
appc.subprocess.getRealName(path.resolve(__dirname, '..', 'bin', 'wp_get_appx_metadata.ps1'), function (err, script) {
if (err) {
return callback(err);
}
appc.subprocess.run(options.powershell || 'powershell', [
'-ExecutionPolicy', 'Bypass', '-NoLogo', '-NonInteractive', '-NoProfile',
'-File',
script,
appxFile
], function (code, out, err) {
if (code) {
var ex = new Error(__('Failed to detect product id of appx: %s', out));
return callback(ex);
}
callback(null, out.trim());
});
});
}