├── examples ├── ssl │ ├── .gitignore │ ├── start.sh │ ├── app.js │ ├── genkeycert.sh │ └── start.js ├── custom-config │ ├── config │ │ ├── production.js │ │ └── development.js │ └── server.js ├── respawn │ ├── config.js │ └── app.js ├── node │ └── app.js ├── config │ ├── config.js │ └── app.js └── tcp │ └── server.js ├── LICENSE ├── package.json ├── README.markdown └── bin └── spark2 /examples/ssl/.gitignore: -------------------------------------------------------------------------------- 1 | *.pem 2 | *.csr -------------------------------------------------------------------------------- /examples/custom-config/config/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | port: 80 3 | }; -------------------------------------------------------------------------------- /examples/custom-config/config/development.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | host: 'localhost', 3 | port: 3000 4 | }; -------------------------------------------------------------------------------- /examples/respawn/config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | env: 'production', 4 | port: 8181, 5 | host: '127.0.0.1', 6 | workers: 10 7 | } 8 | -------------------------------------------------------------------------------- /examples/ssl/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # 3 | # Use ./genkeycert.sh before running! 4 | # 5 | # Demonstrates usage of SSL support in the executable(1) binary. 6 | # 7 | 8 | ../../bin/spark2 --ssl-key privatekey.pem --ssl-crt certificate.pem 9 | -------------------------------------------------------------------------------- /examples/node/app.js: -------------------------------------------------------------------------------- 1 | // Yes, this is a vanilla node app 2 | var http = require('http'); 3 | 4 | module.exports = http.createServer(function(req, res){ 5 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 6 | res.end('Hello World\n'); 7 | }); 8 | -------------------------------------------------------------------------------- /examples/ssl/app.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | module.exports = http.createServer(function(req, res) { 4 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 5 | var secure = req.socket.secure; 6 | res.end('secure=' + secure + ', that is ' + (secure ? 'good' : 'bad')); 7 | }); 8 | -------------------------------------------------------------------------------- /examples/config/config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | env: 'production', 4 | port: 4321, 5 | host: '127.0.0.1', 6 | verbose: true, 7 | 'no-color': true, 8 | eval: ['require("sys").puts("anything you want!")', 9 | 'require("sys").puts("or use an array for several flags")'] 10 | } -------------------------------------------------------------------------------- /examples/ssl/genkeycert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # http://www.silassewell.com/blog/2010/06/03/node-js-https-ssl-server-example/ 4 | 5 | openssl genrsa -out privatekey.pem 1024 6 | openssl req -new -key privatekey.pem -out certrequest.csr 7 | openssl x509 -req -in certrequest.csr -signkey privatekey.pem -out certificate.pem 8 | -------------------------------------------------------------------------------- /examples/tcp/server.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var net = require('net'), 7 | sys = require('sys'); 8 | 9 | module.exports = net.createServer(function(stream){ 10 | stream.addListener('data', function(chunk){ 11 | stream.write(chunk); 12 | }); 13 | stream.addListener('end', function(){ 14 | stream.end(); 15 | }); 16 | }); -------------------------------------------------------------------------------- /examples/respawn/app.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | 3 | module.exports = http.createServer(function(req, res){ 4 | res.writeHead(200, { 5 | 'Content-Type': 'text/plain' 6 | }); 7 | res.end('Wahoo: ' + new Date()); 8 | }); 9 | setTimeout(function() { 10 | console.log('PID Crash: ' + process.pid); 11 | throw('Killing the PID'); 12 | }, (parseInt(process.pid) / 20)); 13 | -------------------------------------------------------------------------------- /examples/ssl/start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * Demonstrates enabling SSL on the nodejs http.Server when starting 4 | * Connect from JavaScript. 5 | */ 6 | 7 | var crypto = require('crypto'); 8 | var fs = require('fs'); 9 | 10 | var app = require('./app'); 11 | app.env = { host: '*', 12 | port: 3000 13 | }; 14 | 15 | var creds = crypto.createCredentials( 16 | { key: fs.readFileSync('privatekey.pem', 'ascii'), 17 | cert: fs.readFileSync('certificate.pem', 'ascii') 18 | }); 19 | app.setSecure(creds); 20 | 21 | app.listen(3000); 22 | console.log("https://localhost:3000/") 23 | -------------------------------------------------------------------------------- /examples/config/app.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var http = require('http'); 7 | 8 | /** 9 | * `$ spark2 --chdir examples/config` 10 | * will auto-detect ./config.js and apply it. 11 | * 12 | * `$ spark2 --config path/to/config examples/config.app` 13 | * will load the app relative to CWD, and the given config module. 14 | * 15 | * NOTE: you can use --config several times to apply different configurations 16 | * if needed. 17 | */ 18 | 19 | module.exports = http.createServer(function(req, res){ 20 | res.writeHead(200, { 21 | 'Content-Type': 'text/plain' 22 | }); 23 | res.end('Wahoo'); 24 | }); 25 | -------------------------------------------------------------------------------- /examples/custom-config/server.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var sys = require('sys'), 7 | http = require('http'); 8 | 9 | // Based on the current env name require / mixin the 10 | // additional file-based configuration. Try using 11 | // --env production 12 | 13 | var conf = require('./config/' + process.sparkEnv.name); 14 | for (var key in conf) { 15 | process.sparkEnv[key] = conf[key]; 16 | } 17 | 18 | sys.log('loading config file "config/' + process.sparkEnv.name + '.js"'); 19 | 20 | module.exports = http.createServer(function(req, res, next){ 21 | res.writeHead(200, { 'Content-Type': 'text/plain' }); 22 | res.end(sys.inspect(process.sparkEnv)); 23 | }); 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Sencha Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spark2", 3 | "description": "A more powerful node server starter - Fork of the original", 4 | "version": "2.0.12", 5 | "author": "Dav Glass ", 6 | "bugs": { "web" : "http://github.com/davglass/spark2/issues" }, 7 | "contributors": [ 8 | { "name": "Dav Glass", "email": "davglass@gmail.com" }, 9 | { "name": "Łukasz Piestrzeniewicz", "email": "bragi@ragnarson.com" }, 10 | { "name": "Tim Caswell", "email": "tim@sencha.com" }, 11 | { "name": "TJ Holowaychuk", "email": "tj@sencha.com" }, 12 | { "name": "Naitik Shah", "email": "n@daaku.org" }, 13 | { "name": "Francisco Treacy", "email": "francisco.treacy@gmail.com" }, 14 | { "name": "Sergey Belov", "email": "arikon@yandex-team.ru"} 15 | ], 16 | "bin": { "spark2": "./bin/spark2" }, 17 | "engines": { "node": ">= 0.1.199" }, 18 | "preferGlobal" : "true", 19 | "dependencies": { 20 | "lsof": ">=0.0.1" 21 | }, 22 | "licenses":[ 23 | { 24 | "type" : "MIT", 25 | "url" : "https://github.com/davglass/spark2/blob/master/LICENSE" 26 | } 27 | ], 28 | "repository": { 29 | "type":"git", 30 | "url":"http://github.com/davglass/spark2.git" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Spark2 2 | 3 | Spark2 is a fork of the Spark command-line tool used to start up node server instances written by [Tj Holowaychuk](http://github.com/visionmedia) and [Tim Caswell](http://github.com/creationix). It's part of the [Connect](http://github.com/senchalabs/connect) framework, however can be used standalone with _any_ node server. 4 | 5 | ## Why the fork? 6 | 7 | Due to the nature of the CLA required to submit my changes back to Sencha, I have chosen to fork Spark myself and add the features I needed. 8 | 9 | ## What is different? 10 | 11 | For the most part, they are identical. Spark2 contains some helpful management tools missing from the original. 12 | 13 | The most useful is the "auto re-spawn children" feature. If a worker dies, spark2 will respawn a replacement child. 14 | 15 | You can also manage the server for hot code deployment. The original Spark had the main process listen on the app as well as the workers. Spark2's main process doesn't listen on the app, only the workers do. 16 | That allows you to do things like this: 17 | 18 | spark2 -n 10 & 19 | --exit the terminal and log back in.. 20 | --Now update your source, then 21 | spark2 --respawn (This will respawn all the worker processes to reload the code. 22 | 23 | You can also call: 24 | spark2 --kill to kill the main process and it's workers. 25 | 26 | This can also be done from the shell by sending the right signals: 27 | 28 | kill -s SIGCONT PID (respawn all children) 29 | kill PID (kill) 30 | 31 | I also fixed an issue where command line arguments are overwritten if there is a config. Now commandline arguments override the config so you can test locally with a production config. 32 | 33 | stdout/stderror of the worker is forwarded to the master pid. 34 | 35 | Simple support for access.log, just add an accesslog file pointer to the config, then apply 36 | 37 | app.use(express.logger()); 38 | 39 | stdout will autoforward from the worker to the master process and write the result to the provided file. 40 | 41 | Watch file (for development) 42 | 43 | spark2 -v -E development -n 1 --watch 44 | 45 | This will listen with `fs.watchFile` on the {app,server}.{js,coffee} file that started the process. If the mtime or the file size changes, it will respawn all the workers. Basically means that you save the app js file and the children will respawn so you can just refresh the page. 46 | This works pretty well since the main process file doesn't start the "app", it just starts the children and they control the app. 47 | 48 | ## Process Status Checking 49 | 50 | As of 2.0.10, spark2 has some status checking built in. Once you start a `spark2` process, you can 51 | reenter that directory and execute `spark2 --status`. This will show sometime like this: 52 | 53 | ------------------------------------------------------------------ 54 | Showing status for spark2 process at: 21773 55 | ------------------------------------------------------------------ 56 | Current children: 10 57 | Total # of spawned children: 10 58 | Total # of open FDs: 67 59 | Total # of open pipes(FIFO): 26 60 | Started: Thu Dec 23 2010 21:38:35 GMT+0000 (UTC) 61 | Now : Thu Dec 23 2010 21:46:45 GMT+0000 (UTC) 62 | ------------------------------------------------------------------ 63 | Memory Usage 64 | ------------------------------------------------------------------ 65 | rss: 9568256 66 | vsize: 639512576 67 | heapTotal: 4704448 68 | heapUsed: 2594328 69 | ------------------------------------------------------------------ 70 | 71 | I fixed a major bug in this release that was not freeing up file descriptors when a child died. I added 72 | this status message so you can track not only the memory of the main process (doesn't include children yet) 73 | and you can see the "open file descriptors" as well as the open pipes. This # will normally be around 3 times 74 | the number of workers you have configured. (stdin, stdout, stderr of all workers) 75 | 76 | ## Features 77 | 78 | Spark2 provides the following options when starting a server. 79 | 80 | - Port/Host to listen on 81 | - SSL hosting with specified key/certificate 82 | - Automatic child process spawning for multi-process node servers that share a common port. 83 | - User/Group to drop to after binding (if run as root) 84 | - Environment modes (development/testing/production) 85 | - Modify the node require paths 86 | - Can load any of these configs from from a file (optionally grouped by env) 87 | - Can change the working directory before running the app 88 | - `--comment` option to label processes in system tools like `ps` or `htop` 89 | - Pass arbitrary code or environment variables to the app 90 | 91 | ## Making an app spark compatible 92 | 93 | Any node server can be used with spark2. All you need to do it create a file called `app.js` or `server.js` that exports the instance of `http.Server` or `net.Server`. 94 | 95 | A hello-world example would look like this: 96 | 97 | module.exports = require('http').createServer(function (req, res) { 98 | res.writeHead(200, {"Content-Type":"text-plain"}); 99 | res.end("Hello World"); 100 | }); 101 | 102 | And then to run it you simply go to the folder containing the `app.js` or `server.js` and type: 103 | 104 | $ spark2 105 | 106 | The output you'll see will be: 107 | 108 | Spark2 server(34037) listening on http://*:3000 in development mode 109 | 110 | Where `34037` is the process id. If you want 4 processes to balance the requests across, no problem. 111 | 112 | $ spark2 -n 4 113 | 114 | And you'll see: 115 | 116 | Spark2 server(34049) listening on http://*:3000 in development mode 117 | Spark2 server(34050) listening on http://*:3000 in development mode 118 | Spark2 server(34052) listening on http://*:3000 in development mode 119 | Spark2 server(34051) listening on http://*:3000 in development mode 120 | 121 | -------------------------------------------------------------------------------- /bin/spark2: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /*! 4 | * Sencha Spark 5 | * Copyright(c) 2010 Sencha Inc. 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | 13 | var child_process = require('child_process'), 14 | netBinding = process.binding('net'), 15 | dirname = require('path').dirname, 16 | exists = require('path').existsSync, 17 | http = require('http'), 18 | util = require(process.binding('natives').util ? 'util' : 'sys'), 19 | fs = require('fs'), 20 | cwd = process.cwd(), 21 | net = require('net'), 22 | data = { 23 | start: null, 24 | children: 0, 25 | errors: [] 26 | }; 27 | 28 | 29 | /** 30 | * Coffee script support 31 | * Try to require coffee script, ignore if it fails. 32 | */ 33 | var coffee_script; 34 | try { coffee_script = require('coffee-script') } catch (e) {} 35 | 36 | 37 | /** 38 | * Framework version. 39 | */ 40 | 41 | var showVersion = function() { 42 | var path = require('path'); 43 | var json = path.join(__dirname, '../', 'package.json'); 44 | json = fs.readFileSync(json, encoding='utf8'); 45 | json = JSON.parse(json); 46 | abort(json.version); 47 | } 48 | 49 | /** 50 | * Process comment. 51 | */ 52 | 53 | var comment; 54 | 55 | /** 56 | * Use child process workers to utilize all cores 57 | */ 58 | 59 | var workers = 1; 60 | 61 | /** 62 | * Verbose output. 63 | */ 64 | 65 | var verbose; 66 | 67 | var respawn; 68 | var kill; 69 | var force; 70 | var status; 71 | var spawn; 72 | var pidfile; 73 | var accesslog; 74 | var watch; 75 | var debugApp; 76 | 77 | /** 78 | * Colored terminal output. 79 | */ 80 | 81 | var useColors = true; 82 | 83 | /** 84 | * Environment defaults. 85 | */ 86 | 87 | var env = process.sparkEnv = { 88 | name: process.env.NODE_ENV || 'development', 89 | port: 3000, 90 | host: null 91 | }; 92 | 93 | /** 94 | * Usage documentation. 95 | */ 96 | 97 | var usage = '' 98 | + '[bold]{Usage}: spark2 [options]\n' 99 | + '\n' 100 | + '[bold]{Options}:\n' 101 | + ' --comment Ignored, this is to label the process in htop\n' 102 | + ' -H, --host ADDR Host IP address, defaults to INADDR_ANY\n' 103 | + ' -p, --port NUM Port number, defaults to 3000\n' 104 | + ' --ssl-key PATH SSL key file\n' 105 | + ' --ssl-crt PATH SSL certificate file\n' 106 | + ' -n, --workers NUM Number of worker processes to spawn\n' 107 | + ' -I, --include PATH Unshift the given path to require.paths\n' 108 | + ' --pidfile PATH Use this file as the tmp pidfile\n' 109 | + ' --accesslog PATH Use this file to write stdout (express.logger()) data\n' 110 | + ' -E, --env NAME Set environment, defaults to "development"\n' 111 | + ' -M, --mode NAME Alias of -E, --env\n' 112 | + ' -e, --eval CODE Evaluate the given string\n' 113 | + ' -C, --chdir PATH Change to the given path\n' 114 | + ' -c, --config PATH Load configuration module\n' 115 | + ' -u, --user ID|NAME Change user with setuid()\n' 116 | + ' -g, --group ID|NAME Change group with setgid()\n' 117 | + ' -v, --verbose Enable verbose output\n' 118 | + ' -V, --version Output spark2 version\n' 119 | + ' -D, --debug Enable debug mode on app child process\n' 120 | + ' -K, --no-color Suppress colored terminal output\n' 121 | + ' -h, --help Outputy help information\n' 122 | + ' --ENV VAL Sets the given spark2 environment variable\n' 123 | + ' --kill Kill the currently running server\n' 124 | + ' --respawn Restart all workers on the currently running server\n' 125 | + ' --force Kill the lock file on the current server and start\n' 126 | + ' --watch Watch the app file and restart workers if the file size or mtime changes\n' 127 | + ' --status Report status information about this spark2 process\n' 128 | ; 129 | 130 | /** 131 | * Log the given msg to stderr. 132 | * 133 | * @param {String} msg 134 | */ 135 | 136 | function log(msg) { 137 | if (verbose) util.error('... ' + colorize(msg)); 138 | } 139 | 140 | /** 141 | * Colorize the given string when color is enabled, 142 | * otherwise strip placeholders. 143 | * 144 | * @param {String} str 145 | * @return {String} 146 | */ 147 | 148 | function colorize(str) { 149 | var colors = useColors 150 | ? { bold: 1 } 151 | : {}; 152 | str = str+''; 153 | return str.replace(/\[(\w+)\]\{([^}]+)\}/g, function(_, color, str){ 154 | return '\x1B[' + colors[color] + 'm' + str +'\x1B[0m'; 155 | }); 156 | } 157 | 158 | /** 159 | * Strip ".js" extension from the given path. 160 | * 161 | * @param {String} path 162 | * @return {String} 163 | */ 164 | 165 | function modulePath(path){ 166 | return path.replace(/\.js$/, ''); 167 | } 168 | 169 | /** 170 | * Exit with the given message. 171 | * 172 | * @param {String} msg 173 | * @param {Number} code 174 | */ 175 | 176 | function abort(msg, code) { 177 | util.error(colorize(msg)); 178 | process.exit(code || 1); 179 | } 180 | 181 | /** 182 | * Load the given configuration file. 183 | * 184 | * @param {String} file 185 | */ 186 | 187 | function loadConfig(file) { 188 | if (file[0] !== '/') { 189 | file = process.cwd() + '/' + file; 190 | } 191 | log('loading config [bold]{`' + file + "'}"); 192 | var args = [], 193 | config = require(file); 194 | for (var key in config) { 195 | var val = config[key] instanceof Array 196 | ? config[key] 197 | : [config[key]]; 198 | 199 | // Prefix flag 200 | var key = '--' + key; 201 | 202 | // Apply flags 203 | val.forEach(function(val){ 204 | log(' ' + key + ' ' + (val === true ? '' : util.inspect(val))); 205 | args.push(key); 206 | if (typeof val !== 'boolean') { 207 | args.push(val); 208 | } 209 | if (key === '--watch') { 210 | if (val && typeof val === 'boolean') { 211 | args.push(cwd); 212 | } 213 | } 214 | }); 215 | } 216 | //Remove the config key from the arguments.. 217 | var args2 = process.argv.slice(2); 218 | args.forEach(function(v, k) { 219 | if (v === '--config') { 220 | args.splice(k, 2); 221 | } 222 | }); 223 | args2.forEach(function(v, k) { 224 | if (v === '--config') { 225 | args2.splice(k, 2); 226 | } 227 | }); 228 | 229 | if (args.length) { parseArguments(args); }; 230 | if (args2.length) { parseArguments(args2); }; 231 | } 232 | 233 | 234 | 235 | 236 | /** 237 | * Require application module at the given path, 238 | * which must be an instanceof net.Server, otherwise 239 | * abort. 240 | * 241 | * @param {String} path 242 | * @return {Server} 243 | */ 244 | 245 | function requireApp(path) { 246 | var app = require(path); 247 | try { 248 | if (app instanceof net.Server) { 249 | return app; 250 | } else { 251 | throw new Error('invalid server'); 252 | } 253 | } catch (err) { 254 | abort("invalid application.\n" 255 | + "at: `" + path + "'\n" 256 | + "must export a , ex: `module.exports = http.createServer(...);'\n"); 257 | } 258 | } 259 | 260 | /** 261 | * Get path to application. 262 | * 263 | * - supports env.appPath 264 | * - auto-detects {app,server}.js 265 | * 266 | * @return {String} 267 | */ 268 | 269 | function getAppPath(inc) { 270 | var path = (env.appPath || ''); 271 | if (path[0] !== '/') { 272 | path = process.cwd() + '/' + path; 273 | } 274 | 275 | // App path not given, try app.js and server.js 276 | if (!env.appPath) { 277 | if (exists(path + 'app.js')) { 278 | log('detected app.js'); 279 | path += 'app'; 280 | if (inc) { path += '.js'; } 281 | } else if (exists(path + 'server.js')) { 282 | log('detected server.js'); 283 | path += 'server'; 284 | if (inc) { path += '.js'; } 285 | } else if (coffee_script && exists(path + 'app.coffee')) { 286 | log('detected app.coffee'); 287 | path += 'app'; 288 | if (inc) { path += '.coffee'; } 289 | } else if (coffee_script && exists(path + 'server.coffee')) { 290 | log('detected server.coffee'); 291 | path += 'server'; 292 | if (inc) { path += '.coffee'; } 293 | } else { 294 | abort('app not found, pass a module path, or create {app,server}.{js,coffee}'); 295 | } 296 | } 297 | 298 | return path; 299 | } 300 | 301 | /** 302 | * Enable SSL for app if desired 303 | */ 304 | 305 | function enableSSL(app, env) { 306 | if (env.sslKey) { 307 | var crypto = require('crypto'); 308 | app.setSecure(crypto.createCredentials({ 309 | key: env.sslKey, 310 | cert: env.sslCrt 311 | })); 312 | } 313 | } 314 | 315 | function changeUser() { 316 | // user / group 317 | if (env.gid) { 318 | log('group [bold]{' + env.gid + '}'); 319 | process.setgid(env.gid); 320 | } 321 | if (env.uid) { 322 | log('user [bold]{' + env.uid + '}'); 323 | process.setuid(env.uid); 324 | } 325 | } 326 | 327 | 328 | /** 329 | * Start child worker process. 330 | */ 331 | 332 | function startWorker() { 333 | var stdin = new net.Stream(0, 'unix'); 334 | stdin.addListener('data', function(json) { 335 | process.sparkEnv = env = JSON.parse(json.toString()); 336 | }); 337 | stdin.addListener('fd', function(fd){ 338 | var app = requireApp(getAppPath()); 339 | app.isChild = true; 340 | util.error('Spark2 server(' + process.pid + ') listening on ' 341 | + 'http' + (env.sslKey ? 's' : '') + '://' 342 | + (env.host || '*') + ':' + env.port 343 | + ' in ' + env.name + ' mode'); 344 | 345 | enableSSL(app, env); 346 | app.listenFD(fd); 347 | }); 348 | stdin.resume(); 349 | 350 | } 351 | 352 | 353 | function spawnChild() { 354 | data.children++; 355 | // Create an unnamed unix socket to pass the fd to the child. 356 | var fds = netBinding.socketpair(); 357 | var sFds = [fds[1], fds[2], fds[3]]; 358 | //var sFds = [-1, -1, -1]; 359 | 360 | // Collect the child process arguments 361 | var args = [process.argv[1], '--child']; 362 | if (comment) { 363 | args.push('--comment', comment); 364 | } 365 | if (debugApp) { 366 | args.unshift('--debug'); 367 | } 368 | 369 | // Spawn the child process 370 | var child = child_process.spawn( 371 | process.argv[0], 372 | args, 373 | undefined, 374 | sFds 375 | ); 376 | 377 | log('child spawned [bold]{' + child.pid + '}'); 378 | 379 | // For some reason stdin isn't getting set, patch it externally 380 | if (!child.stdin) { 381 | child.stdin = new net.Stream(fds[0], 'unix'); 382 | } 383 | if (!child.stdout) { 384 | child.stdout = new net.Stream(fds[1], 'unix'); 385 | } 386 | if (!child.stderr) { 387 | child.stderr = new net.Stream(fds[2], 'unix'); 388 | } 389 | 390 | child.stdspark = new net.Stream(fds[3], 'unix'); 391 | 392 | child.stdin.write(JSON.stringify(env), 'ascii', mainFD); 393 | child.stdout.addListener('data', function(d) { 394 | if (verbose) { 395 | util.print(d); 396 | } 397 | if (accesslog) { 398 | writeAccess(d); 399 | } 400 | }); 401 | child.stderr.addListener('data', function(d) { 402 | if (verbose) { 403 | util.print(d); 404 | } 405 | }); 406 | 407 | 408 | return child; 409 | } 410 | 411 | var aLog; 412 | 413 | var writeAccess = function(data) { 414 | if (data) { 415 | if (!aLog) { 416 | aLog = fs.createWriteStream(accesslog, { flags: 'w+', encoding: 'ascii', mode: 0777 }); 417 | } 418 | if (aLog) { 419 | aLog.write(data); 420 | } 421 | } 422 | } 423 | 424 | var childSig = function() { 425 | for (var i in children) { 426 | if (!children[i].pid) { 427 | log('Child Died (' + i + '), respawning...'); 428 | children[i].stderr.destroy(); 429 | children[i].stdout.destroy(); 430 | children[i].stdin.destroy(); 431 | children[i].stdspark.destroy(); 432 | children[i].fds.forEach(function(i) { 433 | /** 434 | * This is a dirty, dirty hack. 435 | * It appears that if you create a child process with customFD's 436 | * The underlying code gives new file descriptors, but doesn't 437 | * give us access to them. It only gives us the # back. 438 | * So we have to walk through the file descriptors and create a new stream 439 | * then destroy that stream to get rid of the hanging descriptors 440 | * Or will will get this error 441 | * readpipe(): Too many open files 442 | */ 443 | try { 444 | var s = new net.Stream(i, 'unix'); 445 | s.destroy(); 446 | } catch (e) {} 447 | }); 448 | var c = spawnChild(); 449 | delete children[i]; 450 | children[c.pid] = c; 451 | } 452 | } 453 | }; 454 | 455 | var respawnAll = function() { 456 | log('Respawning all children'); 457 | for (var i in children) { 458 | children[i].kill('SIGTERM'); 459 | } 460 | } 461 | 462 | var getTmpFile = function(path) { 463 | if (pidfile) { 464 | return pidfile; 465 | } 466 | path = path.replace(/\//g, '-').replace(/ /g, ''); 467 | //path = process.pid +''; 468 | var tmpDir = process.env.TMPDIR; 469 | if (!tmpDir) { 470 | tmpDir = '/tmp/'; 471 | } 472 | var file = tmpDir + 'spark2-' + path; 473 | return file; 474 | } 475 | 476 | var writePid = function(path) { 477 | var file = getTmpFile(path); 478 | fs.writeFile(file, process.pid+'', encoding='ascii'); 479 | } 480 | 481 | var getMainPid = function(path) { 482 | return fs.readFileSync(getTmpFile(path)); 483 | } 484 | 485 | var watchFile = function(cur, prev) { 486 | var reason; 487 | if (cur.size !== prev.size) { 488 | reason = 'Filesize is different'; 489 | } 490 | if (cur.mtime.getTime() !== prev.mtime.getTime()) { 491 | reason = 'mtime is different'; 492 | } 493 | if (reason) { 494 | log('File has changed (' + reason + '), respawning... '); 495 | respawnAll(); 496 | } 497 | }; 498 | 499 | var children = {}, 500 | mainFD; 501 | 502 | var showStatus = function(pid) { 503 | process.kill(pid, 'SIGUSR1'); 504 | setTimeout(function() { 505 | var data = fs.readFileSync('./spark2-status.log', 'utf8'); 506 | fs.unlinkSync('./spark2-status.log'); 507 | console.log(data); 508 | }, 2000); 509 | }; 510 | 511 | var showInternalStatus = function() { 512 | var lsof = require('lsof'); 513 | var mem = process.memoryUsage(); 514 | var str = []; 515 | lsof.counters(function(d) { 516 | str.push('------------------------------------------------------------------'); 517 | str.push(' Showing status for spark2 process at: ' + process.pid); 518 | str.push('------------------------------------------------------------------'); 519 | str.push(' Current children: ' + Object.keys(children).length); 520 | str.push(' Total # of spawned children: ' + data.children); 521 | str.push(' Total # of open FDs: ' + d.open); 522 | if (d.types.pipe) { 523 | str.push(' Total # of open pipes: ' + d.types.pipe); 524 | } 525 | if (d.types.fifo) { 526 | str.push(' Total # of open pipes(FIFO): ' + d.types.fifo); 527 | } 528 | str.push(' Started: ' + data.start); 529 | str.push(' Now : ' + new Date()); 530 | str.push('------------------------------------------------------------------'); 531 | str.push(' Memory Usage'); 532 | str.push('------------------------------------------------------------------'); 533 | for (var i in mem) { 534 | str.push(' ' + i + ': ' + mem[i]); 535 | } 536 | str.push('------------------------------------------------------------------'); 537 | str.push(''); 538 | 539 | fs.writeFileSync('spark2-status.log', str.join('\n'), 'utf8'); 540 | }); 541 | } 542 | 543 | /** 544 | * Start the process. 545 | */ 546 | 547 | function start() { 548 | data.start = new Date(); 549 | process.on('uncaughtException', function(e) { 550 | data.errors.push(e.message) 551 | }); 552 | 553 | log('starting'); 554 | 555 | // Detect config.js 556 | if (exists('./config.js')) { 557 | log('detected config.js'); 558 | loadConfig('./config.js'); 559 | } 560 | 561 | if (!workers) { 562 | abort('Must have at least one worker..'); 563 | } 564 | 565 | // Application path 566 | var path = getAppPath(); 567 | 568 | if (exists(getTmpFile(path))) { 569 | log(getTmpFile(path)); 570 | if (status) { 571 | showStatus(parseInt(getMainPid(path))); 572 | return; 573 | } 574 | if (respawn || kill || force) { 575 | var pid = parseInt(getMainPid(path)), 576 | sig = 'SIGTERM'; 577 | 578 | if (isNaN(pid)) { 579 | force = true; 580 | } 581 | 582 | if (respawn) { 583 | sig = 'SIGCONT'; 584 | } 585 | if (force || kill) { 586 | fs.unlinkSync(getTmpFile(path)); 587 | kill = true; 588 | } 589 | if (kill || respawn) { 590 | try { 591 | process.kill(pid, sig); 592 | } catch (e) {} 593 | } 594 | } else { 595 | abort('PID already exists for this process.. Use spark2 {--kill , --respawn, --force, --status}'); 596 | } 597 | if (!force) { 598 | return; 599 | } 600 | } 601 | 602 | // Spawn worker processes 603 | if (workers) { 604 | log('starting with (' + workers + ') workers'); 605 | if (process.version < '0.1.98' && process.version > "0.1.199") { 606 | abort('Cannot use workers with a version older than v0.1.98'); 607 | } 608 | 609 | var afNum = (netBinding.isIP(env.host) == 6) ? 6 : 4; 610 | if (env.host && (netBinding.isIP(env.host) === 0)) { 611 | throw new Error('--host must be an IP Address'); 612 | } 613 | var fd = netBinding.socket('tcp' + afNum); 614 | netBinding.bind(fd, env.port, env.host); 615 | netBinding.listen(fd, 128); 616 | mainFD = fd; 617 | 618 | changeUser(); 619 | 620 | for (var i = 0; i < workers; i++) { 621 | var c = spawnChild(); 622 | children[c.pid] = c; 623 | } 624 | 625 | changeUser(); 626 | 627 | // Forward signals to child processes, keeps the zombies at bay. 628 | ['SIGINT', 'SIGHUP', 'SIGTERM'].forEach(function(signal){ 629 | process.addListener(signal, function() { 630 | fs.unlinkSync(getTmpFile(path)); 631 | for (var c in children) { 632 | try { 633 | children[c].kill(signal); 634 | } catch (err) { 635 | // Ignore 636 | } 637 | } 638 | process.exit(); 639 | }); 640 | }); 641 | 642 | process.addListener('SIGCHLD', childSig); 643 | process.addListener('SIGCONT', respawnAll); 644 | process.addListener('SIGUSR1', showInternalStatus); 645 | 646 | } 647 | 648 | changeUser(); 649 | 650 | if (watch) { 651 | var cmd = 'find ' + watch + ' -name "*.js" -o -name "*.coffee"'; 652 | log('find: ' + cmd); 653 | child_process.exec(cmd, function (error, out) { 654 | var files = out.trim().split("\n"); 655 | files.forEach(function(file) { 656 | log('Watching file: ' + file); 657 | fs.watchFile(file, { interval: 100 }, watchFile); 658 | }); 659 | }); 660 | } 661 | writePid(path); 662 | } 663 | 664 | /** 665 | * Parse the arguments. 666 | */ 667 | 668 | function parseArguments(args, cmd) { 669 | var arg; 670 | 671 | /** 672 | * Return shifted argument, or 673 | * abort with the given prefix. 674 | * 675 | * @param {String} prefix 676 | * @return {String} 677 | */ 678 | 679 | function requireArg(prefix, def) { 680 | if (!args.length && def) { 681 | args.push(def); 682 | } 683 | if (args.length) { 684 | return args.shift(); 685 | } else { 686 | abort(prefix + ' requires an argument.'); 687 | } 688 | } 689 | 690 | // Iterate 691 | while (args.length) { 692 | switch (arg = args.shift()) { 693 | case '--watch': 694 | watch = requireArg('--watch', cwd); 695 | break; 696 | case '--status': 697 | status = true; 698 | break; 699 | case '--force': 700 | force = true; 701 | break; 702 | case '--kill': 703 | kill = true; 704 | break; 705 | case '-R': 706 | case '--respawn': 707 | respawn = true; 708 | break; 709 | case '--comment': 710 | comment = requireArg('--comment'); 711 | break; 712 | case '-D': 713 | case '--debug': 714 | debugApp = true; 715 | break; 716 | case '--child': 717 | cmd = "worker"; 718 | break; 719 | case '-h': 720 | case '--help': 721 | abort(usage); 722 | break; 723 | case '-I': 724 | case '--include': 725 | require.paths.unshift(requireArg('--include')); 726 | break; 727 | case '-e': 728 | case '--eval': 729 | eval(requireArg('--eval')); 730 | break; 731 | case '-p': 732 | case '--port': 733 | env.port = parseInt(requireArg('--port'), 10); 734 | break; 735 | case '-H': 736 | case '--host': 737 | env.host = requireArg('--host'); 738 | break; 739 | case '-u': 740 | case '--user': 741 | env.uid = requireArg('--user'); 742 | break; 743 | case '-g': 744 | case '--group': 745 | env.gid = requireArg('--group'); 746 | break; 747 | case '-C': 748 | case '--chdir': 749 | process.chdir(requireArg('--chdir')); 750 | break; 751 | case '-E': 752 | case '--env': 753 | env.name = requireArg('--env'); 754 | break; 755 | case '-M': 756 | case '--mode': 757 | env.name = requireArg('--mode'); 758 | break; 759 | case '--pidfile': 760 | pidfile = requireArg('--pidfile'); 761 | break; 762 | case '--accesslog': 763 | accesslog = requireArg('--accesslog'); 764 | break; 765 | case '-c': 766 | case '--config': 767 | loadConfig(modulePath(requireArg('--config'))); 768 | break; 769 | case '-v': 770 | case '--verbose': 771 | verbose = true; 772 | break; 773 | case '-V': 774 | case '--version': 775 | showVersion(); 776 | break; 777 | case '-K': 778 | case '--no-color': 779 | useColors = false; 780 | break; 781 | case '-n': 782 | case '--workers': 783 | workers = parseInt(requireArg('--workers'), 10); 784 | break; 785 | case '--ssl-key': 786 | env.sslKey = fs.readFileSync(requireArg('--ssl-key'), 'ascii'); 787 | break; 788 | case '--ssl-crt': 789 | env.sslCrt = fs.readFileSync(requireArg('--ssl-crt'), 'ascii'); 790 | break; 791 | default: 792 | if (arg[0] === '-') { 793 | arg = arg.substr(2); 794 | env[arg] = requireArg('--' + arg); 795 | } else { 796 | env.appPath = modulePath(arg); 797 | } 798 | } 799 | } 800 | 801 | // Run the command 802 | 803 | switch (cmd) { 804 | case 'worker': 805 | startWorker(); 806 | break; 807 | case 'start': 808 | start(); 809 | break; 810 | } 811 | } 812 | 813 | // Parse cli arguments 814 | parseArguments(process.argv.slice(2), 'start'); 815 | 816 | --------------------------------------------------------------------------------