关于前端:Node-中脚本遭遇异常时如何安全退出

49次阅读

共计 5106 个字符,预计需要花费 13 分钟才能阅读完成。

本文收录于 GitHub 山月行博客: shfshanyue/blog,内含我在理论工作中碰到的问题、对于业务的思考及在全栈方向上的学习

  • 前端工程化系列
  • Node 进阶系列

一个 Node 相干的我的项目中,总是少不了跑脚本。跑一个脚本拉取配置、解决一些数据以及定时工作更是粗茶淡饭。

在一些重要流程中可能看到脚本的身影:

  1. CI,用以测试、品质保障及部署等
  2. Docker,用以构建镜像
  3. Cron,用以定时工作

如果在这些重要流程中脚本出错无奈及时发现问题,将有可能引发更加荫蔽的问题。

最近察看我的项目镜像构建,会偶然发现一两个镜像尽管构建胜利,但容器却跑不起来的状况。 究其原因,是因为 Exit Code 的问题

Exit Code

什么是 exit code?

exit code 代表一个过程的返回码,通过零碎调用 exit_group 来触发。在 POSIX 中,0 代表失常的返回码,1-255 代表异样返回码,个别被动抛出的错误码都是 1。在 Node 利用中应用 process.exitCode = 1 来代表因不冀望的异样而中断。

这里有一张对于异样码的附表 Appendix E. Exit Codes With Special Meanings。

异样码在操作系统中随处可见,以下是一个对于 cat 命令的异样以及它的 exit code,并应用 strace 追踪零碎调用。

$ 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 ErrorPromise.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,操作系统也会视为它执行胜利。

这在 DockerfileCI 中将留有安全隐患。

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.ht… (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.

总结

  1. 当过程完结的 exit code 为非 0 时,零碎会认为该过程执行失败
  2. 通过 echo $? 可查看终端上一过程的 exit code
  3. Node 中 Promise.reject 时 exit code 为 0
  4. Node 中能够通过 process.exitCode = 1 显式设置 exit code
  5. 在 Node12+ 中能够通过 node --unhandled-rejections=strict error.js 执行脚本,视 Promise.rejectexit code 为 1

关注我

扫码增加我的微信,备注进群,退出高级前端进阶群

<figure>
<img width=”240″ src=”https://user-gold-cdn.xitu.io/2020/6/29/172fe14e18d2b38c?w=430&h=430&f=jpeg&s=38173″ alt=” 加我微信拉你进入面试交换群 ”>
<figcaption> 加我微信拉你进入面试交换群 </figcaption>
</figure>

欢送关注公众号【全栈成长之路】,定时推送 Node 原创及全栈成长文章

<figure>
<img width=”240″ src=”https://shanyue.tech/qrcode.jpg” alt=” 欢送关注 ”>
<figcaption> 欢送关注全栈成长之路 </figcaption>
</figure>

正文完
 0