Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: c811f2f9e7
Fetching contributors…

Cannot retrieve contributors at this time

executable file 524 lines (450 sloc) 12.834 kb
#!/usr/bin/env node
/*!
* Sencha Spark
* Copyright(c) 2010 Sencha Inc.
* MIT Licensed
*/
/**
* Module dependencies.
*/
var child_process = require('child_process'),
netBinding = process.binding('net'),
dirname = require('path').dirname,
exists = require('path').existsSync,
http = require('http'),
util = require(process.binding('natives').util ? 'util' : 'sys'),
fs = require('fs'),
cwd = process.cwd(),
net = require('net');
/**
* Framework version.
*/
var version = '0.3.0';
/**
* Process comment.
*/
var comment;
/**
* Use child process workers to utilize all cores
*/
var workers;
/**
* Verbose output.
*/
var verbose;
/**
* Colored terminal output.
*/
var useColors = true;
/**
* Environment defaults.
*/
var env = process.sparkEnv = {
name: process.env.NODE_ENV || 'development',
port: 3000,
host: null
};
/**
* Usage documentation.
*/
var usage = ''
+ '[bold]{Usage}: spark [options]\n'
+ '\n'
+ '[bold]{Options}:\n'
+ ' --comment Ignored, this is to label the process in htop\n'
+ ' -H, --host ADDR Host address, defaults to INADDR_ANY\n'
+ ' -p, --port NUM Port number, defaults to 3000\n'
+ ' --ssl-key PATH SSL key file\n'
+ ' --ssl-crt PATH SSL certificate file\n'
+ ' -n, --workers NUM Number of worker processes to spawn\n'
+ ' -I, --include PATH Unshift the given path to require.paths\n'
+ ' -E, --env NAME Set environment, defaults to "development"\n'
+ ' -M, --mode NAME Alias of -E, --env\n'
+ ' -e, --eval CODE Evaluate the given string\n'
+ ' -C, --chdir PATH Change to the given path\n'
+ ' -c, --config PATH Load configuration module\n'
+ ' -u, --user ID|NAME Change user with setuid()\n'
+ ' -g, --group ID|NAME Change group with setgid()\n'
+ ' -v, --verbose Enable verbose output\n'
+ ' -V, --version Output spark version\n'
+ ' -K, --no-color Suppress colored terminal output\n'
+ ' -h, --help Outputy help information\n'
+ ' --ENV VAL Sets the given spark environment variable\n'
;
/**
* Log the given msg to stderr.
*
* @param {String} msg
*/
function log(msg) {
if (verbose) util.error('... ' + colorize(msg));
}
/**
* Colorize the given string when color is enabled,
* otherwise strip placeholders.
*
* @param {String} str
* @return {String}
*/
function colorize(str) {
var colors = useColors
? { bold: 1 }
: {};
return str.replace(/\[(\w+)\]\{([^}]+)\}/g, function(_, color, str){
return '\x1B[' + colors[color] + 'm' + str +'\x1B[0m';
});
}
/**
* Ad-hoc sync mkdir -p implementation.
*
* @param {String} path
* @param {Number} mode
*/
function mkdirs(path, mode) {
var segs = path.split('/'),
mode = mode || 0755;
if (segs[0]) {
segs.unshift(process.cwd());
} else {
segs[0] = '/';
}
for (var i = 0, len = segs.length; i < len; ++i) {
var dir = segs.slice(0, i + 1).join('/'),
dir = dir[1] === '/' ? dir.slice(1) : dir;
try {
var stat = fs.statSync(dir);
if (!stat.isDirectory()) {
throw new Error('Failed to mkdir "' + dir + '"');
}
} catch (err) {
if (err.errno === process.ENOENT) {
fs.mkdirSync(dir, mode);
} else {
throw err;
}
}
}
}
/**
* Strip ".js" extension from the given path.
*
* @param {String} path
* @return {String}
*/
function modulePath(path){
return path.replace(/\.js$/, '');
}
/**
* Exit with the given message.
*
* @param {String} msg
* @param {Number} code
*/
function abort(msg, code) {
util.error(colorize(msg));
process.exit(code || 1);
}
/**
* Load the given configuration file.
*
* @param {String} file
*/
function loadConfig(file) {
file = process.cwd() + '/' + file;
log('loading config [bold]{`' + file + "'}");
var args = [],
config = require(file);
for (var key in config) {
var val = config[key] instanceof Array
? config[key]
: [config[key]];
// Prefix flag
var key = '--' + key;
// Apply flags
val.forEach(function(val){
log(' ' + key + ' ' + (val === true ? '' : util.inspect(val)));
args.push(key);
if (typeof val !== 'boolean') {
args.push(val);
}
});
}
parseArguments(args);
}
/**
* Require application module at the given path,
* which must be an instanceof net.Server, otherwise
* abort.
*
* @param {String} path
* @return {Server}
*/
function requireApp(path) {
var app = require(path);
try {
if (app instanceof net.Server) {
return app;
} else {
throw new Error('invalid server');
}
} catch (err) {
abort("invalid application.\n"
+ "at: `" + path + "'\n"
+ "must export a , ex: `module.exports = http.createServer(...);'\n");
}
}
/**
* Get path to application.
*
* - supports env.appPath
* - auto-detects {app,server}.js
*
* @return {String}
*/
function getAppPath() {
var path = (env.appPath || '');
if (path[0] !== '/') {
path = process.cwd() + '/' + path;
}
// App path not given, try app.js and server.js
if (!env.appPath) {
if (exists(path + 'app.js')) {
log('detected app.js');
path += 'app';
} else if (exists(path + 'server.js')) {
log('detected server.js');
path += 'server';
} else {
abort('app not found, pass a module path, or create {app,server}.js');
}
}
return path;
}
/**
* Enable SSL for app if desired
*/
function enableSSL(app, env) {
if (env.sslKey) {
var crypto = require('crypto');
app.setSecure(crypto.createCredentials({
key: env.sslKey,
cert: env.sslCrt
}));
}
}
function changeUser() {
// user / group
if (env.gid) {
log('group [bold]{' + env.gid + '}');
process.setgid(env.gid);
}
if (env.uid) {
log('user [bold]{' + env.uid + '}');
process.setuid(env.uid);
}
}
/**
* Start child worker process.
*/
function startWorker() {
var stdin = new net.Stream(0, 'unix');
stdin.addListener('data', function(json){
process.sparkEnv = env = JSON.parse(json.toString());
});
stdin.addListener('fd', function(fd){
var app = requireApp(getAppPath());
util.error('Spark server(' + process.pid + ') listening on '
+ 'http' + (env.sslKey ? 's' : '') + '://'
+ (env.host || '*') + ':' + env.port
+ ' in ' + env.name + ' mode');
enableSSL(app, env);
app.listenFD(fd);
});
stdin.resume();
}
/**
* Start the process.
*/
function start() {
log('starting');
// Detect config.js
if (exists('./config.js')) {
log('detected config.js');
loadConfig('./config.js');
}
// Application path
var path = getAppPath();
// Spawn worker processes
if (workers) {
if (process.version < '0.1.98' && process.version > "0.1.199") {
abort('Cannot use workers with a version older than v0.1.98');
}
var afNum = (netBinding.isIP(env.host) == 6) ? 6 : 4;
var fd = netBinding.socket('tcp' + afNum);
netBinding.bind(fd, env.port, env.host);
netBinding.listen(fd, 128);
changeUser();
var children = [];
for (var i = 0; i < workers; i++) {
// Create an unnamed unix socket to pass the fd to the child.
var fds = netBinding.socketpair();
// Collect the child process arguments
var args = [__filename, '--child'];
if (comment) {
args.push('--comment', comment);
}
// Spawn the child process
var child = children[i] = child_process.spawn(
process.argv[0],
args,
undefined,
[fds[1], 1, 2]
);
log('child spawned [bold]{' + child.pid + '}');
// For some reason stdin isn't getting set, patch it externally
if (!child.stdin) {
child.stdin = new net.Stream(fds[0], 'unix');
}
child.stdin.write(JSON.stringify(env), 'ascii', fd);
}
changeUser();
// Forward signals to child processes, keeps the zombies at bay.
['SIGINT', 'SIGHUP', 'SIGTERM'].forEach(function(signal){
process.addListener(signal, function(){
children.forEach(function(child){
try {
child.kill(signal);
} catch (err) {
// Ignore
}
});
process.exit();
});
});
return;
}
// Load the app module
var app = requireApp(path);
util.error('Spark server(' + process.pid + ') listening on '
+ 'http' + (env.sslKey ? 's' : '') + '://'
+ (env.host || '*') + ':' + env.port
+ ' in ' + env.name + ' mode');
enableSSL(app, env);
app.listen(env.port, env.host);
changeUser();
}
/**
* Parse the arguments.
*/
function parseArguments(args, cmd) {
var arg;
/**
* Return shifted argument, or
* abort with the given prefix.
*
* @param {String} prefix
* @return {String}
*/
function requireArg(prefix) {
if (args.length) {
return args.shift();
} else {
abort(prefix + ' requires an argument.');
}
}
// Iterate
while (args.length) {
switch (arg = args.shift()) {
case '--comment':
comment = requireArg('--comment');
break;
case '--child':
cmd = "worker";
break;
case '-h':
case '--help':
abort(usage);
break;
case '-I':
case '--include':
require.paths.unshift(requireArg('--include'));
break;
case '-e':
case '--eval':
eval(requireArg('--eval'));
break;
case '-p':
case '--port':
env.port = parseInt(requireArg('--port'), 10);
break;
case '-H':
case '--host':
env.host = requireArg('--host');
break;
case '-u':
case '--user':
env.uid = requireArg('--user');
break;
case '-g':
case '--group':
env.gid = requireArg('--group');
break;
case '-C':
case '--chdir':
process.chdir(requireArg('--chdir'));
break;
case '-E':
case '--env':
env.name = requireArg('--env');
break;
case '-M':
case '--mode':
env.name = requireArg('--mode');
break;
case '-c':
case '--config':
loadConfig(modulePath(requireArg('--config')));
break;
case '-v':
case '--verbose':
verbose = true;
break;
case '-V':
case '--version':
abort(version);
break;
case '-K':
case '--no-color':
useColors = false;
break;
case '-n':
case '--workers':
workers = parseInt(requireArg('--workers'), 10);
break;
case '--ssl-key':
env.sslKey = fs.readFileSync(requireArg('--ssl-key'), 'ascii');
break;
case '--ssl-crt':
env.sslCrt = fs.readFileSync(requireArg('--ssl-crt'), 'ascii');
break;
default:
if (arg[0] === '-') {
arg = arg.substr(2);
env[arg] = requireArg('--' + arg);
} else {
env.appPath = modulePath(arg);
}
}
}
// Run the command
switch (cmd) {
case 'worker':
startWorker();
break;
case 'start':
start();
break;
}
}
// Parse cli arguments
parseArguments(process.argv.slice(2), 'start');
Jump to Line
Something went wrong with that request. Please try again.