UNPKG

16.6 kBJavaScriptView Raw
1/**
2 * An assortment of Windows Store app-related tools.
3 *
4 * @module winstore
5 *
6 * @copyright
7 * Copyright (c) 2014-2016 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 fs = require('fs'),
18 magik = require('./utilities').magik,
19 checkOutdated = require('./utilities').checkOutdated,
20 path = require('path'),
21 visualstudio = require('./visualstudio'),
22 __ = appc.i18n(__dirname).__,
23 wstool = path.resolve(__dirname, '..', 'bin', 'wstool.exe');
24
25var architectures = [ 'arm', 'x86', 'x64' ];
26
27var detectCache,
28 deviceCache = {};
29
30exports.install = install;
31exports.launch = launch;
32exports.uninstall = uninstall;
33exports.detect = detect;
34exports.getAppxPackages = getAppxPackages;
35exports.loopbackExempt = loopbackExempt;
36
37/**
38 * Installs a Windows Store application.
39 *
40 * @param {Object} [options] - An object containing various settings.
41 * @param {String} [options.buildConfiguration='Release'] - The type of configuration to build using. Example: "Release" or "Debug".
42 * @param {Function} [callback(err)] - A function to call after installing the Windows Store app.
43 *
44 * @emits module:winstore#error
45 * @emits module:winstore#installed
46 *
47 * @returns {EventEmitter}
48 */
49function install(projectDir, options, callback) {
50 return magik(options, callback, function (emitter, options, callback) {
51 var scripts = [],
52 packageScript = 'Add-AppDevPackage.ps1';
53
54 // find the Add-AppDevPackage.ps1
55 (function walk(dir) {
56 fs.readdirSync(dir).forEach(function (name) {
57 var file = path.join(dir, name);
58 if (fs.statSync(file).isDirectory()) {
59 walk(file);
60 } else if (name === packageScript && (!options.buildConfiguration || path.basename(dir).indexOf('_' + options.buildConfiguration) !== -1)) {
61 scripts.push(file);
62 }
63 });
64 }(projectDir));
65
66 if (!scripts.length) {
67 var err = new Error(__('Unable to find built application. Please rebuild the project.'));
68 emitter.emit('error', err);
69 return callback(err);
70 }
71
72 // let's grab the first match
73 appc.subprocess.getRealName(scripts[0], function (err, psScript) {
74 if (err) {
75 emitter.emit('error', err);
76 return callback(err);
77 }
78
79 appc.subprocess.run(options.powershell || 'powershell', ['-ExecutionPolicy', 'Bypass', '-NoLogo', '-NoProfile', '-File', psScript, '-Force'], function (code, out, err) {
80 if (!code) {
81 emitter.emit('installed');
82 return callback();
83 }
84
85 // I'm seeing "Please run this script without the -Force parameter" for Win 8.1 store apps.
86 // This originally was "Please rerun the script without the -Force parameter" (for Win 8 hybrid apps?)
87 // It's a hack to check for the common substring. Hopefully use of the exact error codes works better first
88 // Error codes 9 and 14 mean rerun without -Force
89 if ((code && (code == 9 || code == 14)) ||
90 out.indexOf('script without the -Force parameter') !== -1) {
91 require('child_process').exec((options.powershell || 'powershell') + ' -ExecutionPolicy Bypass -NoLogo -NoProfile -Command "Start-Process powershell -Wait -argument ' + psScript + '"', function(code, out, err) {
92 if (err) {
93 emitter.emit('error', err);
94 callback(err);
95 } else {
96 emitter.emit('installed');
97 callback();
98 }
99 });
100 return;
101 }
102
103 // must have been some other issue, error out
104 var ex = new Error(__('Failed to install app: %s', out));
105 emitter.emit('error', ex);
106 callback(ex);
107 });
108 });
109 });
110}
111
112/**
113 * Calls Get-AppxPackage for the full listing of app packages and returns the results as a JSON object keyed by the app name.(appid)
114 *
115 * @param {Object} [options] - An object containing various settings.
116 * @param {String} [options.powershell='powershell'] - Path to the 'powershell' executable.
117 * @param {Function} [callback(err, packages)] - A function to call when we have an error or the full package listing as JSON
118 **/
119function getAppxPackages(options, callback) {
120 appc.subprocess.run(options.powershell || 'powershell', ['-NoLogo', '-NoProfile', '-NonInteractive', '-command', 'Get-AppxPackage'], function (code, out, err) {
121 if (code) {
122 var ex = new Error(__('Could not query the list of installed Windows Store apps: %s', err || code));
123 return callback(ex);
124 }
125
126 var keyValueRegexp = /([a-z]+)\s+:\s(.*)/i,
127 packageName,
128 packages = {},
129 key = '';
130 // FIXME This isn't smart about keys with empty values
131 out.split(/\r\n|\n/).forEach(function (line) {
132 var trimmed = line.trim(),
133 m = trimmed.match(keyValueRegexp),
134 value;
135 if (m) { // key/value pair
136 key = m[1];
137 value = m[2];
138 if (key == 'Name') {
139 packageName = value;
140 packages[packageName] = {};
141 }
142 // store key / value pair for package, "cast" 'False' to false boolean, 'True' to true boolean
143 if (value == 'False') {
144 value = false;
145 } else if (value == 'True') {
146 value = true;
147 }
148 packages[packageName][key] = value;
149 } else { // no match, so either empty line or continuation of multiline value
150 if (trimmed.length > 0) {
151 // append value to last key!
152 packages[packageName][key] += trimmed;
153 }
154 }
155 });
156
157 callback(null, packages);
158 });
159}
160
161/**
162 * Makes a given app (identified by appid) exempt from network isolation for the
163 * loopback IP. Useful for exempting store apps so we can do log relaying to CLI.
164 *
165 * @param {String} appId - The application id.
166 * @param {Object} [options] - An object containing various settings.
167 * @param {String} [options.powershell='powershell'] - Path to the 'powershell' executable.
168 * @param {Function} [callback(err)] - A function to call when we have an error or completed exempting the app from loopback ip network isolation
169 **/
170function loopbackExempt(appId, options, callback) {
171 getAppxPackages(options, function (err, packages) {
172 if (err) {
173 return callback(err);
174 }
175
176 if (!packages[appId]) {
177 var ex = new Error(__('Unable to find an installed app with id: %s', appId));
178 return callback(ex);
179 }
180
181 appc.subprocess.run('CheckNetIsolation.exe', ['LoopbackExempt', '-a', '-n=' + packages[appId].PackageFamilyName], function (code, out, err) {
182 if (!code) {
183 return callback();
184 }
185 return callback(err);
186 });
187 });
188}
189
190/**
191 * Returns the PackageFullName for an app given it's appid.
192 *
193 * @param {String} appId - The application id.
194 * @param {Object} [options] - An object containing various settings.
195 * @param {String} [options.powershell='powershell'] - Path to the 'powershell' executable.
196 * @param {Function} [callback(err, packageName)] - A function to call when we have an error or get the final value
197 **/
198function getPackageFullName(appId, options, callback) {
199 getAppxPackages(options, function (err, packages) {
200 if (err) {
201 return callback(err);
202 }
203
204 return callback(null, packages[appId] && packages[appId].PackageFullName);
205 });
206}
207
208/**
209 * Uninstalls a Windows Store application.
210 *
211 * @param {String} appId - The application id.
212 * @param {Object} [options] - An object containing various settings.
213 * @param {String} [options.powershell='powershell'] - Path to the 'powershell' executable.
214 * @param {Function} [callback(err)] - A function to call after uninstalling the Windows Store app.
215 *
216 * @emits module:winstore#error
217 * @emits module:winstore#uninstalled
218 *
219 * @returns {EventEmitter}
220 */
221function uninstall(appId, options, callback) {
222 return magik(options, callback, function (emitter, options, callback) {
223 getPackageFullName(appId, options, function(err, packageName) {
224 if (err) {
225 emitter.emit('error', err);
226 return callback(err);
227 }
228
229 if (packageName) {
230 appc.subprocess.run(options.powershell || 'powershell', ['-NoLogo', '-NoProfile', '-NonInteractive', '-command', 'Remove-AppxPackage ' + packageName], function (code, out, err) {
231 if (err) {
232 emitter.emit('error', err);
233 callback(err);
234 } else {
235 emitter.emit('uninstalled');
236 callback();
237 }
238 });
239 } else {
240 emitter.emit('uninstalled');
241 callback();
242 }
243 });
244 });
245}
246
247/**
248 * Launches a Windows Store application.
249 *
250 * @param {String} appId - The application id.
251 * @param {String} version - The application version.
252 * @param {Object} [options] - An object containing various settings.
253 * @param {String} [options.powershell='powershell'] - Path to the 'powershell' executable.
254 * @param {String} [options.version] - The specific version of the app to launch. If empty, picks the largest version.
255 * @param {Function} [callback(err, pid)] - A function to call after launching the Windows Store app. pid is output from wstool which should be pid of process launched
256 *
257 * @emits module:winstore#error
258 * @emits module:winstore#installed
259 *
260 * @returns {EventEmitter}
261 */
262function launch(appId, options, callback) {
263 return magik(options, callback, function (emitter, options, callback) {
264 function runTool() {
265 var args = ['launch', '--appid', appId];
266
267 if (options.version) {
268 args.push('--version');
269 args.push(options.version);
270 }
271
272 if (options.windowsAppId) {
273 args.push('--windowsAppId');
274 args.push(options.windowsAppId);
275 }
276
277 appc.subprocess.run(wstool, args, function (code, out, err) {
278 if (code) {
279 var ex = new Error(__('Erroring running wstool (code %s)', code) + '\n' + out);
280 emitter.emit('error', ex);
281 callback(ex);
282 } else {
283 emitter.emit('installed', out.trim());
284 callback(null, out.trim());
285 }
286 });
287 }
288
289 var wsToolCs = path.resolve(__dirname, '..', 'wstool', 'wstool.cs');
290 checkOutdated(wsToolCs, wstool, function(err, outdated) {
291 if (err) {
292 emitter.emit('error', err);
293 return callback(err);
294 }
295
296 if (outdated) {
297 return buildWsTool(options, function (err, path) {
298 if (err) {
299 emitter.emit('error', err);
300 return callback(err);
301 }
302 runTool();
303 });
304 }
305
306 return runTool();
307 });
308 });
309}
310
311/**
312 * Builds our custom wstool.exe from source
313 *
314 * @param {Object} [options] - An object containing various settings.
315 * @param {String} [options.powershell='powershell'] - Path to the 'powershell' executable.
316 * @param {Function} [callback(err, path)] - A function to call after building wstool.exe
317 */
318function buildWsTool(options, callback) {
319 visualstudio.build(appc.util.mix({
320 buildConfiguration: 'Release',
321 project: path.resolve(__dirname, '..', 'wstool', 'wstool.csproj')
322 }, options), function (err, result) {
323 if (err) {
324 return callback(err);
325 }
326
327 var src = path.resolve(__dirname, '..', 'wstool', 'bin', 'Release', 'wstool.exe');
328 if (!fs.existsSync(src)) {
329 var ex = new Error(__('Failed to build the wstool executable.') + (result ? '\n' + result.out : ''));
330 return callback(ex);
331 }
332
333 // sanity check that the wstool.exe wasn't copied by another async task in windowslib
334 if (!fs.existsSync(wstool)) {
335 fs.writeFileSync(wstool, fs.readFileSync(src));
336 }
337
338 callback(null, wstool);
339 });
340}
341
342/**
343 * Detects Windows Store SDKs.
344 *
345 * @param {Object} [options] - An object containing various settings.
346 * @param {Boolean} [options.bypassCache=false] - When true, re-detects the Windows SDKs.
347 * @param {String} [options.preferredWindowsSDK] - The preferred version of the Windows SDK to use by default. Example "8.0".
348 * @param {String} [options.supportedWindowsSDKVersions] - A string with a version number or range to check if a Windows SDK is supported.
349 * @param {Function} [callback(err, results)] - A function to call with the Windows SDK information.
350 *
351 * @emits module:windowsphone#detected
352 * @emits module:windowsphone#error
353 *
354 * @returns {EventEmitter}
355 */
356function detect(options, callback) {
357 return magik(options, callback, function (emitter, options, callback) {
358 if (detectCache && !options.bypassCache) {
359 emitter.emit('detected', detectCache);
360 return callback(null, detectCache);
361 }
362
363 var results = {
364 windows: {},
365 issues: []
366 },
367 searchPaths = [
368 'HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Microsoft SDKs\\Windows', // probably nothing here
369 'HKEY_LOCAL_MACHINE\\Software\\Wow6432Node\\Microsoft\\Microsoft SDKs\\Windows' // this is most likely where Windows SDK will be found
370 ];
371
372 function finalize() {
373 detectCache = results;
374 emitter.emit('detected', results);
375 callback(null, results);
376 }
377
378 async.each(searchPaths, function (keyPath, next) {
379 appc.subprocess.run('reg', ['query', keyPath], function (code, out, err) {
380 var keyRegExp = /.+\\(v\d+\.\d)$/;
381 if (!code) {
382 out.trim().split(/\r\n|\n/).forEach(function (key) {
383 key = key.trim();
384 var m = key.match(keyRegExp);
385 if (!m) {
386 return;
387 }
388 var version = m[1].replace(/^v/, '');
389 if (m) {
390 results.windows || (results.windows = {});
391 results.windows[version] = {
392 version: version,
393 registryKey: keyPath + '\\' + m[1],
394 supported: !options.supportedWindowsSDKVersions || appc.version.satisfies(version, options.supportedWindowsSDKVersions, false), // no maybes
395 path: null,
396 signTool: null,
397 makeCert: null,
398 pvk2pfx: null,
399 selected: false,
400 sdks: []
401 };
402 }
403 });
404 }
405 next();
406 });
407 }, function () {
408 // check if we didn't find any Windows SDKs, then we're done
409 if (!Object.keys(results.windows).length) {
410 results.issues.push({
411 id: 'WINDOWS_STORE_SDK_NOT_INSTALLED',
412 type: 'error',
413 message: __('Microsoft Windows Store SDK not found.') + '\n' +
414 __('You will be unable to build Windows Store apps.')
415 });
416 return finalize();
417 }
418
419 // fetch Windows SDK install information
420 async.each(Object.keys(results.windows), function (ver, next) {
421 appc.subprocess.run('reg', ['query', results.windows[ver].registryKey, '/v', '*'], function (code, out, err) {
422 if (code) {
423 // bad key? either way, remove this version
424 delete results.windows[ver];
425 } else {
426 // get only the values we are interested in
427 out.trim().split(/\r\n|\n/).forEach(function (line) {
428 var parts = line.trim().split(' ').map(function (p) { return p.trim(); });
429 if (parts.length == 3) {
430 if (parts[0] == 'InstallationFolder') {
431 results.windows[ver].path = parts[2];
432
433 function addIfExists(key, exe) {
434 for (var i = 0; i < architectures.length; i++) {
435 var arch = architectures[i],
436 tool = path.join(parts[2], 'bin', arch, exe);
437 if (fs.existsSync(tool)) {
438 !results.windows[ver][key] && (results.windows[ver][key] = {});
439 results.windows[ver][key][arch] = tool;
440 }
441 }
442 }
443
444 addIfExists('signTool', 'SignTool.exe');
445 addIfExists('makeCert', 'MakeCert.exe');
446 addIfExists('pvk2pfx', 'pvk2pfx.exe');
447 }
448 }
449 });
450 }
451 next();
452 });
453 }, function () {
454 // double check if we didn't find any Windows SDKs, then we're done
455 if (Object.keys(results.windows).every(function (v) { return !results.windows[v].path; })) {
456 results.issues.push({
457 id: 'WINDOWS_STORE_SDK_NOT_INSTALLED',
458 type: 'error',
459 message: __('Microsoft Windows Store SDK not found.') + '\n' +
460 __('You will be unable to build Windows Store apps.')
461 });
462 return finalize();
463 }
464
465 // fetch all Windows 10 SDK install information
466 var win10 = '10.0'
467 if (results.windows[win10] && results.windows[win10].path) {
468 var sdks_path = path.join(results.windows[win10].path, 'Extension SDKs', 'WindowsDesktop');
469 if (fs.existsSync(sdks_path)) {
470 results.windows[win10].sdks = fs.readdirSync(sdks_path);
471 }
472 }
473
474 var preferred = options.preferredWindowsSDK;
475 if (!results.windows[preferred] || !results.windows[preferred].supported) {
476 preferred = Object.keys(results.windows).filter(function (v) { return results.windows[v].supported; }).sort().pop();
477 }
478 if (preferred) {
479 results.windows[preferred].selected = true;
480 }
481
482 finalize();
483 });
484 });
485 });
486}