supervisor 的使用和源码分析
supervisor
supervisor 是什么
supervisor 是 nodeJs 热加载插件 ,它可以监控代码中的变化来实现动态加载
启动和使用
# 安装
npm i supervisor -D # 局部安装
npm i supervisor -g -D # 全局安装
# 使用
supervisor <file> # 全局安装
npx supervisor <file> # 局部安装
客户端代码 -shell
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;;
esac
# 如果有 node_modules/.bin/node 且可以执行
if [ -x "$basedir/node" ]; then
"$basedir/node" "$basedir/../supervisor/lib/cli-wrapper.js" "$@" # $@ 执行shell文件的命令参数
ret=$?
else
# 使用全局的node命令运行 node_modules中的supervisor 模块 开到先调用cli-wrapper.js
node "$basedir/../supervisor/lib/cli-wrapper.js" "$@"
ret=$? # $? 变量的值不是 0 的话,就表示上一个命令在执行的过程中出错了
fi
exit $ret
cli-wrapper 文件
#!/usr/bin/env node
var path = require('path'),
fs = require('fs'),
args = process.argv.slice(1) // slice(1) 返回 下表 1 - end 的子数组
var arg, base
do
arg = args.shift() // shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
while (
fs.realpathSync(arg) !== __filename &&
!(base = path.basename(arg)).match(
/^supervisor$|^supervisor.js$|^node-supervisor$/
)
) // 检查args[1] 是否是 本文件 后把 参数传给arg
require('./supervisor').run(args) //运行node_module/supervisor 的run方法
supervisor 文件
文件结构
进入文件,首先查看文件结构

