abliger

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 方法,其他方法都是工具方法 我们一块一块分析
首先入眼可见的是

  1. var 变量声明

var arg, next, watch, ignore, pidFilePath, program, extensions, executor, poll_interval, debugFlag, debugBrkFlag, debugBrkFlagArg, harmony, inspect;

  1. 初始化参数、变量
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 数组。但我无论如何可执行文件和参数都需要放到最后。否者会有一些参数没有解析

  1. 初始化参数
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')
}
  1. 父进程监听退出事件
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)
		})
	})
}
  1. 打印信息
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('')
  1. 执行项目运行文件
// 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())
	}
}
  1. 实现交互
    输入 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)
}
  1. 配置忽略文件
if (ignore) {
	var ignoreItems = ignore.split(',')
	ignoreItems.forEach(function (ignoreItem) {
		ignoreItem = path.resolve(ignoreItem)
		ignoredPaths[ignoreItem] = true // 生成忽略文件的map映射 true忽略
		log("Ignoring directory '" + ignoreItem + "'.")
	})
}
  1. 监控文件
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 的文件模块监控文件的变化。当文件发生变化,会把子线程杀死,再重新启动。如果项目较大,效率实属不高,比较适合小型项目的使用

posted on 2021-06-03 10:46  abliger  阅读(380)  评论(0)    收藏  举报

导航