如何在Bash脚本中捕获npm test子进程PID,解决Ctrl-C残留node进程问题
问题:Ctrl-C终止包装npm test的脚本后残留Node进程,如何正确清理?
我想写一个辅助脚本,执行这条基于npm test的包装命令:
unbuffer npm test 2>&1 | tee >(sed -r "s/\x1B\[[0-9;]*[a-zA-Z]//g" > ./tmp/output.log)
单独跑npm test时,Ctrl-C能彻底终止npm test及其衍生的Node进程,但用上面的命令后,Ctrl-C退出脚本会残留后台Node进程,占着端口导致无法重新执行。
我试过用PID=$!捕获后台进程PID,再通过trap 'kill $PID' SIGINT在脚本退出时终止进程,但只抓到了tee的PID,还是会留下孤儿Node进程。
补充:我的package.json里的scripts部分如下:
"scripts": { "start": "node ./node/lib/ppp.js && node --import ./instrument.mjs --no-warnings=ExperimentalWarning myapp.js", "test": "node ./node/lib/ppp.js && export $(cat .env | grep \"^[^#;]\" | xargs) && node --import ./instrument.mjs --no-warnings=ExperimentalWarning myapp.js" },
解决方案
1. 用进程组统一管理所有子进程
Bash中可以把整个管道命令放入同一个进程组,这样Ctrl-C发送的SIGINT信号会传递给组内所有进程,避免残留。修改脚本如下:
#!/bin/bash # 确保临时目录存在 mkdir -p ./tmp # 开启作业控制,将管道放入后台进程组 set -m unbuffer npm test 2>&1 | tee >(sed -r "s/\x1B\[[0-9;]*[a-zA-Z]//g" > ./tmp/output.log) & GROUP_PID=$! set +m # 捕获SIGINT,向整个进程组发送终止信号 trap 'kill -TERM -$GROUP_PID' SIGINT # 等待进程组完成 wait $GROUP_PID
kill -TERM -$GROUP_PID中的负号表示向整个进程组发送信号,能覆盖npm test衍生的所有Node进程。
2. 兜底:直接清理关联Node进程
如果进程组方案仍有残留,可以在脚本退出时,根据进程特征精准清理:
#!/bin/bash mkdir -p ./tmp # 定义清理函数 cleanup() { # 终止所有运行myapp.js的Node进程 pkill -f "myapp.js" # 可选:如果知道占用的端口,比如3000,也可以用端口清理 # lsof -ti:3000 | xargs kill -TERM 2>/dev/null } # 绑定清理逻辑到SIGINT(Ctrl-C)和脚本退出事件 trap cleanup SIGINT EXIT # 执行包装命令 unbuffer npm test 2>&1 | tee >(sed -r "s/\x1B\[[0-9;]*[a-zA-Z]//g" > ./tmp/output.log)
3. 优化npm test脚本(从根源减少残留)
你的npm test是串行执行两个Node进程,第一个ppp.js执行完才启动myapp.js。可以用exec替换shell进程,让Node进程直接继承父进程的信号处理:
修改package.json的test脚本:
"scripts": { "start": "exec node ./node/lib/ppp.js && exec node --import ./instrument.mjs --no-warnings=ExperimentalWarning myapp.js", "test": "exec node ./node/lib/ppp.js && export $(cat .env | grep \"^[^#;]\" | xargs) && exec node --import ./instrument.mjs --no-warnings=ExperimentalWarning myapp.js" }
exec会让Node进程替换当前的shell进程,信号能更直接地传递,减少孤儿进程产生的概率。
内容的提问来源于stack exchange,提问作者drmrbrewer
相关产品推荐
相关产品推荐

