Node 腳本遭遇異常時如何安全退出

一個 Node 相關的項目中,老是少不了跑腳本。跑一個腳本拉取配置、處理一些數據以及定時任務更是屢見不鮮。html

在一些重要流程中可以看到腳本的身影:node

  1. CI,用以測試、質量保障及部署等
  2. Docker,用以構建鏡像
  3. 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 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.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.

總結

  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

Reference

[1]

Appendix E. Exit Codes With Special Meanings: http://www.tldp.org/LDP/abs/html/exitcodes.html


本文分享自微信公衆號 - 全棧成長之路(shanyue-road)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索