在Node相关的项目中,总是需要运行脚本。运行脚本来拉取配置、处理一些数据和安排任务更是司空见惯。在一些重要流程中可以看到脚本:CI,用于测试、质量保证、部署等Docker,用于构建镜像Cron,用于定时任务如果脚本错误未能检测到这些重要流程中的问题,就会出现可能会出现更隐蔽的问题。最近观察项目镜像构建,偶尔发现一两个镜像虽然构建成功,但是容器无法运行。“原因是因为ExitCode的问题。”退出代码什么是退出代码?exitcode表示进程的返回码,由系统调用exit_group触发。在POSIX中,0代表正常返回码,1-255代表异常返回码,一般主动抛出的错误码都是1。在Node应用中使用process.exitCode=1表示意外异常中止。这是关于异常代码的表格附录E.具有特殊含义的退出代码[1]。异常代码在操作系统中随处可见,下面是cat命令的异常及其退出代码,使用strace跟踪系统调用。$catacat:a:Nosuchfileordirectory#使用strace查看cat的系统调用#-e只显示write和exit_group的系统调用$strace-ewrite,exit_groupcatawrite(2,"cat:",5cat:)=5write(2,"a",1a)=1write(2,":Nosuchfileordirectory",27:Nosuchfileordirectory)=27write(2,"\n",1)=1exit_group(1)=?+++exitedwith1++++从最后可以看出系统调用行,执行的exitcode为1,错误信息输出到stderr(标准错误的fd为2)。如何查看退出码?可以通过strace来判断进程的退出码,但不方便且冗余,尤其是在编程环境的shell中。“有一种简单的方法可以通过echo$确认返回码吗?”$catacat:a:Nosuchfileordirectory$echo$?1thrownewError和Promise.reject的区别下面是两段代码,第一段抛出异常,第二段Promise.reject,两段代码都会打印一段异常信息如下,那么有什么区别他们俩?functionerror(){thrownewError('hello,error')}error()//输出:///Users/shanyue/Documents/note/demo.js:2//thrownewError('hello,world')//^////Error:hello,world//aterror(/Users/shanyue/Documents/note/demo.js:2:9)//atObject.(/Users/shanyue/Documents/note/demo.js:5:1)//atModule._compile(internal/modules/cjs/loader.js:701:30)functionerror(){thrownewError('hello,error')}error()//Output:///Users/shanyue/Documents/note/demo.js:2//thrownewError('hello,world')//^////Error:hello,world//aterror(/Users/shanyue/Documents/note/demo.js:2:9)//atObject.(/Users/shanyue/Documents/note/demo.js:5:1)//atModule._compile(internal/modules/cjs/loader.js:701:30)使用echo$?查看上面两个测试用例的退出码,我们会发现thrownewError()的退出码是1,而Promise.reject()是0。“从操作系统的角度来说,exitcode为0表示进程已经成功运行退出,此时即使有Promise.reject,操作系统也会认为是执行成功。”这将在Dockerfile和CI中留下安全风险。node中Dockerfile的注意事项使用Dockerfile构建镜像时,如果RUN过程返回非零返回码,会导致构建失败。“在Node中的错误处理中,我们倾向于将所有的异常都交给async/await处理,当出现异常时,镜像构建不会因为此时exitcode为0而失败。”这是Promise.reject()问题的简单明了的镜像。FROMnode:12-alpineRUNnode-e"Promise.reject('hello,world')"构建镜像的过程如下:"即使在构建过程中打印了unhandledPromiseRejection消息,仍然构建成功。“$dockerbuild-tdemo.SendingbuildcontexttoDockerdaemon33.28kBStep1/2:FROMnode:12-alpine--->18f4bc975732Step2/2:RUNnode-e"Promise.reject('hello,world')"--->Runningin79a6d53c5aa6(node:1)UnhandledPromiseRejectionWarning:hello,world(node:1)UnhandledPromiseRejectionWarning:UnhandledPromiseRejection。这个错误起源于通过在不带catch块的同步函数内部抛出,或者通过拒绝未使用.catch()处理的promise。要在未处理的promiserejection上终止节点进程,使用CLIflag`--unhandled.node.cli`shtml://apcli/js/seehttpijstrictcli_unhandled_rejections_mode).(rejectionid:1)(node:1)[DEP0018]DeprecationWarning:Unhandledpromiserejectionsaredeprecated.Inthefuture,promiserejectionsthatarenothandledwillterminatetheNode.jsprocesswithanon-zeroexitcode.Removingintermediatecontainer79a6d53c5aa6--->09f07eb993feSuccessfullybuilt09f07eb993feSuccessfullytaggeddemo:latestPromise.reject脚本解决方案能在编译时能发现的问题,绝不要把它放在运行时。因此,在构建镜像或CI时需要执行node脚本时,需要手动指定process.exitCode=1进行异常处理,提前暴露问题runScript().catch(()=>{process.exitCode=1})在构建镜像时,还有一个关于异常解决的建议:(node:1)UnhandledPromiseRejectionWarning:Unhandledpromiserejection。这个错误要么是在没有catch块的情况下在异步函数中抛出,要么是因为拒绝了一个没有用.catch()处理的承诺。要在未处理的承诺拒绝时终止节点进程,请使用CLI标志--unhandled-rejections=strict(请参阅https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode)。(rejectionid:1)根据提示,--unhandled-rejections=strict会将Promise.reject的exitcode设置为1,并在以后的node版本中修复Promiseexceptionexitcodes。$node--unhandled-rejections=strictterror.js--unhandled-rejections=strict配置对node有版本要求:添加于:v12.0.0,v10.17.0默认情况下所有未处理的拒绝都会触发警告加上弃用警告如果没有使用unhandledRejection钩子,则第一次未处理的拒绝。综上所述,当进程ends的退出码不为0时,系统会认为进程执行失败。通过echo$?可以在终端查看上一个进程的exitcodeNode中的Promise.rejectexitcode为0。在Node中可以通过process.exitCode=1显式设置exitcode。在Node12+中,可以通过node--unhandled-rejections=stricterror.js执行脚本,Promise.reject的退出码为1。