Node 腳本遭遇異常時(shí)如何安全退出
一個(gè) Node 相關(guān)的項(xiàng)目中,總是少不了跑腳本。跑一個(gè)腳本拉取配置、處理一些數(shù)據(jù)以及定時(shí)任務(wù)更是家常便飯。
在一些重要流程中能夠看到腳本的身影:
- CI,用以測(cè)試、質(zhì)量保障及部署等
- Docker,用以構(gòu)建鏡像
- Cron,用以定時(shí)任務(wù)
如果在這些重要流程中腳本出錯(cuò)無法及時(shí)發(fā)現(xiàn)問題,將有可能引發(fā)更加隱蔽的問題。
最近觀察項(xiàng)目鏡像構(gòu)建,會(huì)偶爾發(fā)現(xiàn)一兩個(gè)鏡像雖然構(gòu)建成功,但容器卻跑不起來的情況?!妇科湓?,是因?yàn)?Exit Code 的問題」。
Exit Code
什么是 exit code?
exit code 代表一個(gè)進(jìn)程的返回碼,通過系統(tǒng)調(diào)用 exit_group 來觸發(fā)。在 POSIX 中,0 代表正常的返回碼,1-255 代表異常返回碼,一般主動(dòng)拋出的錯(cuò)誤碼都是 1。在 Node 應(yīng)用中使用 process.exitCode = 1 來代表因不期望的異常而中斷。
這里有一張關(guān)于異常碼的附表 Appendix E. Exit Codes With Special Meanings[1]。
異常碼在操作系統(tǒng)中隨處可見,以下是一個(gè)關(guān)于 cat 命令的異常以及它的 exit code,并使用 strace 追蹤系統(tǒng)調(diào)用。
- $ cat a
- cat: a: No such file or directory
- # 使用 strace 查看 cat 的系統(tǒng)調(diào)用
- # -e 只顯示 write 與 exit_group 的系統(tǒng)調(diào)用
- $ 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 +++
從系統(tǒng)調(diào)用的最后一行可以看出,該進(jìn)行的 exit code 是 1,并把錯(cuò)誤信息輸出到 stderr (標(biāo)準(zhǔn)錯(cuò)誤的 fd 為 2) 中
如何查看 exit code
從 strace 中可以來判斷進(jìn)程的 exit code,但是不夠方便過于冗余,特別身處 shell 編程環(huán)境中。
「有一種簡(jiǎn)單的方法,通過 echo $? 來確認(rèn)返回碼」
- $ cat a
- cat: a: No such file or directory
- $ echo $?
- 1
throw new Error與Promise.reject區(qū)別
以下是兩段代碼,第一段拋出一個(gè)異常,第二段 Promise.reject,兩段代碼都會(huì)如下打印出一段異常信息,那么兩者有什么區(qū)別?
- 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)
- 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)
在對(duì)上述兩個(gè)測(cè)試用例使用 echo $? 查看 exit code,我們會(huì)發(fā)現(xiàn) throw new Error() 的 exit code 為 1,而 Promise.reject() 的為 0。
「從操作系統(tǒng)的角度來講,exit code 為 0 代表進(jìn)程成功運(yùn)行并退出,此時(shí)即使有 Promise.reject,操作系統(tǒng)也會(huì)視為它執(zhí)行成功?!?/p>
這在 Dockerfile 與 CI 中將留有安全隱患。
Dockerfile 在 node 中的注意點(diǎn)
當(dāng)使用 Dockerfile 構(gòu)建鏡像時(shí),如果 RUN 的進(jìn)程返回非 0 的返回碼,構(gòu)建就會(huì)失敗。
「而在 Node 中的錯(cuò)誤處理中,我們傾向于所有的異常都交由 async/await 來處理,而當(dāng)發(fā)生異常時(shí),由于此時(shí) exit code 為 0 并不會(huì)導(dǎo)致鏡像構(gòu)建失敗。」
這是一個(gè)淺顯易懂的含 Promise.reject() 問題的鏡像。
- FROM node:12-alpine
- RUN node -e "Promise.reject('hello, world')"
構(gòu)建鏡像過程如下:「即使在構(gòu)建過程打印出了 unhandledPromiseRejection 信息,但是鏡像仍然構(gòu)建成功?!?/p>
- $ 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 腳本解決方案
能在編譯時(shí)能發(fā)現(xiàn)的問題,絕不要放在運(yùn)行時(shí)。所以,構(gòu)建鏡像或 CI 中需要執(zhí)行 node 腳本時(shí),對(duì)異常處理需要手動(dòng)指定 process.exitCode = 1 來提前暴露問題
- runScript().catch(() => {
- process.exitCode = 1
- })
在構(gòu)建鏡像時(shí),也有關(guān)于異常解決方案的建議:
(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) |
根據(jù)提示,--unhandled-rejections=strict 將會(huì)把 Promise.reject 的退出碼設(shè)置為 1,并在將來的 node 版本中修正 Promise 異常退出碼。
- $ node --unhandled-rejections=strict error.js
--unhandled-rejections=strict 的配置對(duì) 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. |
總結(jié)
- 當(dāng)進(jìn)程結(jié)束的 exit code 為非 0 時(shí),系統(tǒng)會(huì)認(rèn)為該進(jìn)程執(zhí)行失敗
- 通過 echo $? 可查看終端上一進(jìn)程的 exit code
- Node 中 Promise.reject 時(shí) exit code 為 0
- Node 中可以通過 process.exitCode = 1 顯式設(shè)置 exit code
- 在 Node12+ 中可以通過 node --unhandled-rejections=strict error.js執(zhí)行腳本,視 Promise.reject 的 exit code 為 1