From b57317bdc25e3d48bb014faa7c063bc289682a1c Mon Sep 17 00:00:00 2001 From: Braden Shepherdson Date: Fri, 27 Sep 2013 11:48:55 -0400 Subject: [PATCH] Refactoring Android project-level and platform scripts to use Q.js This eliminates the use of shelljs.exec's sync mode, which is the source of the filehandle leaks that can cause EMFILE on OSX, and are CPU-intensive everywhere. Tested locally, needs poking before it gets released. --- bin/check_reqs | 7 +- bin/create | 5 +- bin/lib/check_reqs.js | 61 +- bin/lib/create.js | 141 +- bin/node_modules/q/CONTRIBUTING.md | 40 + bin/node_modules/q/LICENSE | 19 + bin/node_modules/q/README.md | 813 +++++++ .../q/benchmark/compare-with-callbacks.js | 71 + bin/node_modules/q/benchmark/scenarios.js | 36 + bin/node_modules/q/package.json | 93 + bin/node_modules/q/q.js | 1937 +++++++++++++++++ bin/node_modules/q/queue.js | 35 + bin/package.json | 7 +- bin/templates/cordova/build | 12 +- bin/templates/cordova/clean | 12 +- bin/templates/cordova/lib/build.js | 24 +- bin/templates/cordova/lib/clean.js | 13 +- bin/templates/cordova/lib/device.js | 85 +- bin/templates/cordova/lib/emulator.js | 349 ++- bin/templates/cordova/lib/exec.js | 43 + bin/templates/cordova/lib/install-device | 14 +- bin/templates/cordova/lib/install-emulator | 14 +- bin/templates/cordova/lib/list-devices | 13 +- .../cordova/lib/list-emulator-images | 13 +- .../cordova/lib/list-started-emulators | 13 +- bin/templates/cordova/lib/log.js | 30 +- bin/templates/cordova/lib/run.js | 124 +- bin/templates/cordova/lib/start-emulator | 15 +- bin/templates/cordova/log | 11 +- bin/templates/cordova/run | 12 +- bin/update | 5 +- 31 files changed, 3604 insertions(+), 463 deletions(-) create mode 100644 bin/node_modules/q/CONTRIBUTING.md create mode 100644 bin/node_modules/q/LICENSE create mode 100644 bin/node_modules/q/README.md create mode 100644 bin/node_modules/q/benchmark/compare-with-callbacks.js create mode 100644 bin/node_modules/q/benchmark/scenarios.js create mode 100644 bin/node_modules/q/package.json create mode 100644 bin/node_modules/q/q.js create mode 100644 bin/node_modules/q/queue.js create mode 100644 bin/templates/cordova/lib/exec.js diff --git a/bin/check_reqs b/bin/check_reqs index 4a8abee4..2ac87521 100755 --- a/bin/check_reqs +++ b/bin/check_reqs @@ -21,7 +21,8 @@ var check_reqs = require('./lib/check_reqs'); -if(!check_reqs.run()) { - process.exit(2); -} +check_reqs.run().done(null, function(err) { + console.log(err); + process.exit(2); +}); diff --git a/bin/create b/bin/create index 020bf862..9ab6689b 100755 --- a/bin/create +++ b/bin/create @@ -31,6 +31,9 @@ if(args.length < 3 || (args[2] == '--help' || args[2] == '/?' || args[2] == '-h' console.log(' : Project name'); process.exit(1); } else { - create.createProject(args[2], args[3], args[4], args[5]); + create.createProject(args[2], args[3], args[4], args[5]).done(null, function(err) { + console.error(err); + process.exit(2); + }); } diff --git a/bin/lib/check_reqs.js b/bin/lib/check_reqs.js index c064499f..80f88a84 100644 --- a/bin/lib/check_reqs.js +++ b/bin/lib/check_reqs.js @@ -20,6 +20,8 @@ */ var shell = require('shelljs'), + child_process = require('child_process'), + Q = require('q'), path = require('path'), fs = require('fs'), ROOT = path.join(__dirname, '..', '..'); @@ -36,43 +38,54 @@ module.exports.get_target = function() { } } +// Returns a promise. module.exports.check_ant = function() { - var test = shell.exec('ant -version', {silent:true, async:false}); - if(test.code > 0) { - console.error('ERROR : executing command \'ant\', make sure you have ant installed and added to your path.'); - return false; - } - return true; + var d = Q.defer(); + child_process.exec('ant -version', function(err, stdout, stderr) { + if (err) d.reject(new Error('ERROR : executing command \'ant\', make sure you have ant installed and added to your path.')); + else d.resolve(); + }); + return d.promise; } +// Returns a promise. module.exports.check_java = function() { if(process.env.JAVA_HOME) { - var test = shell.exec('java', {silent:true, async:false}); - if(test.code > 0) { - console.error('ERROR : executing command \'java\', make sure you java environment is set up. Including your JDK and JRE.'); - return false; - } - return true; + var d = Q.defer(); + child_process.exec('java', function(err, stdout, stderr) { + if(err) d.reject(new Error('ERROR : executing command \'java\', make sure you java environment is set up. Including your JDK and JRE.')); + else d.resolve(); + }); + return d.promise; } else { - console.error('ERROR : Make sure JAVA_HOME is set, as well as paths to your JDK and JRE for java.'); - return false; + return Q.reject(new Error('ERROR : Make sure JAVA_HOME is set, as well as paths to your JDK and JRE for java.')); } } +// Returns a promise. module.exports.check_android = function() { var valid_target = this.get_target(); - var targets = shell.exec('android list targets', {silent:true, async:false}); + var d = Q.defer(); + child_process.exec('android list targets', function(err, stdout, stderr) { + if (err) d.reject(stderr); + else d.resolve(stdout); + }); - if(targets.code > 0 && targets.output.match(/command\snot\sfound/)) { - console.error('The command \"android\" failed. Make sure you have the latest Android SDK installed, and the \"android\" command (inside the tools/ folder) is added to your path.'); - return false; - } else if(!targets.output.match(valid_target)) { - console.error('Please install Android target ' + valid_target.split('-')[1] + ' (the Android newest SDK). Make sure you have the latest Android tools installed as well. Run \"android\" from your command-line to install/update any missing SDKs or tools.'); - return false; - } - return true; + return d.promise.then(function(output) { + if (!output.match(valid_target)) { + return Q.reject(new Error('Please install Android target ' + valid_target.split('-')[1] + ' (the Android newest SDK). Make sure you have the latest Android tools installed as well. Run \"android\" from your command-line to install/update any missing SDKs or tools.')); + } + return Q(); + }, function(stderr) { + if (stderr.match(/command\snot\sfound/)) { + return Q.reject(new Error('The command \"android\" failed. Make sure you have the latest Android SDK installed, and the \"android\" command (inside the tools/ folder) is added to your path.')); + } else { + return Q.reject(new Error('An error occurred while listing Android targets')); + } + }); } +// Returns a promise. module.exports.run = function() { - return this.check_ant() && this.check_java && this.check_android(); + return Q.all([this.check_ant(), this.check_java, this.check_android()]); } diff --git a/bin/lib/create.js b/bin/lib/create.js index f34a244a..9ba460d5 100755 --- a/bin/lib/create.js +++ b/bin/lib/create.js @@ -19,27 +19,25 @@ under the License. */ var shell = require('shelljs'), + child_process = require('child_process'), + Q = require('q'), path = require('path'), fs = require('fs'), check_reqs = require('./check_reqs'), ROOT = path.join(__dirname, '..', '..'); -function exec(command) { - var result; +// Returns a promise. +function exec(command, opt_cwd) { + var d = Q.defer(); try { - result = shell.exec(command, {silent:false, async:false}); + child_process.exec(command, { cwd: opt_cwd }, function(err, stdout, stderr) { + if (err) d.reject(err); + else d.resolve(stdout); + }); } catch(e) { - console.error('Command error on execuation : ' + command); - console.error(e); - process.exit(2); - } - if(result && result.code > 0) { - console.error('Command failed to execute : ' + command); - console.error(result.output); - process.exit(2); - } else { - return result; + return Q.reject('Command error on execution: ' + command + '\n' + e); } + return d.promise; } function setShellFatal(value, func) { @@ -49,19 +47,20 @@ function setShellFatal(value, func) { shell.config.fatal = oldVal; } +// Returns a promise. function ensureJarIsBuilt(version, target_api) { var isDevVersion = /-dev$/.test(version); if (isDevVersion || !fs.existsSync(path.join(ROOT, 'framework', 'cordova-' + version + '.jar')) && fs.existsSync(path.join(ROOT, 'framework'))) { var valid_target = check_reqs.get_target(); console.log('Building cordova-' + version + '.jar'); // update the cordova-android framework for the desired target - exec('android --silent update lib-project --target "' + target_api + '" --path "' + path.join(ROOT, 'framework') + '"'); - // compile cordova.js and cordova.jar - var cwd = process.cwd(); - process.chdir(path.join(ROOT, 'framework')); - exec('ant jar'); - process.chdir(cwd); + return exec('android --silent update lib-project --target "' + target_api + '" --path "' + path.join(ROOT, 'framework') + '"') + .then(function() { + // compile cordova.js and cordova.jar + return exec('ant jar', path.join(ROOT, 'framework')); + }); } + return Q(); } function copyJsAndJar(projectPath, version) { @@ -100,6 +99,8 @@ function copyScripts(projectPath) { * - `package_name`{String} Package name, following reverse-domain style convention. * - `project_name` {String} Project name. * - 'project_template_dir' {String} Path to project template (override). + * + * Returns a promise. */ exports.createProject = function(project_path, package_name, project_name, project_template_dir) { @@ -123,72 +124,72 @@ exports.createProject = function(project_path, package_name, project_name, proje // Check if project already exists if(fs.existsSync(project_path)) { - console.error('Project already exists! Delete and recreate'); - process.exit(2); + return Q.reject('Project already exists! Delete and recreate'); } if (!/[a-zA-Z0-9_]+\.[a-zA-Z0-9_](.[a-zA-Z0-9_])*/.test(package_name)) { - console.error('Package name must look like: com.company.Name'); - process.exit(2); + return Q.reject('Package name must look like: com.company.Name'); } // Check that requirements are met and proper targets are installed - if(!check_reqs.run()) { - process.exit(2); - } + return check_reqs.run() + .then(function() { + // Log the given values for the project + console.log('Creating Cordova project for the Android platform:'); + console.log('\tPath: ' + project_path); + console.log('\tPackage: ' + package_name); + console.log('\tName: ' + project_name); + console.log('\tAndroid target: ' + target_api); - // Log the given values for the project - console.log('Creating Cordova project for the Android platform:'); - console.log('\tPath: ' + project_path); - console.log('\tPackage: ' + package_name); - console.log('\tName: ' + project_name); - console.log('\tAndroid target: ' + target_api); + // build from source. distro should have these files + return ensureJarIsBuilt(VERSION, target_api); + }).then(function() { + console.log('Copying template files...'); - // build from source. distro should have these files - ensureJarIsBuilt(VERSION, target_api); + setShellFatal(true, function() { + // copy project template + shell.cp('-r', path.join(project_template_dir, 'assets'), project_path); + shell.cp('-r', path.join(project_template_dir, 'res'), project_path); + // Manually create directories that would be empty within the template (since git doesn't track directories). + shell.mkdir(path.join(project_path, 'libs')); - console.log('Copying template files...'); + // copy cordova.js, cordova.jar and res/xml + shell.cp('-r', path.join(ROOT, 'framework', 'res', 'xml'), path.join(project_path, 'res')); + copyJsAndJar(project_path, VERSION); - setShellFatal(true, function() { - // copy project template - shell.cp('-r', path.join(project_template_dir, 'assets'), project_path); - shell.cp('-r', path.join(project_template_dir, 'res'), project_path); - // Manually create directories that would be empty within the template (since git doesn't track directories). - shell.mkdir(path.join(project_path, 'libs')); + // interpolate the activity name and package + shell.mkdir('-p', activity_dir); + shell.cp('-f', path.join(project_template_dir, 'Activity.java'), activity_path); + shell.sed('-i', /__ACTIVITY__/, safe_activity_name, activity_path); + shell.sed('-i', /__NAME__/, project_name, path.join(project_path, 'res', 'values', 'strings.xml')); + shell.sed('-i', /__ID__/, package_name, activity_path); - // copy cordova.js, cordova.jar and res/xml - shell.cp('-r', path.join(ROOT, 'framework', 'res', 'xml'), path.join(project_path, 'res')); - copyJsAndJar(project_path, VERSION); - - // interpolate the activity name and package - shell.mkdir('-p', activity_dir); - shell.cp('-f', path.join(project_template_dir, 'Activity.java'), activity_path); - shell.sed('-i', /__ACTIVITY__/, safe_activity_name, activity_path); - shell.sed('-i', /__NAME__/, project_name, path.join(project_path, 'res', 'values', 'strings.xml')); - shell.sed('-i', /__ID__/, package_name, activity_path); - - shell.cp('-f', path.join(project_template_dir, 'AndroidManifest.xml'), manifest_path); - shell.sed('-i', /__ACTIVITY__/, safe_activity_name, manifest_path); - shell.sed('-i', /__PACKAGE__/, package_name, manifest_path); - shell.sed('-i', /__APILEVEL__/, target_api.split('-')[1], manifest_path); - copyScripts(project_path); + shell.cp('-f', path.join(project_template_dir, 'AndroidManifest.xml'), manifest_path); + shell.sed('-i', /__ACTIVITY__/, safe_activity_name, manifest_path); + shell.sed('-i', /__PACKAGE__/, package_name, manifest_path); + shell.sed('-i', /__APILEVEL__/, target_api.split('-')[1], manifest_path); + copyScripts(project_path); + }); + // Link it to local android install. + console.log('Running "android update project"'); + return exec('android --silent update project --target "'+target_api+'" --path "'+ project_path+'"'); + }).then(function() { + console.log('Project successfully created.'); }); - // Link it to local android install. - console.log('Running "android update project"'); - exec('android --silent update project --target "'+target_api+'" --path "'+ project_path+'"'); - console.log('Project successfully created.'); } +// Returns a promise. exports.updateProject = function(projectPath) { // Check that requirements are met and proper targets are installed - if (!check_reqs.run()) { - process.exit(2); - } - var version = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf-8').trim(); - var target_api = check_reqs.get_target(); - ensureJarIsBuilt(version, target_api); - copyJsAndJar(projectPath, version); - copyScripts(projectPath); - console.log('Android project is now at version ' + version); + return check_reqs.run() + .then(function() { + var version = fs.readFileSync(path.join(ROOT, 'VERSION'), 'utf-8').trim(); + var target_api = check_reqs.get_target(); + return ensureJarIsBuilt(version, target_api); + }).then(function() { + copyJsAndJar(projectPath, version); + copyScripts(projectPath); + console.log('Android project is now at version ' + version); + }); }; diff --git a/bin/node_modules/q/CONTRIBUTING.md b/bin/node_modules/q/CONTRIBUTING.md new file mode 100644 index 00000000..500ab17b --- /dev/null +++ b/bin/node_modules/q/CONTRIBUTING.md @@ -0,0 +1,40 @@ + +For pull requests: + +- Be consistent with prevalent style and design decisions. +- Add a Jasmine spec to `specs/q-spec.js`. +- Use `npm test` to avoid regressions. +- Run tests in `q-spec/run.html` in as many supported browsers as you + can find the will to deal with. +- Do not build minified versions; we do this each release. +- If you would be so kind, add a note to `CHANGES.md` in an + appropriate section: + + - `Next Major Version` if it introduces backward incompatibilities + to code in the wild using documented features. + - `Next Minor Version` if it adds a new feature. + - `Next Patch Version` if it fixes a bug. + +For releases: + +- Run `npm test`. +- Run tests in `q-spec/run.html` in a representative sample of every + browser under the sun. +- Run `npm run cover` and make sure you're happy with the results. +- Run `npm run minify` and be sure to commit the resulting `q.min.js`. +- Note the Gzipped size output by the previous command, and update + `README.md` if it has changed to 1 significant digit. +- Stash any local changes. +- Update `CHANGES.md` to reflect all changes in the differences + between `HEAD` and the previous tagged version. Give credit where + credit is due. +- Update `README.md` to address all new, non-experimental features. +- Update the API reference on the Wiki to reflect all non-experimental + features. +- Use `npm version major|minor|patch` to update `package.json`, + commit, and tag the new version. +- Use `npm publish` to send up a new release. +- Send an email to the q-continuum mailing list announcing the new + release and the notes from the change log. This helps folks + maintaining other package ecosystems. + diff --git a/bin/node_modules/q/LICENSE b/bin/node_modules/q/LICENSE new file mode 100644 index 00000000..76c5fe4c --- /dev/null +++ b/bin/node_modules/q/LICENSE @@ -0,0 +1,19 @@ + +Copyright 2009–2012 Kristopher Michael Kowal. All rights reserved. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/bin/node_modules/q/README.md b/bin/node_modules/q/README.md new file mode 100644 index 00000000..c0f513ce --- /dev/null +++ b/bin/node_modules/q/README.md @@ -0,0 +1,813 @@ +[![Build Status](https://secure.travis-ci.org/kriskowal/q.png?branch=master)](http://travis-ci.org/kriskowal/q) + + + Promises/A+ logo + + +If a function cannot return a value or throw an exception without +blocking, it can return a promise instead. A promise is an object +that represents the return value or the thrown exception that the +function may eventually provide. A promise can also be used as a +proxy for a [remote object][Q-Connection] to overcome latency. + +[Q-Connection]: https://github.com/kriskowal/q-connection + +On the first pass, promises can mitigate the “[Pyramid of +Doom][POD]”: the situation where code marches to the right faster +than it marches forward. + +[POD]: http://calculist.org/blog/2011/12/14/why-coroutines-wont-work-on-the-web/ + +```javascript +step1(function (value1) { + step2(value1, function(value2) { + step3(value2, function(value3) { + step4(value3, function(value4) { + // Do something with value4 + }); + }); + }); +}); +``` + +With a promise library, you can flatten the pyramid. + +```javascript +Q.fcall(promisedStep1) +.then(promisedStep2) +.then(promisedStep3) +.then(promisedStep4) +.then(function (value4) { + // Do something with value4 +}) +.catch(function (error) { + // Handle any error from all above steps +}) +.done(); +``` + +With this approach, you also get implicit error propagation, just like `try`, +`catch`, and `finally`. An error in `promisedStep1` will flow all the way to +the `catch` function, where it’s caught and handled. (Here `promisedStepN` is +a version of `stepN` that returns a promise.) + +The callback approach is called an “inversion of control”. +A function that accepts a callback instead of a return value +is saying, “Don’t call me, I’ll call you.”. Promises +[un-invert][IOC] the inversion, cleanly separating the input +arguments from control flow arguments. This simplifies the +use and creation of API’s, particularly variadic, +rest and spread arguments. + +[IOC]: http://www.slideshare.net/domenicdenicola/callbacks-promises-and-coroutines-oh-my-the-evolution-of-asynchronicity-in-javascript + + +## Getting Started + +The Q module can be loaded as: + +- A ``