From 95c78ba14c4e0e8685fea765a5741c55ec7c6e85 Mon Sep 17 00:00:00 2001 From: Patrick Stadler Date: Thu, 13 Mar 2014 16:33:13 +0100 Subject: [PATCH] switch tabs to spaces. jshint is now much stricter. bump version. --- README.md | 5 - bin/fly.js | 42 +-- gulpfile.js | 84 +++-- lib/briefing.js | 78 ++-- lib/flight.js | 92 ++--- lib/flightplan.js | 526 +++++++++++++------------- lib/local.js | 6 +- lib/logger.js | 254 ++++++------- lib/remote.js | 82 ++-- lib/transport/commands.js | 76 ++-- lib/transport/index.js | 764 +++++++++++++++++++------------------- lib/transport/shell.js | 182 ++++----- lib/transport/ssh.js | 133 ++++--- package.json | 2 +- 14 files changed, 1166 insertions(+), 1160 deletions(-) diff --git a/README.md b/README.md index 666dba2..31e476b 100644 --- a/README.md +++ b/README.md @@ -548,10 +548,5 @@ transport.abort('Severe turbulences over the atlantic ocean!'); -## What's planned? - -- Add possibility to define a `sudoUser` per host with `briefing()`. -- Tests will be implemented with upcoming releases. A part of this will be driven by bug reports. - [npm-url]: https://npmjs.org/package/flightplan [npm-image]: https://badge.fury.io/js/flightplan.png \ No newline at end of file diff --git a/bin/fly.js b/bin/fly.js index 5053473..10207a6 100755 --- a/bin/fly.js +++ b/bin/fly.js @@ -1,42 +1,42 @@ #!/usr/bin/env node var path = require('path') - , fs = require('fs') - , program = require('commander') - , logger = new (require('../lib/logger'))() - , version = require('../package.json').version; + , fs = require('fs') + , program = require('commander') + , logger = new (require('../lib/logger'))() + , version = require('../package.json').version; program - .usage(' [options]') - .version(version) - .option('-p, --plan ', 'path to flightplan (default: flightplan.js)', 'flightplan.js') - .option('-u, --username ', 'user for connecting to remote hosts') - .option('-d, --debug', 'enable debug mode') - .parse(process.argv); + .usage(' [options]') + .version(version) + .option('-p, --plan ', 'path to flightplan (default: flightplan.js)', 'flightplan.js') + .option('-u, --username ', 'user for connecting to remote hosts') + .option('-d, --debug', 'enable debug mode') + .parse(process.argv); var flightFile = path.join(process.cwd(), program.plan); if(!fs.existsSync(flightFile)) { - logger.error(logger.format('Unable to load %s', program.plan.white)); - process.exit(1); + logger.error(logger.format('Unable to load %s', program.plan.white)); + process.exit(1); } var flightplan = require(flightFile) - , options = { - username: program.username || null, - debug: program.debug || null - }; + , options = { + username: program.username || null, + debug: program.debug || null + }; var destination = program.args[0]; if(!flightplan.requiresDestination) { - logger.error(logger.format('%s is not a valid flightplan', program.plan.white)); - process.exit(1); + logger.error(logger.format('%s is not a valid flightplan', program.plan.white)); + process.exit(1); } if(!destination && flightplan.requiresDestination()) { - logger.error('Please specfiy a destination'); - program.help(); - process.exit(1); + logger.error('Please specfiy a destination'); + program.help(); + process.exit(1); } flightplan.start(destination, options); \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index d138af7..8910d24 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,46 +1,60 @@ var fs = require('fs') - , gulp = require('gulp') - , stylish = require('jshint-stylish') - , jshint = require('gulp-jshint') - , markdox = require('markdox'); + , gulp = require('gulp') + , stylish = require('jshint-stylish') + , jshint = require('gulp-jshint') + , markdox = require('markdox'); var sourceFiles = ['*.js', 'lib/**/*.js', 'bin/**/*.js']; gulp.task('lint', function() { - var jshintOptions = { - laxcomma: true - }; - return gulp.src(sourceFiles) - .pipe(jshint(jshintOptions)) - .pipe(jshint.reporter(stylish)); + var jshintOptions = { + laxcomma: true, + laxbreak: true, + node: true, + curly: true, + camelcase: true, + eqeqeq: true, + maxdepth: 3, + maxlen: 100, + newcap: true, + noempty: true, + latedef: true, + noarg: true, + unused: true, + trailing: true, + indent: 2 + }; + return gulp.src(sourceFiles) + .pipe(jshint(jshintOptions)) + .pipe(jshint.reporter(stylish)); }); gulp.task('docs', function(taskFinished) { - var sources = ['lib/flightplan.js', 'lib/transport/transport.js'] - , readme = 'README.md' - , tmpFile = 'docs/API.md'; - - var options = { - template: 'docs/template.md.ejs', - output: tmpFile - }; - - markdox.process(sources, options, function() { - var docsStr = fs.readFileSync(tmpFile, 'utf8') - , readmeStr = fs.readFileSync(readme, 'utf8'); - - docsStr = docsStr - .replace(/'/g, "'") - .replace(/"/g, '"') - .replace(/&/g, '&'); - readmeStr = readmeStr.replace(/()(?:\r|\n|.)+()/gm - , "$1" + docsStr + "$2"); - - fs.writeFileSync(readme, readmeStr); - fs.unlinkSync(tmpFile); - console.log('Documentation generated.'); - taskFinished(); - }); + var sources = ['lib/flightplan.js', 'lib/transport/transport.js'] + , readme = 'README.md' + , tmpFile = 'docs/API.md'; + + var options = { + template: 'docs/template.md.ejs', + output: tmpFile + }; + + markdox.process(sources, options, function() { + var docsStr = fs.readFileSync(tmpFile, 'utf8') + , readmeStr = fs.readFileSync(readme, 'utf8'); + + docsStr = docsStr + .replace(/'/g, "'") + .replace(/"/g, '"') + .replace(/&/g, '&'); + readmeStr = readmeStr.replace(/()(?:\r|\n|.)+()/gm + , "$1" + docsStr + "$2"); + + fs.writeFileSync(readme, readmeStr); + fs.unlinkSync(tmpFile); + console.log('Documentation generated.'); + taskFinished(); + }); }); gulp.task('default', ['lint']); \ No newline at end of file diff --git a/lib/briefing.js b/lib/briefing.js index e42836b..aa1a83a 100644 --- a/lib/briefing.js +++ b/lib/briefing.js @@ -1,51 +1,49 @@ -var util = require('util'); - function Briefing(flightplan, config) { - this.flightplan = flightplan; - config = config || {}; - this.debug = config.debug || false; - this.destinations = config.destinations || {}; - this.flightplan.logger.enableDebug(this.debug); + this.flightplan = flightplan; + config = config || {}; + this.debug = config.debug || false; + this.destinations = config.destinations || {}; + this.flightplan.logger.enableDebug(this.debug); } Briefing.prototype = { - applyOptions: function(options) { - var destinations = this.getDestinations(); - if(options.username && destinations) { - destinations.forEach(function(destination) { - var hosts = this.getHostsForDestination(destination); - for(var i=0, len=hosts.length; i < len; i++) { - hosts[i].username = options.username; - } - }.bind(this)); - } - if(options.debug !== undefined && options.debug !== null) { - this.debug = options.debug; - this.flightplan.logger.enableDebug(this.debug); - } - }, + applyOptions: function(options) { + var destinations = this.getDestinations(); + if(options.username && destinations) { + destinations.forEach(function(destination) { + var hosts = this.getHostsForDestination(destination); + for(var i=0, len=hosts.length; i < len; i++) { + hosts[i].username = options.username; + } + }.bind(this)); + } + if(options.debug !== undefined && options.debug !== null) { + this.debug = options.debug; + this.flightplan.logger.enableDebug(this.debug); + } + }, - getDestinations: function() { - return Object.keys(this.destinations); - }, + getDestinations: function() { + return Object.keys(this.destinations); + }, - getHostsForDestination: function(destination) { - try { - var hosts = this.destinations[destination]; - return (hosts instanceof Array) ? hosts : [hosts]; - } catch(e) { - return null; - } - }, + getHostsForDestination: function(destination) { + try { + var hosts = this.destinations[destination]; + return (hosts instanceof Array) ? hosts : [hosts]; + } catch(e) { + return null; + } + }, - hasDestination: function(destination) { - try { - return !!this.destinations[destination]; - } catch(e) { - return false; - } - } + hasDestination: function(destination) { + try { + return !!this.destinations[destination]; + } catch(e) { + return false; + } + } }; module.exports = Briefing; \ No newline at end of file diff --git a/lib/flight.js b/lib/flight.js index 4937d2b..9d1375f 100644 --- a/lib/flight.js +++ b/lib/flight.js @@ -1,66 +1,66 @@ var Fiber = require('fibers') - , Future = require('fibers/future'); + , Future = require('fibers/future'); function Flight(flightplan, transportClass, fn) { - this.flightplan = flightplan; - this.fn = fn; - this.transportClass = transportClass; - this.logger = flightplan.logger; - this.hosts = null; - this.status = { - aborted: false, - crashRecordings: null, - executionTime: 0 // milliseconds - }; + this.flightplan = flightplan; + this.fn = fn; + this.transportClass = transportClass; + this.logger = flightplan.logger; + this.hosts = null; + this.status = { + aborted: false, + crashRecordings: null, + executionTime: 0 // milliseconds + }; } Flight.prototype = { - start: function(destination) { - this.hosts = this.flightplan.briefing().getHostsForDestination(destination); - this.__start(); - return this.getStatus(); - }, + start: function(destination) { + this.hosts = this.flightplan.briefing().getHostsForDestination(destination); + this.__start(); + return this.getStatus(); + }, - abort: function(msg) { - throw new Error(msg); - }, + abort: function(msg) { + throw new Error(msg); + }, - isAborted: function() { - return this.status.aborted; - }, + isAborted: function() { + return this.status.aborted; + }, - getStatus: function() { - return this.status; - }, + getStatus: function() { + return this.status; + }, - __start: function() { - var future = new Future(); + __start: function() { + var future = new Future(); - var task = function() { - Fiber(function() { - var t = process.hrtime(); + var task = function() { + new Fiber(function() { + var t = process.hrtime(); - var transport = new this.transportClass(this); - try { - this.fn(transport); - } catch(e) { - this.status.aborted = true; - this.status.crashRecordings = e.message || null; - this.flightplan.abort(); - } - transport.close(); + var transport = new this.transportClass(this); + try { + this.fn(transport); + } catch(e) { + this.status.aborted = true; + this.status.crashRecordings = e.message || null; + this.flightplan.abort(); + } + transport.close(); - this.status.executionTime = process.hrtime(t); + this.status.executionTime = process.hrtime(t); - return future.return(); - }.bind(this)).run(); + return future.return(); + }.bind(this)).run(); - return future; - }.bind(this); + return future; + }.bind(this); - Future.wait(task()); - } + Future.wait(task()); + } }; module.exports = Flight; \ No newline at end of file diff --git a/lib/flightplan.js b/lib/flightplan.js index 06c4038..ac2bd7e 100644 --- a/lib/flightplan.js +++ b/lib/flightplan.js @@ -1,9 +1,9 @@ var Fiber = require('fibers') - , prettyTime = require('pretty-hrtime') - , Logger = require('./logger') - , Briefing = require('./briefing') - , LocalFlight = require('./local') - , RemoteFlight = require('./remote'); + , prettyTime = require('pretty-hrtime') + , Logger = require('./logger') + , Briefing = require('./briefing') + , LocalFlight = require('./local') + , RemoteFlight = require('./remote'); /** * A flightplan is a set of subsequent flights to be executed on one or more @@ -61,267 +61,271 @@ var Fiber = require('fibers') * @return flightplan */ function Flightplan() { - this._briefing = null; - this.flights = []; - this.target = { - destination: null, - hosts: [] - }; - this.status = { - aborted: false, - executionTime: 0 - }; - this.hasRemoteFlights = false; - this.logger = new Logger(); - - this.successCallback = function() {}; - this.disasterCallback = function() {}; - this.debriefingCallback = function() {}; - - process.on('SIGINT', function() { - this.logger.space(); - this.logger.error('Flightplan was interrupted'.error); - process.exit(1); - }.bind(this)); - - process.on('uncaughtException', function(err) { - this.logger.error(err.stack); - this.disasterCallback(); - this.debriefingCallback(); - this.logger.error('Flightplan aborted'.error); - process.exit(1); - }.bind(this)); - - module.parent.parent.exports = this; // expose to user's flightplan.js + this._briefing = null; + this.flights = []; + this.target = { + destination: null, + hosts: [] + }; + this.status = { + aborted: false, + executionTime: 0 + }; + this.hasRemoteFlights = false; + this.logger = new Logger(); + + this.successCallback = function() {}; + this.disasterCallback = function() {}; + this.debriefingCallback = function() {}; + + process.on('SIGINT', function() { + this.logger.space(); + this.logger.error('Flightplan was interrupted'.error); + process.exit(1); + }.bind(this)); + + process.on('uncaughtException', function(err) { + this.logger.error(err.stack); + this.disasterCallback(); + this.debriefingCallback(); + this.logger.error('Flightplan aborted'.error); + process.exit(1); + }.bind(this)); + + module.parent.parent.exports = this; // expose to user's flightplan.js } Flightplan.prototype = { - /** - * Configure the flightplan's destinations with `briefing()`. Without a - * proper briefing you can't do remote flights which require at - * least one destination. Each destination consists of one ore more hosts. - * - * Values in the hosts section are passed directly to the `connect()` - * method of [mscdex/ssh2](https://github.com/mscdex/ssh2#connection-methods) - * with one exception: `privateKey` needs to be passed as a string - * containing the path to the keyfile instead of the key itself. - * - * ```javascript - * plan.briefing({ - * destinations: { - * // run with `fly staging` - * 'staging': { - * // see: https://github.com/mscdex/ssh2#connection-methods - * host: 'staging.pstadler.sh', - * username: 'pstadler', - * agent: process.env.SSH_AUTH_SOCK - * }, - * // run with `fly production` - * 'production': [ - * { - * host: 'www1.pstadler.sh', - * username: 'pstadler', - * agent: process.env.SSH_AUTH_SOCK - * }, - * { - * host: 'www2.pstadler.sh', - * username: 'pstadler', - * agent: process.env.SSH_AUTH_SOCK - * }, - * ] - * } - * }); - * ``` - * - * You can override the `username` value of all hosts by calling `fly` with - * the `-u|--username` option: - * - * ```bash - * fly production --username=admin - * ``` - * - * @method briefing(config) - * @return this - */ - briefing: function(config) { - if(!config) { - return this._briefing; - } - this._briefing = new Briefing(this, config); - return this; - }, - - /** - * Calling this method registers a local flight. Local flights are - * executed on your localhost. When `fn` gets called a `Transport` object - * is passed with the first argument. - * - * ```javascript - * plan.local(function(local) { - * local.echo('hello from your localhost.'); - * }); - * ``` - * - * @method local(fn) - * @return this - */ - local: function(fn) { - this.flights.push(new LocalFlight(this, fn)); - return this; - }, - - /** - * Calling this method registers a remote flight. Remote - * flights are executed on the current destination's remote hosts defined - * with `briefing()`. When `fn` gets called a `Transport` object is passed - * with the first argument. - * - * ```javascript - * plan.remote(function(remote) { - * remote.echo('hello from the remote host.'); - * }); - * ``` - * - * @method remote(fn) - * @return this - */ - remote: function(fn) { - this.flights.push(new RemoteFlight(this, fn)); - this.hasRemoteFlights = true; - return this; - }, - - /** - * `fn()` is called after the flightplan (and therefore all flights) - * succeeded. - * - * @method success(fn) - * @return this - */ - success: function(fn) { - this.successCallback = fn; - return this; - }, - - /** - * `fn()` is called after the flightplan was aborted. - * - * @method disaster(fn) - * @return this - */ - disaster: function(fn) { - this.disasterCallback = fn; - return this; - }, - - /** - * `fn()` is called at the very end of the flightplan's execution. - * - * @method debriefing(fn) - */ - debriefing: function(fn) { - this.debriefingCallback = fn; - return this; - }, - - - /** - * Whether the flightplan is aborted or not. - * - * @method isAborted() - * @return Boolean - */ - isAborted: function() { - return this.status.aborted; - }, - - /** - * Calling this method will abort the flightplan and prevent any further - * flights from being executed. - * - * ```javascript - * plan.abort(); - * ``` - * - * @method abort() - */ - abort: function() { - this.status.aborted = true; - }, - - requiresDestination: function() { - return this.hasRemoteFlights; - }, - - start: function(destination, options) { - this.target.destination = destination; - - if(this.briefing()) { - this.briefing().applyOptions(options); - } else { - this.briefing(options); - } - this.logger.debug('Briefing done'); - if(this.requiresDestination() && !this.briefing().hasDestination(this.target.destination)) { - this.logger.error((destination || '').warn, 'is not a valid destination'); - process.exit(1); - } - this.target.hosts = this.briefing().getHostsForDestination(this.target.destination); - - if(this.isAborted()) { - this.logger.error('Flightplan aborted'.error); - process.exit(1); - } - this.logger.info('Executing flightplan with'.info, String(this.flights.length).magenta - , 'planned flight(s) to'.info, (this.target.destination || 'localhost').warn); - this.logger.space(); - - Fiber(function() { - - var t = process.hrtime(); - - for(var i=0, len=this.flights.length; i < len; i++) { - var flight = this.flights[i]; - - this.logger.info('Flight'.info, this.logger.format('%s/%s', i+1, len).magenta, 'launched...'.info); - this.logger.space(); - - flight.start(this.target.destination); - - var status = flight.getStatus() - , flightNumber = this.logger.format('%s/%s', i+1, len).magenta - , executionTime = prettyTime(status.executionTime).magenta - , crashReason = !status.crashRecordings ? '' : this.logger.format('when %s', status.crashRecordings); - - if(flight.isAborted()) { - this.logger.error('Flight'.error, flightNumber, 'aborted after'.error, executionTime, crashReason); - this.logger.space(); - break; - } - this.logger.success('Flight'.success, flightNumber, 'landed after'.success, executionTime); - this.logger.space(); - } - - this.status.executionTime = process.hrtime(t); - - if(this.isAborted()) { - this.logger.error('Flightplan aborted after'.error - , prettyTime(this.status.executionTime).magenta); - this.disasterCallback(); - this.debriefingCallback(); - process.exit(1); - } else { - this.logger.success('Flightplan finished after'.success - , prettyTime(this.status.executionTime).magenta); - this.successCallback(); - this.debriefingCallback(); - process.exit(0); - } - - }.bind(this)).run(); - } + /** + * Configure the flightplan's destinations with `briefing()`. Without a + * proper briefing you can't do remote flights which require at + * least one destination. Each destination consists of one ore more hosts. + * + * Values in the hosts section are passed directly to the `connect()` + * method of [mscdex/ssh2](https://github.com/mscdex/ssh2#connection-methods) + * with one exception: `privateKey` needs to be passed as a string + * containing the path to the keyfile instead of the key itself. + * + * ```javascript + * plan.briefing({ + * destinations: { + * // run with `fly staging` + * 'staging': { + * // see: https://github.com/mscdex/ssh2#connection-methods + * host: 'staging.pstadler.sh', + * username: 'pstadler', + * agent: process.env.SSH_AUTH_SOCK + * }, + * // run with `fly production` + * 'production': [ + * { + * host: 'www1.pstadler.sh', + * username: 'pstadler', + * agent: process.env.SSH_AUTH_SOCK + * }, + * { + * host: 'www2.pstadler.sh', + * username: 'pstadler', + * agent: process.env.SSH_AUTH_SOCK + * }, + * ] + * } + * }); + * ``` + * + * You can override the `username` value of all hosts by calling `fly` with + * the `-u|--username` option: + * + * ```bash + * fly production --username=admin + * ``` + * + * @method briefing(config) + * @return this + */ + briefing: function(config) { + if(!config) { + return this._briefing; + } + this._briefing = new Briefing(this, config); + return this; + }, + + /** + * Calling this method registers a local flight. Local flights are + * executed on your localhost. When `fn` gets called a `Transport` object + * is passed with the first argument. + * + * ```javascript + * plan.local(function(local) { + * local.echo('hello from your localhost.'); + * }); + * ``` + * + * @method local(fn) + * @return this + */ + local: function(fn) { + this.flights.push(new LocalFlight(this, fn)); + return this; + }, + + /** + * Calling this method registers a remote flight. Remote + * flights are executed on the current destination's remote hosts defined + * with `briefing()`. When `fn` gets called a `Transport` object is passed + * with the first argument. + * + * ```javascript + * plan.remote(function(remote) { + * remote.echo('hello from the remote host.'); + * }); + * ``` + * + * @method remote(fn) + * @return this + */ + remote: function(fn) { + this.flights.push(new RemoteFlight(this, fn)); + this.hasRemoteFlights = true; + return this; + }, + + /** + * `fn()` is called after the flightplan (and therefore all flights) + * succeeded. + * + * @method success(fn) + * @return this + */ + success: function(fn) { + this.successCallback = fn; + return this; + }, + + /** + * `fn()` is called after the flightplan was aborted. + * + * @method disaster(fn) + * @return this + */ + disaster: function(fn) { + this.disasterCallback = fn; + return this; + }, + + /** + * `fn()` is called at the very end of the flightplan's execution. + * + * @method debriefing(fn) + */ + debriefing: function(fn) { + this.debriefingCallback = fn; + return this; + }, + + + /** + * Whether the flightplan is aborted or not. + * + * @method isAborted() + * @return Boolean + */ + isAborted: function() { + return this.status.aborted; + }, + + /** + * Calling this method will abort the flightplan and prevent any further + * flights from being executed. + * + * ```javascript + * plan.abort(); + * ``` + * + * @method abort() + */ + abort: function() { + this.status.aborted = true; + }, + + requiresDestination: function() { + return this.hasRemoteFlights; + }, + + start: function(destination, options) { + this.target.destination = destination; + + if(this.briefing()) { + this.briefing().applyOptions(options); + } else { + this.briefing(options); + } + this.logger.debug('Briefing done'); + if(this.requiresDestination() && !this.briefing().hasDestination(this.target.destination)) { + this.logger.error((destination || '').warn, 'is not a valid destination'); + process.exit(1); + } + this.target.hosts = this.briefing().getHostsForDestination(this.target.destination); + + if(this.isAborted()) { + this.logger.error('Flightplan aborted'.error); + process.exit(1); + } + this.logger.info('Executing flightplan with'.info, String(this.flights.length).magenta + , 'planned flight(s) to'.info, (this.target.destination || 'localhost').warn); + this.logger.space(); + + new Fiber(function() { + + var t = process.hrtime(); + + for(var i=0, len=this.flights.length; i < len; i++) { + var flight = this.flights[i]; + + this.logger.info('Flight'.info, this.logger.format('%s/%s', i+1, len).magenta + , 'launched...'.info); + this.logger.space(); + + flight.start(this.target.destination); + + var status = flight.getStatus() + , flightNumber = this.logger.format('%s/%s', i+1, len).magenta + , executionTime = prettyTime(status.executionTime).magenta + , crashReason = !status.crashRecordings + ? '' + : this.logger.format('when %s', status.crashRecordings); + + if(flight.isAborted()) { + this.logger.error('Flight'.error, flightNumber, 'aborted after'.error + , executionTime, crashReason); + this.logger.space(); + break; + } + this.logger.success('Flight'.success, flightNumber, 'landed after'.success, executionTime); + this.logger.space(); + } + + this.status.executionTime = process.hrtime(t); + + if(this.isAborted()) { + this.logger.error('Flightplan aborted after'.error + , prettyTime(this.status.executionTime).magenta); + this.disasterCallback(); + this.debriefingCallback(); + process.exit(1); + } else { + this.logger.success('Flightplan finished after'.success + , prettyTime(this.status.executionTime).magenta); + this.successCallback(); + this.debriefingCallback(); + process.exit(0); + } + + }.bind(this)).run(); + } }; diff --git a/lib/local.js b/lib/local.js index 13cbb08..d298500 100644 --- a/lib/local.js +++ b/lib/local.js @@ -1,9 +1,9 @@ var util = require('util') - , Flight = require('./flight') - , ShellTransport = require('./transport/shell'); + , Flight = require('./flight') + , ShellTransport = require('./transport/shell'); function LocalFlight(flightplan, fn) { - LocalFlight.super_.call(this, flightplan, ShellTransport, fn); + LocalFlight.super_.call(this, flightplan, ShellTransport, fn); } util.inherits(LocalFlight, Flight); diff --git a/lib/logger.js b/lib/logger.js index 4ef175c..adbec58 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,14 +1,14 @@ var util = require('util') - , colors = require('colors'); + , colors = require('colors'); var messageTypes = { - log: 'yellow', - info: 'blue', - success: 'green', - warn: 'yellow', - error: 'red', - command: 'white', - debug: 'cyan' + log: 'yellow', + info: 'blue', + success: 'green', + warn: 'yellow', + error: 'red', + command: 'white', + debug: 'cyan' }; colors.setTheme(messageTypes); @@ -16,132 +16,132 @@ colors.setTheme(messageTypes); var __debug = false; // persistent function Logger(debug) { - __debug = (debug !== null && debug !== undefined) ? debug : __debug; - this.symbol = '✈'; - this.prefix = ''; - - this._logStream = function() { - var writePrefix = true; - return function(symbol, prefix, msg) { - var lines = msg.split('\n'); - var out = []; - for(var i=0, len=lines.length; i < len; i++) { - if(writePrefix && lines[i] !== '') { - out.push(this._format(symbol, prefix, lines[i])); - if(i+1 === lines.length) { - writePrefix = false; // stream will continue - } - } else { - if(i+1 !== lines.length || lines[i] === '') { - writePrefix = true; - } - out.push(lines[i]); - } - } - process.stdout.write(out.join('\n')); - }.bind(this); - }; - - this.stdout = (function() { - var logStream = this._logStream(); - return function() { - var data = this._parseArgs(arguments, false); - logStream('>'.grey, this.prefix, data); - }.bind(this); - }.bind(this))(); - - this.stderr = (function() { - var logStream = this._logStream(); - return function() { - var data = this._parseArgs(arguments, false); - logStream('>'.error, this.prefix, data); - }.bind(this); - }.bind(this))(); - - this.stdwarn = (function() { - var logStream = this._logStream(); - return function() { - var data = this._parseArgs(arguments, false); - logStream('>'.warn, this.prefix, data); - }.bind(this); - }.bind(this))(); + __debug = (debug !== null && debug !== undefined) ? debug : __debug; + this.symbol = '✈'; + this.prefix = ''; + + this._logStream = function() { + var writePrefix = true; + return function(symbol, prefix, msg) { + var lines = msg.split('\n'); + var out = []; + for(var i=0, len=lines.length; i < len; i++) { + if(writePrefix && lines[i] !== '') { + out.push(this._format(symbol, prefix, lines[i])); + if(i+1 === lines.length) { + writePrefix = false; // stream will continue + } + } else { + if(i+1 !== lines.length || lines[i] === '') { + writePrefix = true; + } + out.push(lines[i]); + } + } + process.stdout.write(out.join('\n')); + }.bind(this); + }; + + this.stdout = (function() { + var logStream = this._logStream(); + return function() { + var data = this._parseArgs(arguments, false); + logStream('>'.grey, this.prefix, data); + }.bind(this); + }.bind(this))(); + + this.stderr = (function() { + var logStream = this._logStream(); + return function() { + var data = this._parseArgs(arguments, false); + logStream('>'.error, this.prefix, data); + }.bind(this); + }.bind(this))(); + + this.stdwarn = (function() { + var logStream = this._logStream(); + return function() { + var data = this._parseArgs(arguments, false); + logStream('>'.warn, this.prefix, data); + }.bind(this); + }.bind(this))(); } Object.keys(messageTypes).forEach(function(type) { - Logger.prototype[type] = function() { - var msg = this._parseArgs(arguments); - this._log(this.symbol[type], this.prefix, msg); - }; + Logger.prototype[type] = function() { + var msg = this._parseArgs(arguments); + this._log(this.symbol[type], this.prefix, msg); + }; }); Logger.prototype = util._extend(Logger.prototype, { - enableDebug: function(flag) { - __debug = !!flag; - }, - - debugEnabled: function() { - return __debug; - }, - - clone: function() { - return new Logger(); - }, - - cloneWithPrefix: function(prefix) { - var logger = this.clone(); - logger.symbol = '●'; - logger.prefix = prefix; - logger._format = function(symbol, prefix, msg) { - return util.format('%s %s %s', (prefix || '').grey, symbol, msg); - }; - return logger; - }, - - command: function() { - var msg = this._parseArgs(arguments); - this._log('$'.command, this.prefix, msg.command); - }, - - debug: function() { - if(__debug) { - var msg = this._parseArgs(arguments); - this._log(this.symbol.debug, this.prefix, msg); - } - }, - - space: function() { - process.stdout.write('\n'); - }, - - log: function() { - var msg = this._parseArgs(arguments); - this._log(this.symbol.cyan, this.prefix, msg.cyan); - }, - - _log: function(symbol, prefix, msg) { - var lines = msg.split('\n'); - var out = []; - for(var i=0, len=lines.length; i < len; i++) { - var line = lines[i].trim(); - out.push(this._format(symbol, prefix, line)); - } - process.stdout.write(out.join('\n') + '\n'); - }, - - format: util.format, // convenience method - - _format: function(symbol, prefix, msg) { - return util.format('%s%s %s', symbol, prefix, msg); - }, - - _parseArgs: function(args, trim) { - var str = Array.prototype.slice.call(args, 0).join(' '); - if(trim !== false) { - str = str.trim(); - } - return str; - } + enableDebug: function(flag) { + __debug = !!flag; + }, + + debugEnabled: function() { + return __debug; + }, + + clone: function() { + return new Logger(); + }, + + cloneWithPrefix: function(prefix) { + var logger = this.clone(); + logger.symbol = '●'; + logger.prefix = prefix; + logger._format = function(symbol, prefix, msg) { + return util.format('%s %s %s', (prefix || '').grey, symbol, msg); + }; + return logger; + }, + + command: function() { + var msg = this._parseArgs(arguments); + this._log('$'.command, this.prefix, msg.command); + }, + + debug: function() { + if(__debug) { + var msg = this._parseArgs(arguments); + this._log(this.symbol.debug, this.prefix, msg); + } + }, + + space: function() { + process.stdout.write('\n'); + }, + + log: function() { + var msg = this._parseArgs(arguments); + this._log(this.symbol.cyan, this.prefix, msg.cyan); + }, + + _log: function(symbol, prefix, msg) { + var lines = msg.split('\n'); + var out = []; + for(var i=0, len=lines.length; i < len; i++) { + var line = lines[i].trim(); + out.push(this._format(symbol, prefix, line)); + } + process.stdout.write(out.join('\n') + '\n'); + }, + + format: util.format, // convenience method + + _format: function(symbol, prefix, msg) { + return util.format('%s%s %s', symbol, prefix, msg); + }, + + _parseArgs: function(args, trim) { + var str = Array.prototype.slice.call(args, 0).join(' '); + if(trim !== false) { + str = str.trim(); + } + return str; + } }); diff --git a/lib/remote.js b/lib/remote.js index 75d974b..88682ce 100644 --- a/lib/remote.js +++ b/lib/remote.js @@ -1,53 +1,53 @@ var util = require('util') - , Fiber = require('fibers') - , Future = require('fibers/future') - , Flight = require('./flight') - , SSHTransport = require('./transport/ssh'); + , Fiber = require('fibers') + , Future = require('fibers/future') + , Flight = require('./flight') + , SSHTransport = require('./transport/ssh'); function RemoteFlight(flightplan, fn) { - RemoteFlight.super_.call(this, flightplan, SSHTransport, fn); + RemoteFlight.super_.call(this, flightplan, SSHTransport, fn); - if(!this.flightplan.briefing()) { - this.logger.error("You can\'t do remote flights without a briefing."); - this.flightplan.abort(); - } + if(!this.flightplan.briefing()) { + this.logger.error("You can't do remote flights without a briefing."); + this.flightplan.abort(); + } } util.inherits(RemoteFlight, Flight); RemoteFlight.prototype.__start = function() { - var task = function(host) { - var future = new Future(); - var flight = new RemoteFlight(this.flightplan, this.fn); - Fiber(function() { - var t = process.hrtime(); - - var transport = new flight.transportClass(flight, host); - try { - flight.fn(transport); - } catch(e) { - this.status.aborted = true; - this.status.crashRecordings = e.message || null; - this.flightplan.abort(); - } - transport.close(); - - flight.status.executionTime = process.hrtime(t); - this.status.executionTime = flight.status.executionTime; - - return future.return(); - }.bind(this)).run(); - - return future; - }.bind(this); - - var tasks = []; - for(var i=0, len=this.hosts.length; i < len; i++) { - tasks.push(task(this.hosts[i])); - } - Future.wait(tasks); - - return this.status; + var task = function(host) { + var future = new Future(); + var flight = new RemoteFlight(this.flightplan, this.fn); + new Fiber(function() { + var t = process.hrtime(); + + var transport = new flight.transportClass(flight, host); + try { + flight.fn(transport); + } catch(e) { + this.status.aborted = true; + this.status.crashRecordings = e.message || null; + this.flightplan.abort(); + } + transport.close(); + + flight.status.executionTime = process.hrtime(t); + this.status.executionTime = flight.status.executionTime; + + return future.return(); + }.bind(this)).run(); + + return future; + }.bind(this); + + var tasks = []; + for(var i=0, len=this.hosts.length; i < len; i++) { + tasks.push(task(this.hosts[i])); + } + Future.wait(tasks); + + return this.status; }; diff --git a/lib/transport/commands.js b/lib/transport/commands.js index b570411..2492875 100644 --- a/lib/transport/commands.js +++ b/lib/transport/commands.js @@ -1,46 +1,44 @@ -var util = require('util'); - var common = [ - 'awk', - 'cat', - 'cd', - 'chgrp', - 'chmod', - 'chown', - 'cp', - 'echo', - 'find', - 'ftp', - 'grep', - 'groups', - 'hostname', - 'kill', - 'ln', - 'whoami', - 'ls', - 'mkdir', - 'mv', - 'ps', - 'pwd', - 'rm', - 'rmdir', - 'scp', - 'sed', - 'tail', - 'tar', - 'touch', - 'unzip', - 'whoami', - 'zip' + 'awk', + 'cat', + 'cd', + 'chgrp', + 'chmod', + 'chown', + 'cp', + 'echo', + 'find', + 'ftp', + 'grep', + 'groups', + 'hostname', + 'kill', + 'ln', + 'whoami', + 'ls', + 'mkdir', + 'mv', + 'ps', + 'pwd', + 'rm', + 'rmdir', + 'scp', + 'sed', + 'tail', + 'tar', + 'touch', + 'unzip', + 'whoami', + 'zip' ]; var extra = [ - 'git', - 'hg', - 'node', - 'npm', - 'rsync', - 'svn' + 'git', + 'hg', + 'node', + 'npm', + 'rsync', + 'svn' ]; module.exports = common.concat(extra); \ No newline at end of file diff --git a/lib/transport/index.js b/lib/transport/index.js index da89e4a..296fd0b 100644 --- a/lib/transport/index.js +++ b/lib/transport/index.js @@ -1,7 +1,7 @@ var util = require('util') - , Fiber = require('fibers') - , prompt = require('prompt') - , commands = require('./commands'); + , Fiber = require('fibers') + , prompt = require('prompt') + , commands = require('./commands'); /** * A transport is the interface you use during flights. Basically they @@ -43,413 +43,413 @@ var util = require('util') * @return transport */ function Transport(flight) { - this.flight = flight; - this.target = {}; - this.logger = flight.logger; + this.flight = flight; + this.target = {}; + this.logger = flight.logger; - this.options = { - silent: false, - failsafe: false - }; + this.options = { + silent: false, + failsafe: false + }; - this._execWith = ''; + this._execWith = ''; - commands.forEach(function(cmd) { - this[cmd] = function(args, opts) { - opts = this._parseOptions(opts); - return this.__exec(cmd, args, opts); - }; - }, this); + commands.forEach(function(cmd) { + this[cmd] = function(args, opts) { + opts = this._parseOptions(opts); + return this.__exec(cmd, args, opts); + }; + }, this); } Transport.prototype = { - /** - * To execute a command you have the choice between using `exec()` or one - * of the handy wrappers for often used commands: - * `transport.exec('ls -al')` is the same as `transport.ls('-al')`. If a - * command returns a non-zero exit code, the flightplan will be aborted and - * all subsequent commands and flights won't get executed. - * - * #### Options - * Options can be passed as a second argument. If `failsafe: true` is - * passed, the command is allowed to fail (i.e. exiting with a non-zero - * exit code), whereas `silent: true` will simply suppress its output. - * - * ```javascript - * // output of `ls -al` is suppressed - * transport.ls('-al', {silent: true}); - * - * // flightplan continues even if command fails with exit code `1` - * transport.ls('-al foo', {failsafe: true}); // ls: foo: No such file or directory - * - * // both options together - * transport.ls('-al foo', {silent: true, failsafe: true}); - * ``` - * - * To apply these options to multiple commands check out the docs of - * `transport.silent()` and `transport.failsafe()`. - * - * #### Return value - * Each command returns an object containing `code`, `stdout` and`stderr`: - * - * ```javascript - * var retval = transport.echo('Hello world'); - * console.log(retval); // { code: 0, stdout: 'Hello world\n', stderr: null } - * ``` - * - * @method exec(command[, options]) - * @return code: int, stdout: String, stderr: String - */ - exec: function(args, opts) { - args = args.split(' '); - cmd = args.shift(); - opts = this._parseOptions(opts); - return this.__exec(cmd, args.join(' '), opts); - }, + /** + * To execute a command you have the choice between using `exec()` or one + * of the handy wrappers for often used commands: + * `transport.exec('ls -al')` is the same as `transport.ls('-al')`. If a + * command returns a non-zero exit code, the flightplan will be aborted and + * all subsequent commands and flights won't get executed. + * + * #### Options + * Options can be passed as a second argument. If `failsafe: true` is + * passed, the command is allowed to fail (i.e. exiting with a non-zero + * exit code), whereas `silent: true` will simply suppress its output. + * + * ```javascript + * // output of `ls -al` is suppressed + * transport.ls('-al', {silent: true}); + * + * // flightplan continues even if command fails with exit code `1` + * transport.ls('-al foo', {failsafe: true}); // ls: foo: No such file or directory + * + * // both options together + * transport.ls('-al foo', {silent: true, failsafe: true}); + * ``` + * + * To apply these options to multiple commands check out the docs of + * `transport.silent()` and `transport.failsafe()`. + * + * #### Return value + * Each command returns an object containing `code`, `stdout` and`stderr`: + * + * ```javascript + * var retval = transport.echo('Hello world'); + * console.log(retval); // { code: 0, stdout: 'Hello world\n', stderr: null } + * ``` + * + * @method exec(command[, options]) + * @return code: int, stdout: String, stderr: String + */ + exec: function(args, opts) { + args = args.split(' '); + cmd = args.shift(); + opts = this._parseOptions(opts); + return this.__exec(cmd, args.join(' '), opts); + }, - /** - * Execute a command as another user with `sudo()`. It has the same - * signature as `exec()`. Per default, the user under which the command - * will be executed is "root". This can be changed by passing - * `user: "name"` with the second argument: - * - * ```javascript - * // will run: sudo -u root -i bash -c 'Hello world' - * transport.sudo('echo Hello world'); - * - * // will run sudo -u www -i bash -c 'Hello world' - * transport.sudo('echo Hello world', {user: 'www'}); - * - * // further options passed (see `exec()`) - * transport.sudo('echo Hello world', {user: 'www', silent: true, failsafe: true}); - * ``` - * - * Flightplan's `sudo()` requires a certain setup on your host. In order to - * make things work on a typical Ubuntu installation, follow these rules: - * - * ```bash - * # Scenario: - * # 'pstadler' is the user for connecting to the host and 'www' is the user - * # under which you want to execute commands with sudo. - * - * # 1. 'pstadler' has to be in the sudo group: - * $ groups pstadler - * pstadler : pstadler sudo - * - * # 2. 'pstadler' needs to be able to run sudo -u 'www' without a password. - * # In order to do this, add the following line to /etc/sudoers: - * pstadler ALL=(www) NOPASSWD: ALL - * - * # 3. user 'www' needs to have a login shell (e.g. bash, sh, zsh, ...) - * $ cat /etc/passwd | grep www - * www:x:1002:1002::/home/www:/bin/bash # GOOD - * www:x:1002:1002::/home/www:/bin/false # BAD - * ``` - * - * @method sudo(command[, options]) - * @return code: int, stdout: String, stderr: String - */ - sudo: function(args, opts) { - var user = util.format('-u %s', opts && opts.user ? opts.user : 'root'); - var format = "%s -i bash -c '%s'"; - args = util.format(format, user, args); - opts = this._parseOptions(opts); - return this.__exec('sudo', args, opts); - }, + /** + * Execute a command as another user with `sudo()`. It has the same + * signature as `exec()`. Per default, the user under which the command + * will be executed is "root". This can be changed by passing + * `user: "name"` with the second argument: + * + * ```javascript + * // will run: sudo -u root -i bash -c 'Hello world' + * transport.sudo('echo Hello world'); + * + * // will run sudo -u www -i bash -c 'Hello world' + * transport.sudo('echo Hello world', {user: 'www'}); + * + * // further options passed (see `exec()`) + * transport.sudo('echo Hello world', {user: 'www', silent: true, failsafe: true}); + * ``` + * + * Flightplan's `sudo()` requires a certain setup on your host. In order to + * make things work on a typical Ubuntu installation, follow these rules: + * + * ```bash + * # Scenario: + * # 'pstadler' is the user for connecting to the host and 'www' is the user + * # under which you want to execute commands with sudo. + * + * # 1. 'pstadler' has to be in the sudo group: + * $ groups pstadler + * pstadler : pstadler sudo + * + * # 2. 'pstadler' needs to be able to run sudo -u 'www' without a password. + * # In order to do this, add the following line to /etc/sudoers: + * pstadler ALL=(www) NOPASSWD: ALL + * + * # 3. user 'www' needs to have a login shell (e.g. bash, sh, zsh, ...) + * $ cat /etc/passwd | grep www + * www:x:1002:1002::/home/www:/bin/bash # GOOD + * www:x:1002:1002::/home/www:/bin/false # BAD + * ``` + * + * @method sudo(command[, options]) + * @return code: int, stdout: String, stderr: String + */ + sudo: function(args, opts) { + var user = util.format('-u %s', opts && opts.user ? opts.user : 'root'); + var format = "%s -i bash -c '%s'"; + args = util.format(format, user, args); + opts = this._parseOptions(opts); + return this.__exec('sudo', args, opts); + }, - /** - * Copy a list of files to the current destination's remote host(s) using - * `rsync` with the SSH protocol. File transfers are executed in parallel. - * After finishing all transfers, an array containing results from - * `transport.exec()` is returned. This method is only available on local - * flights. - * - * ```javascript - * var files = ['path/to/file1', 'path/to/file2']; - * local.transfer(files, '/tmp/foo'); - * ``` - * - * #### Files argument - * To make things more comfortable, the `files` argument doesn't have to be - * passed as an array. Results from previous commands and zero-terminated - * strings are handled as well: - * - * ```javascript - * // use result from a previous command - * var files = local.git('ls-files', {silent: true}); // get list of files under version control - * local.transfer(files, '/tmp/foo'); - * - * // use zero-terminated result from a previous command - * var files = local.exec('(git ls-files -z;find node_modules -type f -print0)', {silent: true}); - * local.transfer(files, '/tmp/foo'); - * - * // use results from multiple commands - * var result1 = local.git('ls-files', {silent: true}).stdout.split('\n'); - * var result2 = local.find('node_modules -type f', {silent: true}).stdout.split('\n'); - * var files = result1.concat(result2); - * files.push('path/to/another/file'); - * local.transfer(files, '/tmp/foo'); - * ``` - * - * `transfer()` will use the current host's username defined with - * `briefing()` unless `fly` is called with the `-u|--username` option. - * In this case the latter will be used. If debugging is enabled - * (either with `briefing()` or with `fly --debug`), `rsync` is executed - * in verbose mode (`-v`). - * - * @method transfer(files, remoteDir[, options]) - * @return [results] - */ - transfer: function(files, remoteDir, options) { - this.__transfer(files, remoteDir, options); - }, + /** + * Copy a list of files to the current destination's remote host(s) using + * `rsync` with the SSH protocol. File transfers are executed in parallel. + * After finishing all transfers, an array containing results from + * `transport.exec()` is returned. This method is only available on local + * flights. + * + * ```javascript + * var files = ['path/to/file1', 'path/to/file2']; + * local.transfer(files, '/tmp/foo'); + * ``` + * + * #### Files argument + * To make things more comfortable, the `files` argument doesn't have to be + * passed as an array. Results from previous commands and zero-terminated + * strings are handled as well: + * + * ```javascript + * // use result from a previous command + * var files = local.git('ls-files', {silent: true}); // get list of files under version control + * local.transfer(files, '/tmp/foo'); + * + * // use zero-terminated result from a previous command + * var files = local.exec('(git ls-files -z;find node_modules -type f -print0)', {silent: true}); + * local.transfer(files, '/tmp/foo'); + * + * // use results from multiple commands + * var result1 = local.git('ls-files', {silent: true}).stdout.split('\n'); + * var result2 = local.find('node_modules -type f', {silent: true}).stdout.split('\n'); + * var files = result1.concat(result2); + * files.push('path/to/another/file'); + * local.transfer(files, '/tmp/foo'); + * ``` + * + * `transfer()` will use the current host's username defined with + * `briefing()` unless `fly` is called with the `-u|--username` option. + * In this case the latter will be used. If debugging is enabled + * (either with `briefing()` or with `fly --debug`), `rsync` is executed + * in verbose mode (`-v`). + * + * @method transfer(files, remoteDir[, options]) + * @return [results] + */ + transfer: function(files, remoteDir, options) { + this.__transfer(files, remoteDir, options); + }, - /** - * Prompt for user input. - * - * ```javascript - * var input = transport.prompt('Are you sure you want to continue? [yes]'); - * if(input.indexOf('yes') === -1) { - * transport.abort('user canceled flight'); - * } - * - * // prompt for password (with UNIX-style hidden input) - * var password = transport.prompt('Enter your password:', { hidden: true }); - * - * // prompt when deploying to a specific destination - * if(plan.target.destination === 'production') { - * var input = transport.prompt('Ready for deploying to production? [yes]'); - * if(input.indexOf('yes') === -1) { - * transport.abort('user canceled flight'); - * } - * } - * ``` - * - * @method prompt(message[, options]) - * @return input - */ - prompt: function(message, options) { - options = options || {}; - var fiber = Fiber.current; + /** + * Prompt for user input. + * + * ```javascript + * var input = transport.prompt('Are you sure you want to continue? [yes]'); + * if(input.indexOf('yes') === -1) { + * transport.abort('user canceled flight'); + * } + * + * // prompt for password (with UNIX-style hidden input) + * var password = transport.prompt('Enter your password:', { hidden: true }); + * + * // prompt when deploying to a specific destination + * if(plan.target.destination === 'production') { + * var input = transport.prompt('Ready for deploying to production? [yes]'); + * if(input.indexOf('yes') === -1) { + * transport.abort('user canceled flight'); + * } + * } + * ``` + * + * @method prompt(message[, options]) + * @return input + */ + prompt: function(message, options) { + options = options || {}; + var fiber = Fiber.current; - prompt.delimiter = ''; - prompt.message = 'prompt'.grey; - prompt.start(); + prompt.delimiter = ''; + prompt.message = 'prompt'.grey; + prompt.start(); - prompt.get([{ - name: 'input', - description: ' ' + message.white, - hidden: options.hidden || false, - required: options.required || false - }], function(err, result) { - if(err) { - this.logger.space(); - fiber.throwInto(new Error('user canceled prompt')); - } - fiber.run(result ? result.input : null); - }.bind(this)); + prompt.get([{ + name: 'input', + description: ' ' + message.white, + hidden: options.hidden || false, + required: options.required || false + }], function(err, result) { + if(err) { + this.logger.space(); + fiber.throwInto(new Error('user canceled prompt')); + } + fiber.run(result ? result.input : null); + }.bind(this)); - return Fiber.yield(); - }, + return Fiber.yield(); + }, - /** - * Print a message to stdout. Flightplan takes care that the message - * is formatted correctly within the current context. - * - * ```javascript - * transport.log('Copying files to remote hosts'); - * ``` - * - * @method log(message) - */ - log: function() { - this.logger.log.apply(this.logger, arguments); - }, + /** + * Print a message to stdout. Flightplan takes care that the message + * is formatted correctly within the current context. + * + * ```javascript + * transport.log('Copying files to remote hosts'); + * ``` + * + * @method log(message) + */ + log: function() { + this.logger.log.apply(this.logger, arguments); + }, - /** - * Execute a function and return after the callback `done` is called. - * This is used for running asynchronous functions in a synchronous way. - * - * The callback takes an optional argument which is then returned by - * `waitFor()`. - * - * ```javascript - * var result = transport.waitFor(function(done) { - * require('node-notifier').notify({ - * message: 'Hello World' - * }, function(err, response) { - * done(err || 'sent!'); - * }); - * }); - * console.log(result); // 'sent!' - * ``` - * - * @method waitFor(fn(done)) - * @return mixed - */ - waitFor: function(fn) { - var fiber = Fiber.current; - fn(function(ret) { fiber.run(ret); }); - return Fiber.yield(); - }, + /** + * Execute a function and return after the callback `done` is called. + * This is used for running asynchronous functions in a synchronous way. + * + * The callback takes an optional argument which is then returned by + * `waitFor()`. + * + * ```javascript + * var result = transport.waitFor(function(done) { + * require('node-notifier').notify({ + * message: 'Hello World' + * }, function(err, response) { + * done(err || 'sent!'); + * }); + * }); + * console.log(result); // 'sent!' + * ``` + * + * @method waitFor(fn(done)) + * @return mixed + */ + waitFor: function(fn) { + var fiber = Fiber.current; + fn(function(ret) { fiber.run(ret); }); + return Fiber.yield(); + }, - /** - * Execute commands with a certain context. - * - * ```javascript - * transport.with('cd /tmp', function() { - * transport.ls('-al'); // 'cd /tmp && ls -al' - * }); - * - * transport.with({silent: true, failsafe: true}, function() { - * transport.ls('-al'); // output suppressed, fail safely - * }); - * - * transport.with('cd /tmp', {silent: true}, function() { - * transport.ls('-al'); // 'cd /tmp && ls -al', output suppressed - * }); - * ``` - * - * @method with(cmd|options[, options], fn) - */ - with: function() { - var previousOptions = util._extend({}, this.options); // clone - var args = Array.prototype.slice.call(arguments, 0); + /** + * Execute commands with a certain context. + * + * ```javascript + * transport.with('cd /tmp', function() { + * transport.ls('-al'); // 'cd /tmp && ls -al' + * }); + * + * transport.with({silent: true, failsafe: true}, function() { + * transport.ls('-al'); // output suppressed, fail safely + * }); + * + * transport.with('cd /tmp', {silent: true}, function() { + * transport.ls('-al'); // 'cd /tmp && ls -al', output suppressed + * }); + * ``` + * + * @method with(cmd|options[, options], fn) + */ + with: function() { + var previousOptions = util._extend({}, this.options); // clone + var args = Array.prototype.slice.call(arguments, 0); - for(var i in args) { - if(typeof args[i] === 'string') { - this._execWith = args[i] + ' && '; - } else if(typeof args[i] === 'object') { - this.options = util._extend(this.options, args[i]); - } else if(typeof args[i] === 'function') { - args[i](); - } - } - this._execWith = ''; - this.options = previousOptions; - }, + for(var i in args) { + if(typeof args[i] === 'string') { + this._execWith = args[i] + ' && '; + } else if(typeof args[i] === 'object') { + this.options = util._extend(this.options, args[i]); + } else if(typeof args[i] === 'function') { + args[i](); + } + } + this._execWith = ''; + this.options = previousOptions; + }, - /** - * When calling `silent()` all subsequent commands are executed without - * printing their output to stdout until `verbose()` is called. - * - * ```javascript - * transport.ls(); // output will be printed to stdout - * transport.silent(); - * transport.ls(); // output won't be printed to stdout - * ``` - * - * @method silent() - */ - silent: function() { - this.options.silent = true; - }, + /** + * When calling `silent()` all subsequent commands are executed without + * printing their output to stdout until `verbose()` is called. + * + * ```javascript + * transport.ls(); // output will be printed to stdout + * transport.silent(); + * transport.ls(); // output won't be printed to stdout + * ``` + * + * @method silent() + */ + silent: function() { + this.options.silent = true; + }, - /** - * Calling `verbose()` reverts the behavior introduced with `silent()`. - * Output of commands will be printed to stdout. - * - * ```javascript - * transport.silent(); - * transport.ls(); // output won't be printed to stdout - * transport.verbose(); - * transport.ls(); // output will be printed to stdout - * ``` - * - * @method verbose() - */ - verbose: function() { - this.options.silent = false; - }, + /** + * Calling `verbose()` reverts the behavior introduced with `silent()`. + * Output of commands will be printed to stdout. + * + * ```javascript + * transport.silent(); + * transport.ls(); // output won't be printed to stdout + * transport.verbose(); + * transport.ls(); // output will be printed to stdout + * ``` + * + * @method verbose() + */ + verbose: function() { + this.options.silent = false; + }, - /** - * When calling `failsafe()`, all subsequent commands are allowed to fail - * until `unsafe()` is called. In other words, the flight will continue - * even if the return code of the command is not `0`. This is helpful if - * either you expect a command to fail or their nature is to return a - * non-zero exit code. - * - * ```javascript - * transport.failsafe(); - * transport.ls('foo'); // ls: foo: No such file or directory - * transport.log('Previous command failed, but flight was not aborted'); - * ``` - * - * @method failsafe() - */ - failsafe: function() { - this.options.failsafe = true; - }, + /** + * When calling `failsafe()`, all subsequent commands are allowed to fail + * until `unsafe()` is called. In other words, the flight will continue + * even if the return code of the command is not `0`. This is helpful if + * either you expect a command to fail or their nature is to return a + * non-zero exit code. + * + * ```javascript + * transport.failsafe(); + * transport.ls('foo'); // ls: foo: No such file or directory + * transport.log('Previous command failed, but flight was not aborted'); + * ``` + * + * @method failsafe() + */ + failsafe: function() { + this.options.failsafe = true; + }, - /** - * Calling `unsafe()` reverts the behavior introduced with `failsafe()`. - * The flight will be aborted if a subsequent command fails (i.e. returns - * a non-zero exit code). This is the default behavior. - * - * ```javascript - * transport.failsafe(); - * transport.ls('foo'); // ls: foo: No such file or directory - * transport.log('Previous command failed, but flight was not aborted'); - * transport.unsafe(); - * transport.ls('foo'); // ls: foo: No such file or directory - * // flight aborted - * ``` - * - * @method unsafe() - */ - unsafe: function() { - this.options.failsafe = false; - }, + /** + * Calling `unsafe()` reverts the behavior introduced with `failsafe()`. + * The flight will be aborted if a subsequent command fails (i.e. returns + * a non-zero exit code). This is the default behavior. + * + * ```javascript + * transport.failsafe(); + * transport.ls('foo'); // ls: foo: No such file or directory + * transport.log('Previous command failed, but flight was not aborted'); + * transport.unsafe(); + * transport.ls('foo'); // ls: foo: No such file or directory + * // flight aborted + * ``` + * + * @method unsafe() + */ + unsafe: function() { + this.options.failsafe = false; + }, - /** - * Print a debug message to stdout if debug mode is enabled. Flightplan - * takes care that the message is formatted correctly within the current - * context. - * - * ```javascript - * transport.debug('Copying files to remote hosts'); - * ``` - * - * @method debug(message) - */ - debug: function() { - this.logger.debug.apply(this.logger, arguments); - }, + /** + * Print a debug message to stdout if debug mode is enabled. Flightplan + * takes care that the message is formatted correctly within the current + * context. + * + * ```javascript + * transport.debug('Copying files to remote hosts'); + * ``` + * + * @method debug(message) + */ + debug: function() { + this.logger.debug.apply(this.logger, arguments); + }, - /** - * Manually abort the current flight and prevent any further commands and - * flights from being executed. An optional message can be passed which - * is displayed after the flight has been aborted. - * - * ```javascript - * transport.abort('Severe turbulences over the atlantic ocean!'); - * ``` - * - * @method abort([message]) - */ - abort: function() { - this.flight.abort.apply(this.flight, arguments); - }, + /** + * Manually abort the current flight and prevent any further commands and + * flights from being executed. An optional message can be passed which + * is displayed after the flight has been aborted. + * + * ```javascript + * transport.abort('Severe turbulences over the atlantic ocean!'); + * ``` + * + * @method abort([message]) + */ + abort: function() { + this.flight.abort.apply(this.flight, arguments); + }, - close: function() { - this.__close(); - }, + close: function() { + this.__close(); + }, - __close: function() { - }, + __close: function() { + }, - __exec: function() { - }, + __exec: function() { + }, - __transfer: function() { - throw new Error('transfer: transport does not support this method'); - }, + __transfer: function() { + throw new Error('transfer: transport does not support this method'); + }, - _parseOptions: function(opts) { - var options = util._extend({}, this.options); // clone - return util._extend(options, opts); - } + _parseOptions: function(opts) { + var options = util._extend({}, this.options); // clone + return util._extend(options, opts); + } }; diff --git a/lib/transport/shell.js b/lib/transport/shell.js index 1d32cfb..9ca2a76 100644 --- a/lib/transport/shell.js +++ b/lib/transport/shell.js @@ -1,105 +1,105 @@ var util = require('util') - , exec = require("child_process").exec - , Fiber = require('fibers') - , Future = require('fibers/future') - , Transport = require('./index'); + , exec = require("child_process").exec + , Fiber = require('fibers') + , Future = require('fibers/future') + , Transport = require('./index'); function ShellTransport(flight) { - ShellTransport.super_.call(this, flight); - this.target.host = 'localhost'; - this.logger = this.logger.cloneWithPrefix(this.target.host); + ShellTransport.super_.call(this, flight); + this.target.host = 'localhost'; + this.logger = this.logger.cloneWithPrefix(this.target.host); } util.inherits(ShellTransport, Transport); ShellTransport.prototype.__exec = function(cmd, args, options) { - var fiber = Fiber.current; - var proc = null; - var ret = { - code: 0, - stdout: null, - stderr: null - }; - - cmd = this._execWith + cmd + (args ? ' ' + args : ''); - this.logger.command(cmd); - proc = exec(cmd); - - proc.stdout.on('data', function(data) { - ret.stdout = (ret.stdout || '') + data; - if(!options.silent) { - this.logger.stdout(String(data)); - } - }.bind(this)); - - proc.stderr.on('data', function(data) { - ret.stderr += data; - (options.failsafe ? this.logger.stdwarn : this.logger.stderr)(String(data)); - }.bind(this)); - - proc.on('close', function(code) { - ret.code = code; - if(ret.code === 0) { - this.logger.success('ok'.success); - } else if(options.failsafe) { - this.logger.warn(this.logger.format('failed safely').warn, 'with exit code:', ret.code); - } else { - this.logger.error(this.logger.format('failed').error, 'with exit code:', ret.code); - fiber.throwInto(new Error(this.logger.format('`%s` failed on localhost', cmd.white))); - } - fiber.run(ret); - }.bind(this)); - - return Fiber.yield(); + var fiber = Fiber.current; + var proc = null; + var ret = { + code: 0, + stdout: null, + stderr: null + }; + + cmd = this._execWith + cmd + (args ? ' ' + args : ''); + this.logger.command(cmd); + proc = exec(cmd); + + proc.stdout.on('data', function(data) { + ret.stdout = (ret.stdout || '') + data; + if(!options.silent) { + this.logger.stdout(String(data)); + } + }.bind(this)); + + proc.stderr.on('data', function(data) { + ret.stderr += data; + (options.failsafe ? this.logger.stdwarn : this.logger.stderr)(String(data)); + }.bind(this)); + + proc.on('close', function(code) { + ret.code = code; + if(ret.code === 0) { + this.logger.success('ok'.success); + } else if(options.failsafe) { + this.logger.warn(this.logger.format('failed safely').warn, 'with exit code:', ret.code); + } else { + this.logger.error(this.logger.format('failed').error, 'with exit code:', ret.code); + fiber.throwInto(new Error(this.logger.format('`%s` failed on localhost', cmd.white))); + } + fiber.run(ret); + }.bind(this)); + + return Fiber.yield(); }; ShellTransport.prototype.__transfer = function(files, remoteDir, options) { - if(!remoteDir) { - throw new Error('transfer: missing remote path'); - } - - if(files instanceof Array) { - files = files.join('\n'); - } else if(files instanceof Object) { - if(!files.hasOwnProperty('stdout')) { - throw new Error('transfer: invalid object passed'); - } - files = files.stdout; - } - - files = (files || '').trim().replace(/[\r|\n|\0]/mg, '\\n'); - if(!files) { - throw new Error('transfer: empty file list passed'); - } - - var rsyncFlags = '-az' + (this.logger.debugEnabled() ? 'v': ''); - var _results = []; - var task = function(config) { - var future = new Future(); - - Fiber(function() { - var sshFlags = (config.privateKey ? ' -i ' + config.privateKey : ''); - var remoteUrl = util.format('%s%s:%s' - , (config.username ? config.username + '@' : '') - , config.host, remoteDir); - - var cmd = util.format('(echo "%s") | rsync --files-from - %s --rsh="ssh -p%s%s" ./ %s' - , files, rsyncFlags, config.port || 22 - , sshFlags, remoteUrl); - - _results.push(this.exec(cmd, options)); - return future.return(); - }.bind(this)).run(); - - return future; - }.bind(this); - - var tasks = []; - for(var i=0, len=this.flight.hosts.length; i < len; i++) { - tasks.push(task(this.flight.hosts[i])); - } - Future.wait(tasks); - return _results; + if(!remoteDir) { + throw new Error('transfer: missing remote path'); + } + + if(files instanceof Array) { + files = files.join('\n'); + } else if(files instanceof Object) { + if(!files.hasOwnProperty('stdout')) { + throw new Error('transfer: invalid object passed'); + } + files = files.stdout; + } + + files = (files || '').trim().replace(/[\r|\n|\0]/mg, '\\n'); + if(!files) { + throw new Error('transfer: empty file list passed'); + } + + var rsyncFlags = '-az' + (this.logger.debugEnabled() ? 'v': ''); + var _results = []; + var task = function(config) { + var future = new Future(); + + new Fiber(function() { + var sshFlags = (config.privateKey ? ' -i ' + config.privateKey : ''); + var remoteUrl = util.format('%s%s:%s' + , (config.username ? config.username + '@' : '') + , config.host, remoteDir); + + var cmd = util.format('(echo "%s") | rsync --files-from - %s --rsh="ssh -p%s%s" ./ %s' + , files, rsyncFlags, config.port || 22 + , sshFlags, remoteUrl); + + _results.push(this.exec(cmd, options)); + return future.return(); + }.bind(this)).run(); + + return future; + }.bind(this); + + var tasks = []; + for(var i=0, len=this.flight.hosts.length; i < len; i++) { + tasks.push(task(this.flight.hosts[i])); + } + Future.wait(tasks); + return _results; }; module.exports = ShellTransport; \ No newline at end of file diff --git a/lib/transport/ssh.js b/lib/transport/ssh.js index 35e160f..1459a44 100644 --- a/lib/transport/ssh.js +++ b/lib/transport/ssh.js @@ -1,87 +1,84 @@ var util = require('util') - , fs = require('fs') - , Fiber = require('fibers') - , Connection = require('ssh2') - , Transport = require('./index'); + , fs = require('fs') + , Fiber = require('fibers') + , Connection = require('ssh2') + , Transport = require('./index'); function SSHTransport(flight, target) { - SSHTransport.super_.call(this, flight); - this.target = target; - this.logger = this.logger.cloneWithPrefix(this.target.host); + SSHTransport.super_.call(this, flight); + this.target = target; + this.logger = this.logger.cloneWithPrefix(this.target.host); - this.connection = new Connection(); + this.connection = new Connection(); - var _fiber = Fiber.current; + var _fiber = Fiber.current; - this.connection.on('ready', function() { - _fiber.run(); - }); + this.connection.on('ready', function() { + _fiber.run(); + }); - var options = util._extend({}, this.target); // clone - delete options.exec; - if(options.privateKey) { - options.privateKey = fs.readFileSync(options.privateKey, { encoding: 'utf8' }); - } - this.connection.connect(options); + var options = util._extend({}, this.target); // clone + delete options.exec; + if(options.privateKey) { + options.privateKey = fs.readFileSync(options.privateKey, { encoding: 'utf8' }); + } + this.connection.connect(options); - return Fiber.yield(); + return Fiber.yield(); } util.inherits(SSHTransport, Transport); SSHTransport.prototype.__exec = function(cmd, args, options) { - var fiber = Fiber.current; - var ret = { - code: 0, - stdout: null, - stderr: null - }; - cmd = this._execWith + cmd + (args ? ' ' + args : ''); - - this.logger.command(cmd); - var execOpts = options.exec || this.target.exec || {}; - - this.connection.exec(cmd, execOpts, function(err, stream) { - if(err) { - // TODO - } - - stream.on('data', function(data, extended) { - if(extended === 'stderr') { - ret.stderr = (ret.stderr || '') + data; - (options.failsafe ? this.logger.stdwarn : this.logger.stderr)(String(data)); - } else { - ret.stdout = (ret.stdout || '') + data; - if(!options.silent) { - this.logger.stdout(data); - } - } - }.bind(this)); - - stream.on('exit', function(code, signal) { - ret.code = code; - }.bind(this)); - - stream.on('close', function() { - if(ret.code === 0) { - this.logger.success('ok'.success); - } else if(options.failsafe) { - this.logger.warn(this.logger.format('safely failed').warn, 'with exit code:', ret.code); - } else { - this.logger.error(this.logger.format('failed').error, 'with exit code:', ret.code); - fiber.throwInto(new Error(this.logger.format('`%s` failed on %s' - , cmd.white, this.target.host.warn))); - } - fiber.run(ret); - }.bind(this)); - - }.bind(this)); - - return Fiber.yield(); + var fiber = Fiber.current; + var ret = { + code: 0, + stdout: null, + stderr: null + }; + cmd = this._execWith + cmd + (args ? ' ' + args : ''); + + this.logger.command(cmd); + var execOpts = options.exec || this.target.exec || {}; + + this.connection.exec(cmd, execOpts, function(err, stream) { + + stream.on('data', function(data, extended) { + if(extended === 'stderr') { + ret.stderr = (ret.stderr || '') + data; + (options.failsafe ? this.logger.stdwarn : this.logger.stderr)(String(data)); + } else { + ret.stdout = (ret.stdout || '') + data; + if(!options.silent) { + this.logger.stdout(data); + } + } + }.bind(this)); + + stream.on('exit', function(code) { + ret.code = code; + }.bind(this)); + + stream.on('close', function() { + if(ret.code === 0) { + this.logger.success('ok'.success); + } else if(options.failsafe) { + this.logger.warn(this.logger.format('safely failed').warn, 'with exit code:', ret.code); + } else { + this.logger.error(this.logger.format('failed').error, 'with exit code:', ret.code); + fiber.throwInto(new Error(this.logger.format('`%s` failed on %s' + , cmd.white, this.target.host.warn))); + } + fiber.run(ret); + }.bind(this)); + + }.bind(this)); + + return Fiber.yield(); }; SSHTransport.prototype.__close = function() { - this.connection.end(); + this.connection.end(); }; module.exports = SSHTransport; \ No newline at end of file diff --git a/package.json b/package.json index 9a1dec0..a396a9c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "flightplan", "description": "Library for streamlining application deployment or systems administration tasks", - "version": "0.1.10", + "version": "0.2.0", "author": "Patrick Stadler ", "keywords": [ "deploy",