一个 Node 相关的项目中,老是少不了跑脚本。跑一个脚本拉取配置、处理一些数据以及定时任务更是屡见不鲜。html
在一些重要流程中可以看到脚本的身影:node
-
CI,用以测试、质量保障及部署等 -
Docker,用以构建镜像 -
Cron,用以定时任务
若是在这些重要流程中脚本出错没法及时发现问题,将有可能引起更加隐蔽的问题。web
最近观察项目镜像构建,会偶尔发现一两个镜像虽然构建成功,但容器却跑不起来的状况。「究其缘由,是由于 Exit Code
的问题」。docker
Exit Code
❝什么是 exit code?shell
❞
exit code
表明一个进程的返回码,经过系统调用 exit_group
来触发。在 POSIX
中,0
表明正常的返回码,1-255
表明异常返回码,通常主动抛出的错误码都是 1
。在 Node 应用中使用 process.exitCode = 1
来表明因不指望的异常而中断。编程
这里有一张关于异常码的附表 Appendix E. Exit Codes With Special Meanings[1]。api
异常码在操做系统中随处可见,如下是一个关于 cat
命令的异常以及它的 exit code
,并使用 strace
追踪系统调用。promise
$ cat a
cat: a: No such file or directory
# 使用 strace 查看 cat 的系统调用
# -e 只显示 write 与 exit_group 的系统调用
$ strace -e write,exit_group cat a
write(2, "cat: ", 5cat: ) = 5
write(2, "a", 1a) = 1
write(2, ": No such file or directory", 27: No such file or directory) = 27
write(2, "\n", 1
) = 1
exit_group(1) = ?
+++ exited with 1 +++
从系统调用的最后一行能够看出,该进行的 exit code
是 1,并把错误信息输出到 stderr
(标准错误的 fd 为 2) 中安全
如何查看 exit code
从 strace
中能够来判断进程的 exit code
,可是不够方便过于冗余,特别身处 shell 编程环境中。微信
「有一种简单的方法,经过 echo $?
来确认返回码」
$ cat a
cat: a: No such file or directory
$ echo $?
1
throw new Error
与 Promise.reject
区别
如下是两段代码,第一段抛出一个异常,第二段 Promise.reject
,两段代码都会以下打印出一段异常信息,那么二者有什么区别?
function error () {
throw new Error('hello, error')
}
error()
// Output:
// /Users/shanyue/Documents/note/demo.js:2
// throw new Error('hello, world')
// ^
//
// Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)
// at Object.<anonymous> (/Users/shanyue/Documents/note/demo.js:5:1)
// at Module._compile (internal/modules/cjs/loader.js:701:30)
async function error () {
return new Error('hello, error')
}
error()
// Output:
// (node:60356) UnhandledPromiseRejectionWarning: Error: hello, world
// at error (/Users/shanyue/Documents/note/demo.js:2:9)
// at Object.<anonymous> (/Users/shanyue/Documents/note/demo.js:5:1)
// at Module._compile (internal/modules/cjs/loader.js:701:30)
// at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)
// (node:2787) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
// (node:2787) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
在对上述两个测试用例使用 echo $?
查看 exit code,咱们会发现 throw new Error()
的 exit code
为 1,而 Promise.reject()
的为 0。
「从操做系统的角度来说,exit code
为 0 表明进程成功运行并退出,此时即便有 Promise.reject
,操做系统也会视为它执行成功。」
这在 Dockerfile
与 CI
中将留有安全隐患。
Dockerfile 在 node 中的注意点
当使用 Dockerfile
构建镜像时,若是 RUN
的进程返回非 0 的返回码,构建就会失败。
「而在 Node
中的错误处理中,咱们倾向于全部的异常都交由 async/await
来处理,而当发生异常时,因为此时 exit code 为 0 并不会致使镜像构建失败。」
这是一个浅显易懂的含 Promise.reject()
问题的镜像。
FROM node:12-alpine
RUN node -e "Promise.reject('hello, world')"
构建镜像过程以下:「即便在构建过程打印出了 unhandledPromiseRejection
信息,可是镜像仍然构建成功。」
$ docker build -t demo .
Sending build context to Docker daemon 33.28kB
Step 1/2 : FROM node:12-alpine
---> 18f4bc975732
Step 2/2 : RUN node -e "Promise.reject('hello, world')"
---> Running in 79a6d53c5aa6
(node:1) UnhandledPromiseRejectionWarning: hello, world
(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:1) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Removing intermediate container 79a6d53c5aa6
---> 09f07eb993fe
Successfully built 09f07eb993fe
Successfully tagged demo:latest
Promise.reject 脚本解决方案
能在编译时能发现的问题,毫不要放在运行时。因此,构建镜像或 CI 中须要执行 node 脚本时,对异常处理须要手动指定 process.exitCode = 1
来提早暴露问题
runScript().catch(() => {
process.exitCode = 1
})
在构建镜像时,也有关于异常解决方案的建议:
❝(node:1) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag
❞--unhandled-rejections=strict
(see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
根据提示,--unhandled-rejections=strict
将会把 Promise.reject
的退出码设置为 1
,并在未来的 node 版本中修正 Promise
异常退出码。
$ node --unhandled-rejections=strict error.js
--unhandled-rejections=strict
的配置对 node 有版本要求:
❝Added in: v12.0.0, v10.17.0
By default all unhandled rejections trigger a warning plus a deprecation warning for the very first unhandled rejection in case no unhandledRejection hook is used.
❞
总结
-
当进程结束的 exit code 为非 0 时,系统会认为该进程执行失败 -
经过 echo $?
可查看终端上一进程的 exit code -
Node 中 Promise.reject
时 exit code 为 0 -
Node 中能够经过 process.exitCode = 1
显式设置 exit code -
在 Node12+ 中能够经过 node --unhandled-rejections=strict error.js
执行脚本,视Promise.reject
的exit code
为 1
Reference
Appendix E. Exit Codes With Special Meanings: http://www.tldp.org/LDP/abs/html/exitcodes.html
本文分享自微信公众号 - 全栈成长之路(shanyue-road)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。