diff --git a/README.md b/README.md index 95f9df2..739e55a 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Features: * Clustering - take advantage of multiple CPU cores * Properly handles SIGTERM and SIGHUP for integration with service wrappers * Supports POSIX operating systems (does not support Windows) + * Supports using sockets opened by systemd or launchd Usage: ------ @@ -131,7 +132,6 @@ If you want to deploy on a restricted port such as 80 or 443 without sudo, try Note that there are 3 layers of process spawning between the naught CLI and your server. So you'll want to use the `--deep` option with authbind. - Using a service wrapper: ------------------------ @@ -160,6 +160,73 @@ When you run with `--daemon-mode false`, the process tree looks like this: * worker 2 * etc +Using a socket from systemd or launchd +-------------------------------------- + +When using naught from systemd or launchd, you must use `--daemon-mode false`. + +systemd and launchd can be configured to listen on a port and launch naught +when a connection is detected on that port. The intention is that your server +will only run when it is actually needed. systemd or launchd will provide the +open socket to naught on a file descriptor, and naught will pass that file +descriptor on to your server as it launches it. naught will set the +`LISTEN_FD` environment variable to the number of the file descriptor on +which your server should listen, which it could do like this: + + ```js + server.listen(process.env.LISTEN_FD ? {fd: parseInt(process.env.LISTEN_FD, 10)} : (process.env.PORT || 8000)); + ``` + +naught automatically detects if it was launched by systemd and passes the +socket along. For use from launchd, pass the `--launchd-socket` flag when +starting naught, and provide the name of the key you defined in the Sockets +dictionary in your server's launchd plist. Here is a sample plist: + + ```xml + + + + + EnvironmentVariables + + NODE_ENV + production + PATH + /usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin + + Label + com.example.myserver + ProgramArguments + + /usr/local/bin/node + node_modules/.bin/naught + start + --daemon-mode + false + --launchd-socket + Listeners + server.js + + Sockets + + Listeners + + SockFamily + IPv4v6 + SockServiceName + 8000 + + + WorkingDirectory + /opt/myserver + + + ``` + +If you want to pass a socket in a file descriptor to naught started from some +process other than systemd or launchd, you can set the LISTEN_FD environment +variable to the file descriptor number when you launch naught. + CLI: ---- @@ -202,6 +269,7 @@ CLI: --daemon-mode true --remove-old-ipc false --node-args '' + --launchd-socket '' naught stop [options] [ipc-file] diff --git a/example/server.js b/example/server.js new file mode 100644 index 0000000..4472077 --- /dev/null +++ b/example/server.js @@ -0,0 +1,64 @@ +// npm install express express-domain-errors express-graceful-exit +var domain = require('domain'); +var domainError = require('express-domain-errors'); +var express = require('express'); +var gracefulExit = require('express-graceful-exit'); +var http = require('http'); +var util = require('util'); + +var serverDomain = domain.create(); +serverDomain.run(function () { + var app, server; + + function sendOfflineMsg() { + if (process.send) { + process.send('offline'); + } + } + + function doGracefulExit(err) { + console.log('Server shutting down'); + gracefulExit.gracefulExitHandler(app, server); + } + + process.on('message', function (message) { + if (message === 'shutdown') { + doGracefulExit(); + } + }); + + app = express(); + app.use(domainError(sendOfflineMsg, doGracefulExit)); + app.use(gracefulExit.middleware(app)); + + app.get('/', function (req, res) { + res.set('Content-Type', 'text/plain'); + res.send('Hello world\n'); + }); + app.get('/bad', function (req, res) { + process.nextTick(/*process.domain.intercept*/(function () { + nonexistentFunction(); + })); + }); + + app.use(function (req, res, next) { + var err = new Error(util.format('The requested URL %s was not found on this server.', req.url)); + err.status = 404; + next(err); + }); + + app.use(function (err, req, res, next) { + var status = err.status || 500; + var message = err.message || 'The server encountered an internal error or misconfiguration and was unable to complete your request.'; + res.set('Content-Type', 'text/plain'); + res.status(status); + res.send(http.STATUS_CODES[status] + '\n\n' + message + '\n'); + }); + + server = app.listen(process.env.LISTEN_FD ? {fd: parseInt(process.env.LISTEN_FD, 10)} : (process.env.PORT || 8000), function () { + console.log('Server listening on port %d', server.address().port); + if (process.send) { + process.send('online'); + } + }); +}); diff --git a/lib/daemon.js b/lib/daemon.js index 08545bd..05a39fc 100644 --- a/lib/daemon.js +++ b/lib/daemon.js @@ -22,6 +22,7 @@ function startDaemon(argv) { var script = argv.shift(); var nodeArgsStr = argv.shift(); var pidFile = argv.shift(); + var launchdSocket = argv.shift(); var naughtLog = null; var stderrLog = null; @@ -187,9 +188,38 @@ function startDaemon(argv) { var nodeArgs = splitCmdLine(nodeArgsStr); var stdoutValue = (stdoutBehavior === 'inherit') ? process.stdout : stdoutBehavior; var stderrValue = (stderrBehavior === 'inherit') ? process.stderr : stderrBehavior; + var listenFd = parseInt(process.env.LISTEN_FD, 10); + if (!listenFd) { + if (parseInt(process.env.LISTEN_PID, 10) === process.pid) { + // systemd + var listenFds = parseInt(process.env.LISTEN_FDS, 10); + if (listenFds === 0) { + // systemd provided no sockets + // log this? + } else if (listenFds > 1) { + // systemd provided too many sockets + // log this? + } else { + // SD_LISTEN_FDS_START + listenFd = 3; + } + } else if (launchdSocket) { + // launchd + try { + listenFd = require('node-launchd').getSocketFileDescriptorForName(launchdSocket); + } catch (e) { + // could not get launchd socket + } + } + } + var stdio = [process.stdin, stdoutValue, stderrValue, 'ipc']; + if (listenFd) { + process.env.LISTEN_FD = stdio.length; + stdio.push(listenFd); + } master = spawn(process.execPath, nodeArgs.concat([path.join(__dirname, "master.js"), workerCount, script]).concat(argv), { env: process.env, - stdio: [process.stdin, stdoutValue, stderrValue, 'ipc'], + stdio: stdio, cwd: process.cwd(), }); master.on('message', onMessage); diff --git a/lib/main.js b/lib/main.js index ca526f5..7137d8a 100755 --- a/lib/main.js +++ b/lib/main.js @@ -52,7 +52,8 @@ var cmds = { " --cwd " + CWD + "\n" + " --daemon-mode true\n" + " --remove-old-ipc false\n" + - " --node-args ''", + " --node-args ''\n" + + " --launchd-socket ''", fn: function(argv){ var options = { 'worker-count': '1', @@ -66,6 +67,7 @@ var cmds = { 'daemon-mode': 'true', 'remove-old-ipc': 'false', 'node-args': '', + 'launchd-socket': '', }; var arr = chompArgv(options, argv) , err = arr[0] @@ -379,7 +381,8 @@ function startScript(options, script, argv){ options['max-log-size'], path.resolve(CWD, script), options['node-args'], - options['pid-file'] + options['pid-file'], + options['launchd-socket'] ].concat(argv); if (options['daemon-mode']) { startDaemonChild(args); diff --git a/package.json b/package.json index b41012e..752647a 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,9 @@ "mkdirp": "~0.5.0", "async": "~0.9.0" }, + "optionalDependencies": { + "node-launchd": "0.0.3" + }, "bugs": { "url": "https://github.com/andrewrk/naught/issues" },