help
我们要了解一个事务的运行,可以先从它的使用看起
function help() {
print("")("Node Supervisor is used to restart programs when they crash.")(
"It can also be used to restart programs when a *.js file changes."
)("")("Usage:")(" supervisor [options] <program>")...
这里我们发现 help 函数调用了 print 函数,而且 print 函数后面跟随了多个小括号,即:可以连续调用多次。
接下来我们查看 print 函数,print 函数不是 nodeJs 自带的函数,它在 supervisor.js 中定义
function print(m) {
console.log(m)
return print
}
可以从 print(m) 中发现 print 实际调用的是 console.log() 函数。但是 print()函数,最后把 print 自身返回的出去。
他就是函数版的链式调用,注意此处不能返回 this ,原因是 this 指向的是 node 的全局变量。
运行结果 ->中文翻译
Supervisor 是一个被用来重启崩溃的项目。它也能被用来 监控一个 *.js文件的改变来重启项目
用法:
supervisor [options] <program>
supervisor [options] -- <program> [args ...]
必须:
<program>
项目的启动文件
可选项:
-w|--watch <watchItems>
要监视更改的文件夹或js文件的逗号分隔列表。
当js文件发生更改时,重新加载程序
默认值为“.”
-i|--ignore <ignoreItems>
要忽略的文件夹或js文件的逗号分隔列表。
无默认值
--ignore-symlinks
在查找要监视的文件时启用符号链接(symbolic links)忽略。
-p|--poll-interval <milliseconds>
How often to poll watched files for changes.
Defaults to Node default.
轮询监视文件的更改频率。 单位毫秒
默认为node默认值。
-e|--extensions <extensions>
除了默认值之外,还要监视的特定文件扩展名。
当--watch选项包括文件夹时使用
默认为'node,js'
-x|--exec <executable>
运行指定工程的可执行程序
默认为'node'
--debug[=port]
Start node with --debug flag.
--debug-brk[=port]
Start node with --debug-brk[=port] flag.
--harmony
Start node with --harmony flag.
--inspect
Start node with --inspect flag.
--harmony_default_parameters
Start node with --harmony_default_parameters flag.
-n|--no-restart-on error|exit
如果监督程序结束,不要自动重新启动。
Supervisor 将等待源文件中的更改。
如果内容是“error”,退出代码0仍将重新启动。
如果内容是“退出”,则不管退出代码如何,都不能重新启动。
如果内容是“成功”,则只有退出代码为0时才不重新启动。
-t|--non-interactive
禁用交互容量。
有了这个选项,supervisor 就不会监听标准输入。
-k|--instant-kill
使用 SIGKILL(-9)终止child而不是更温和的SIGTERM
--force-watch
使用fs.watch 代替 fs.watchFile.
如果您在windows机器上看到高cpu负载,这可能很有用。
-s|--timestamp
打印运行后的日志时间戳。
使任务上次运行的时间更容易判断。
-h|--help|-?
展示用法说明
-q|--quiet
抑制调试消息
-V|--verbose
显示额外的调试消息
Options available after start:
rs - 重启进程.
如果没有更改文件,则用于重新启动 supervisor
例子:
supervisor myapp.js
supervisor myapp.coffee
supervisor -w scripts -e myext -x myrunner myapp
supervisor -- server.js -h host -p port
run 方法
supervisor 只运行 run 方法,其他方法都是工具方法 我们一块一块分析
首先入眼可见的是
- var 变量声明
var arg, next, watch, ignore, pidFilePath, program, extensions, executor, poll_interval, debugFlag, debugBrkFlag, debugBrkFlagArg, harmony, inspect;
- 初始化参数、变量
while ((arg = args.shift())) {
// 运行到args 为空为止
if (arg === '--help' || arg === '-h' || arg === '-?') {
return help()
} else if (arg === '--quiet' || arg === '-q') {
debug = false
log = function () {}
} else if (arg === '--harmony') {
harmony = true
} else if (arg === '--inspect') {
inspect = true
} else if (arg === '--harmony_default_parameters') {
harmony_default_parameters = true
} else if (arg === '--harmony_destructuring') {
harmony_destructuring = true
} else if (arg === '--verbose' || arg === '-V') {
verbose = true
} else if (arg === '--restart-verbose' || arg === '-RV') {
restartVerbose = true
} else if (arg === '--watch' || arg === '-w') {
watch = args.shift()
} else if (arg == '--non-interactive' || arg === '-t') {
interactive = false
} else if (arg === '--ignore' || arg === '-i') {
ignore = args.shift()
} else if (arg === '--save-pid' || arg === '-pid') {
pidFilePath = args.shift()
} else if (arg === '--ignore-symlinks') {
ignoreSymLinks = true
} else if (arg === '--poll-interval' || arg === '-p') {
poll_interval = parseInt(args.shift())
} else if (arg === '--extensions' || arg === '-e') {
extensions = args.shift()
} else if (arg === '--exec' || arg === '-x') {
executor = args.shift()
} else if (arg === '--no-restart-on' || arg === '-n') {
noRestartOn = args.shift()
} else if (
arg.indexOf('--debug') > -1 &&
arg.indexOf('--debug-brk') === -1
) {
debugFlag = arg
} else if (arg.indexOf('--debug-brk') >= 0) {
debugBrkFlag = true
debugBrkFlagArg = arg
} else if (arg === '--force-watch') {
forceWatchFlag = true
} else if (arg === '--instant-kill' || arg === '-k') {
instantKillFlag = true
} else if (arg === '--timestamp' || arg === '-s') {
timestampFlag = true
} else if (arg === '--') {
program = args
break
} else if (arg[0] != '-' && !args.length) {
// Assume last arg is the program
program = [arg]
}
}
我们这里主要关注 program 数组 , 它存储了要执行的文件和执行文件需要的参数,注意最后两个 else if 都是给 program 赋值。 第一个 解析 -- 后,把剩下的所有的参数都赋值给 program 数组。第二个,就是简单的把最后一个参数 赋给 program 数组。但我无论如何可执行文件和参数都需要放到最后。否者会有一些参数没有解析
- 初始化参数
if (!program) {
return help() // 如果program 参数为空,调用help方法,并终止程序
}
// 如果值不存在,初始化默认值
if (!watch) {
watch = '.' // 表示命令行,运行目录
}
if (!poll_interval) {
poll_interval = 1000 // 每一秒监控一次
}
// 获得[*.js,.js] 数组
var programExt = program.join(' ').match(/.*\.(\S*)/)
// 获得文件的拓展名 programExt 为真 返回 programExt[1],为假 返回 programExt
programExt = programExt && programExt[1]
if (!extensions) {
// 如果 --extensions 为空
// If no extensions passed try to guess from the program
extensions = 'node,js'
if (programExt && extensions.indexOf(programExt) == -1) {
// 如果programExt 不是node或js的类别
// Support coffee and litcoffee extensions
if (programExt === 'coffee' || programExt === 'litcoffee') {
extensions += ',coffee,litcoffee'
} else {
extensions += ',' + programExt
}
}
}
// 可监控文件的正则
fileExtensionPattern = new RegExp(
'^.*.(' + extensions.replace(/,/g, '|') + ')$'
)
// 初始化executor
if (!executor) {
executor =
programExt === 'coffee' || programExt === 'litcoffee'
? 'coffee'
: 'node' // 如果使用coffeescript 语言可自动换成对应的执行器
}
4.封装 node 运行参数
// 把参数重新放入数组
if (debugFlag) {
program.unshift(debugFlag)
}
if (debugBrkFlag) {
program.unshift(debugBrkFlagArg)
}
if (harmony) {
program.unshift('--harmony')
}
if (inspect) {
program.unshift('--inspect')
}
if (harmony_default_parameters) {
program.unshift('--harmony_default_parameters')
}
if (harmony_destructuring) {
program.unshift('--harmony_destructuring')
}
if (executor === 'coffee' && (debugFlag || debugBrkFlag)) {
// coffee does not understand debug or debug-brk, make coffee pass options to node
program.unshift('--nodejs')
}
- 父进程监听退出事件
if (pidFilePath) {
// pidFilePath 是通过 --save-pid 获得参数的, 但help没有对应说明
var pid = process.pid
//
// verify if we have write access canWrite 是用来验证pidFilePath 这个文件能否打开,写入
//
canWrite(pidFilePath, function (err) {
if (err) {
log('Continuing...')
} else {
fs.writeFileSync(pidFilePath, pid + '\n') // 如果验证无错,把pid 存入文件中
}
})
}
var deletePidFile = function () {
fs.exists(pidFilePath, function (exists) {
//exists函数过时
if (exists) {
log('Removing pid file')
fs.unlinkSync(pidFilePath) // 删除文件
} else {
log('No pid file to remove...')
}
process.exit()
})
}
try {
// Pass kill signals through to child
;['SIGTERM', 'SIGINT', 'SIGHUP', 'SIGQUIT'].forEach(function (signal) {
process.on(signal, function () {
//process.on()方法可以监听进程事件。 后跟回调函数
var child = exports.child
if (child) {
log('Received ' + signal + ', killing child process...')
child.kill(signal)
}
if (pidFilePath) {
deletePidFile()
} else {
process.exit()
}
})
})
} catch (e) {
// Windows doesn't support signals yet, so they simply don't get this handling.
// https://github.com/joyent/node/issues/1553
}
// 添加进程退出函数
process.on('exit', function () {
var child = exports.child
if (child) {
log('Parent process exiting, terminating child...')
child.kill('SIGTERM')
}
})
工具函数
/**
* Determine if a file can be written
*/
function canWrite(path, callback) {
fs.open(path, 'w', function (err, fd) {
if (err) {
if (err.code === 'EISDIR') {
log("Can't open " + path + ". It's a directory.")
}
if (err.code === 'EACCESS') {
log("Can't open " + path + '. No access.')
} else {
log("Can't open " + path + '.')
}
return callback(err)
}
fs.close(fd, function (err) {
if (err) return callback(err)
callback(null, true)
})
})
}
- 打印信息
log('')
log('Running node-supervisor with')
log(" program '" + program.join(' ') + "'")
log(" --watch '" + watch + "'")
if (!interactive) {
log(' --non-interactive')
}
if (ignore) {
log(" --ignore '" + ignore + "'")
}
if (pidFilePath) {
log(" --save-pid '" + pidFilePath + "'")
}
log(" --extensions '" + extensions + "'")
log(" --exec '" + executor + "'")
log('')
- 执行项目运行文件
// store the call to startProgramm in startChildProcess
// in order to call it later
startChildProcess = function () {
startProgram(program, executor) // executor default node program 输入的重排后的命令参数
}
// if we have a program, then run it, and restart when it crashes.
// if we have a watch folder, then watch the folder for changes and restart the prog
startChildProcess()
启动函数
function startProgram(prog, exec) {
log("Starting child process with '" + exec + ' ' + prog.join(' ') + "'")
crash_queued = false
/*
stdio: "inherit"
将相应的 stdio 流传给父进程或从父进程传入。
*/
var child = (exports.child = spawn(exec, prog, { stdio: 'inherit' }))
// Support for Windows ".cmd" files
// On Windows 8.1, spawn can't launch apps without the .cmd extention
// If already retried, let the app crash ... :'(
if (process.platform === 'win32' && exec.indexOf('.cmd') == -1) {
// 如果是win32平台 且命令没有.cmd 结尾 监听运行错误 重新执行
child.on('error', function (err) {
if (err.code === 'ENOENT') return startProgram(prog, exec + '.cmd')
})
}
// 注意此处当stdio 为inherit时 child.stdout为ull ,把spawn 注释掉配置 if条件可以进入
if (child.stdout) {
// node < 0.8 doesn't understand the 'inherit' option, so pass through manually
// 捕获子线程输出
child.stdout.addListener('data', function (chunk) {
chunk && console.log(chunk)
})
child.stderr.addListener('data', function (chunk) {
chunk && console.error(chunk)
})
}
// 子线程退出配置
child.addListener('exit', function (code) {
logTimestamp()
if (!crash_queued) {
log(
'Program ' +
exec +
' ' +
prog.join(' ') +
' exited with code ' +
code +
'\n'
)
exports.child = null
if (
noRestartOn == 'exit' ||
(noRestartOn == 'error' && code !== 0) ||
(noRestartOn == 'success' && code === 0)
)
// 如果不配置--no-restart-on 此条件永远不会执行
return
}
startProgram(prog, exec)
})
}
function logTimestamp() {
if (timestampFlag) {
// use console.log() directly rather than log() so that -q/--quiet
// does not override/silence it
console.log(Date().toString())
}
}
- 实现交互
输入 rs 重启子线程
// If interaction has not been disabled, start the CLI
if (interactive) {
//
// Read input from stdin
//
var stdin = process.stdin
stdin.setEncoding('utf8')
// 坚挺控制台输入
stdin.on('readable', function () {
var chunk = process.stdin.read() // 读取输入信息 此处只能读入一次,具体问题不知道,但可以stdin.addlistener(data,callback) 事件正确读入
//
// Restart process when user inputs rs
//
if ((chunk !== null && chunk === 'rs\n') || chunk === 'rs\r\n') {
// process.stdout.write('data: ' + chunk);
crash()
}
})
}
function crash() {
if (crash_queued) return
crash_queued = true
var child = exports.child
setTimeout(function () {
if (child) {
// 根据--instant-kill配置 instantKillFlag 选择杀死进程的方式
if (instantKillFlag) {
log('crashing child with SIGKILL')
process.kill(child.pid, 'SIGKILL')
} else {
log('crashing child')
process.kill(child.pid, 'SIGTERM')
}
} else {
log('restarting child')
startChildProcess()
}
}, 50)
}
- 配置忽略文件
if (ignore) {
var ignoreItems = ignore.split(',')
ignoreItems.forEach(function (ignoreItem) {
ignoreItem = path.resolve(ignoreItem)
ignoredPaths[ignoreItem] = true // 生成忽略文件的map映射 true忽略
log("Ignoring directory '" + ignoreItem + "'.")
})
}
- 监控文件
var watchItems = watch.split(',')
watchItems.forEach(function (watchItem) {
watchItem = path.resolve(watchItem)
if (!ignoredPaths[watchItem]) {
// 如果监控的文件不在忽略文件中 ,进入if
log("Watching directory '" + watchItem + "' for changes.")
if (interactive) {
log('Press rs for restarting the process.')
}
findAllWatchFiles(watchItem, function (f) {
watchGivenFile(f, poll_interval)
})
}
})
var findAllWatchFiles = function (dir, callback) {
dir = path.resolve(dir)
if (ignoredPaths[dir]) return
fs[ignoreSymLinks ? 'lstat' : 'stat'](dir, function (err, stats) {
if (err) {
console.error('Error retrieving stats for file: ' + dir)
} else {
if (ignoreSymLinks && stats.isSymbolicLink()) {
log("Ignoring symbolic link '" + dir + "'.")
return
}
if (stats.isDirectory()) {
if (isWindowsWithoutWatchFile || forceWatchFlag) callback(dir)
fs.readdir(dir, function (err, fileNames) {
if (err) {
console.error('Error reading path: ' + dir)
} else {
fileNames.forEach(function (fileName) {
findAllWatchFiles(
path.join(dir, fileName),
callback
) // 递归直到找到本目录所有文件
})
}
})
} else {
if (
(!isWindowsWithoutWatchFile || !forceWatchFlag) &&
dir.match(fileExtensionPattern)
) {
callback(dir)
}
}
}
})
}
var nodeVersion = process.version.split('.')
var isWindowsWithoutWatchFile =
process.platform === 'win32' && parseInt(nodeVersion[1]) <= 6
function watchGivenFile(watch, poll_interval) {
if (isWindowsWithoutWatchFile || forceWatchFlag) {
fs.watch(watch, { persistent: true, interval: poll_interval }, crashWin)
} else {
fs.watchFile(
watch,
{ persistent: true, interval: poll_interval },
function (oldStat, newStat) {
// we only care about modification time, not access time.
if (newStat.mtime.getTime() !== oldStat.mtime.getTime()) {
if (verbose) {
log('file changed: ' + watch)
}
}
crash() // 文件变化重启子线程
}
)
}
if (verbose) {
log("watching file '" + watch + "'")
}
}
supervisor 源码分析到这里,可以发现 supervisor 的原理就是开启子线程运行项目启动文件,在使用 node 的文件模块监控文件的变化。当文件发生变化,会把子线程杀死,再重新启动。如果项目较大,效率实属不高,比较适合小型项目的使用
浙公网安备 33010602011771号