├── .eslintignore ├── example ├── test.js └── master.js ├── .gitignore ├── package.json ├── LICENSE ├── bin └── nounou ├── .eslintrc ├── README.md └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | *.debug.js 2 | *.min.js 3 | node_modules/* 4 | -------------------------------------------------------------------------------- /example/test.js: -------------------------------------------------------------------------------- 1 | setTimeout(function () { 2 | process.send({type: 'suicide'}); 3 | process.exit(0); 4 | // throw new Error('exit with exception'); 5 | }, 1000); 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nounou", 3 | "version": "1.2.1", 4 | "description": "Node.js process daemon", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "make test" 8 | }, 9 | "bin": { 10 | "nounou": "bin/nounou" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/JacksonTian/nounou.git" 15 | }, 16 | "keywords": [ 17 | "daemon", "nounou" 18 | ], 19 | "author": "Jackson Tian", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/JacksonTian/nounou/issues" 23 | }, 24 | "homepage": "https://github.com/JacksonTian/nounou#readme", 25 | "files": [ 26 | "index.js", 27 | "bin" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /example/master.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const path = require('path'); 5 | const nounou = require('../'); 6 | const timerPath = path.join(__dirname, 'test.js'); 7 | 8 | // 任务进程 9 | nounou(timerPath).on('fork', function (worker) { 10 | console.log('[%s] [worker:%d] new task worker start', Date(), worker.pid); 11 | }).on('disconnect', function (worker) { 12 | console.error('[%s] [master:%s] task worker: %s disconnect.', 13 | Date(), process.pid, worker.pid); 14 | }).on('unexpectedExit', function (worker, code, signal) { 15 | var err = new Error(util.format('task worker %s died (code: %s, signal: %s)', 16 | worker.pid, code, signal)); 17 | err.name = 'WorkerDiedError'; 18 | console.error('[%s] [master:%s] worker exit: %s', Date(), process.pid, err.stack); 19 | }).on('reachReforkLimit', function () { 20 | console.error('Too much refork!!!!!!'); 21 | }); 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jackson Tian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /bin/nounou: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | 6 | const nounou = require('../'); 7 | 8 | const [workerPath, count] = process.argv.slice(2); 9 | 10 | const filepath = path.resolve(process.cwd(), workerPath); 11 | const name = path.basename(filepath, '.js'); 12 | 13 | const pid = process.pid; 14 | 15 | function utcTime() { 16 | return new Date().toUTCString(); 17 | } 18 | 19 | nounou(filepath, { 20 | count: parseInt(count, 10) || 1 21 | }) 22 | .on('fork', function (worker) { 23 | console.log(`[${utcTime()}] [master:${pid}] [${name}:${worker.pid}]` + 24 | ` new worker start`); 25 | }) 26 | .on('disconnect', function (worker) { 27 | console.error(`[${utcTime()}] [master:${pid}] [${name}:${worker.pid}]` + 28 | ` disconnect, suicide: ${worker.suicide}.`); 29 | }) 30 | .on('unexpectedExit', function (worker, code, signal) { 31 | var message = `${name} ${worker.pid} died (code: ${code}, signal: ${signal})`; 32 | var err = new Error(message); 33 | err.name = name + 'DiedError'; 34 | console.error(`${utcTime()} [${pid}] worker exit: ${err.stack}`); 35 | }).on('reachReforkLimit', function () { 36 | console.error(`${utcTime()} [${pid}] ${name} Too much refork!!!!!!`); 37 | }); 38 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 2 6 | ], 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ], 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "semi": [2, "always"], 16 | "strict": [2, "global"], 17 | "curly": 2, 18 | "eqeqeq": 2, 19 | "no-eval": 2, 20 | "guard-for-in": 2, 21 | "no-caller": 2, 22 | "no-else-return": 2, 23 | "no-eq-null": 2, 24 | "no-extend-native": 2, 25 | "no-extra-bind": 2, 26 | "no-floating-decimal": 2, 27 | "no-implied-eval": 2, 28 | "no-labels": 2, 29 | "no-with": 2, 30 | "no-loop-func": 1, 31 | "no-native-reassign": 2, 32 | "no-redeclare": [2, {"builtinGlobals": true}], 33 | "no-delete-var": 2, 34 | "no-shadow-restricted-names": 2, 35 | "no-undef-init": 2, 36 | "no-use-before-define": 2, 37 | "no-unused-vars": [2, {"args": "none"}], 38 | "no-undef": 2, 39 | "callback-return": [2, ["callback", "cb", "next"]], 40 | "global-require": 0, 41 | "no-console": 0 42 | }, 43 | "env": { 44 | "es6": true, 45 | "node": true, 46 | "browser": true 47 | }, 48 | "globals": { 49 | "describe": true, 50 | "it": true, 51 | "before": true, 52 | "after": true 53 | }, 54 | "extends": "eslint:recommended" 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nounou(保姆) 2 | Node.js process deamon. 3 | 4 | ## 非 Cluster 模式的进程守护 5 | cluster 的 fork 形式仅能对单一进程使用。nounou 适合其他情况,比如守护定时任务。 6 | 7 | > 注意:nounou 仅负责重启,不负责调度,不负责负载均衡。 8 | 9 | ## Usage 10 | 11 | 编程式使用: 12 | 13 | ```js 14 | const nounou = require('nounou'); 15 | const timerPath = '/path/to/timer.js'; 16 | 17 | // 任务进程 18 | nounou(timerPath).on('fork', (worker) => { 19 | console.log('[%s] [%d] new task worker start', Date(), worker.pid); 20 | }).on('disconnect', (worker) => { 21 | console.error('[%s] [%s] task worker: %s disconnect.', 22 | Date(), process.pid, worker.pid); 23 | }).on('unexpectedExit', (worker, code, signal) => { 24 | var err = new Error(util.format('task worker %s died (code: %s, signal: %s)', 25 | worker.pid, code, signal)); 26 | err.name = 'WorkerDiedError'; 27 | console.error('[%s] [%s] worker exit: %s', Date(), process.pid, err.stack); 28 | }).on('reachReforkLimit', () => { 29 | console.error('Too much refork!!!!!!'); 30 | }); 31 | ``` 32 | 33 | 命令式使用: 34 | 35 | ```sh 36 | $ nounou /path/to/timer.js 37 | # multi workers 38 | $ nounou /path/to/timer.js 2 39 | ``` 40 | 41 | ## 正常退出 42 | 如果子进程在运行一段时间后需要退出,之后无需重启。需要通过如下方式进行退出: 43 | 44 | ```js 45 | process.send({type: 'suicide'}); 46 | process.exit(0); 47 | ``` 48 | 49 | 该行为会触发 `expectedExit` 事件,标志退出符合预期,无需重启。 50 | 51 | ## Events 52 | 53 | - `exit`。退出事件。 54 | - `expectedExit`。预期的退出事件。 55 | - `unexpectedExit`。非预期的退出事件。 56 | - `disconnect`。IPC通道断开的事件。 57 | - `reachReforkLimit`。单位时间内重启次数达到上限。该事件后,进程不会再次重启。 58 | 59 | ## 注意事项 60 | 通常 kill 掉 nounou 主进程,它守护的子进程并不会随之而退出。如需子进程跟随父进程退出,需要以下代码: 61 | 62 | ```js 63 | // exiting with parent process 64 | process.on('disconnect', () => { 65 | console.log('exiting with parent process'); 66 | process.exit(0); 67 | }); 68 | ``` 69 | 70 | ## License 71 | The MIT license 72 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitter = require('events'); 4 | const util = require('util'); 5 | const fork = require('child_process').fork; 6 | 7 | module.exports = function (modulePath, options = {}) { 8 | const refork = options.refork !== false; 9 | const limit = options.limit || 10; 10 | const count = options.count || 1; 11 | const duration = options.duration || 60000; // 1 min 12 | const disconnects = {}; 13 | var disconnectCount = 0; 14 | var unexpectedCount = 0; 15 | 16 | const deamon = new EventEmitter(); 17 | const reforks = []; 18 | 19 | /** 20 | * allow refork 21 | */ 22 | function allow() { 23 | if (!refork) { 24 | return false; 25 | } 26 | 27 | const times = reforks.push(Date.now()); 28 | 29 | if (times > limit) { 30 | reforks.shift(); 31 | } 32 | 33 | const span = reforks[reforks.length - 1] - reforks[0]; 34 | const canFork = reforks.length < limit || span > duration; 35 | 36 | if (!canFork) { 37 | deamon.emit('reachReforkLimit'); 38 | } 39 | 40 | return canFork; 41 | } 42 | 43 | /** 44 | * uncaughtException default handler 45 | */ 46 | function onerror(err) { 47 | console.error('[%s] [%s] master uncaughtException: %s', Date(), process.pid, err.stack); 48 | console.error(err); 49 | console.error('(total %d disconnect, %d unexpected exit)', disconnectCount, unexpectedCount); 50 | } 51 | 52 | /** 53 | * unexpectedExit default handler 54 | */ 55 | function onUnexpected(worker, code, signal) { 56 | const err = new Error(util.format('worker:%s died unexpected (code: %s, signal: %s)', 57 | worker.pid, code, signal)); 58 | err.name = 'UnexpectedWorkerDiedError'; 59 | 60 | console.error('[%s] [%s] (total %d disconnect, %d unexpected exit) %s', 61 | Date(), process.pid, disconnectCount, unexpectedCount, err.stack); 62 | } 63 | 64 | var _fork = function () { 65 | const cp = fork(modulePath, options.args); 66 | deamon.emit('fork', cp); 67 | cp.on('disconnect', function () { 68 | deamon.emit('disconnect', cp); 69 | }); 70 | cp.on('exit', function (code, signal) { 71 | deamon.emit('exit', cp, code, signal); 72 | }); 73 | cp.on('message', function (message) { 74 | if (message && message.type === 'suicide') { 75 | cp.suicide = true; 76 | } 77 | }); 78 | }; 79 | 80 | var _refork = function () { 81 | if (allow()) { 82 | _fork(); 83 | } 84 | }; 85 | 86 | process.nextTick(function () { 87 | for (var i = 0; i < count; i++) { 88 | _fork(); 89 | } 90 | }); 91 | 92 | deamon.on('disconnect', function (worker) { 93 | disconnectCount++; 94 | disconnects[worker.pid] = new Date(); 95 | if (!worker.suicide) { 96 | _refork(); 97 | } 98 | }); 99 | 100 | deamon.on('exit', function (worker, code, signal) { 101 | // ignore suicied or exit normally worker 102 | if (worker.suicide || code === 0) { 103 | deamon.emit('expectedExit', worker, code, signal); 104 | return; 105 | } 106 | 107 | if (disconnects[worker.pid]) { 108 | delete disconnects[worker.pid]; 109 | // worker disconnect first, exit expected 110 | return; 111 | } 112 | 113 | unexpectedCount++; 114 | _refork(); 115 | deamon.emit('unexpectedExit', worker, code, signal); 116 | }); 117 | 118 | // defer to set the listeners 119 | // so you can listen this by your own 120 | process.nextTick(function () { 121 | if (process.listenerCount('uncaughtException') === 0) { 122 | process.on('uncaughtException', onerror); 123 | } 124 | 125 | if (deamon.listenerCount('unexpectedExit') === 0) { 126 | deamon.on('unexpectedExit', onUnexpected); 127 | } 128 | }); 129 | 130 | return deamon; 131 | }; 132 | --------------------------------------------------------------------------------