进程、信号与优雅关机
专栏:TypeScript 与 Node 地基 · 第 10 / 13 篇:::info 学习目标 完成本篇后你能够:解释 SIGTERM/SIGINT 与退出码的语义;实现一个带超时的优雅关机;说明为什么 CI 场景必须正确处理退出码。 前置:第 8 篇完成。预计时长:45 分钟。 :::
第 2 篇(dsh 专栏)提过:SIGTERM → 优雅退出(码 0),SIGINT → 130,第二次信号强制走。这一篇把它的原理与最小实现写出来。
信号:操作系统发给进程的”通知”
| 信号 | 谁发的 | 含义 | Node 里的事件 |
|---|---|---|---|
| SIGINT | Ctrl+C | 用户要求中断 | process.on('SIGINT') |
| SIGTERM | kill / 容器停止 | 请你正常退出 | process.on('SIGTERM') |
| SIGKILL | kill -9 | 无条件杀死(无法捕获) | — |
关键认知:信号是”请求”不是”命令”(SIGKILL 除外)。收到 SIGTERM 的进程有机会做完手头的事再退出——这就是”优雅关机”的窗口。
退出码:脚本世界的对话语言
process.exit(n) 的 n 就是退出码,惯例:0 = 成功,非 0 = 失败(具体码自定)。CI 和脚本靠它判断成败:agent-run "任务" 返回 1,CI 就标红。dsh 的约定(SIGTERM→0、SIGINT→130)与 Unix 传统一致。
优雅关机的四步
最小实现:带超时的优雅关机
最小实现:带超时的优雅关机
// graceful.ts —— 可复用的优雅关机
export function setupGracefulShutdown(server: { close(cb: () => void): void }) {
let shuttingDown = false
const pending = new Set<Function>()
function track<T>(p: Promise<T>): Promise<T> { // 登记进行中的任务
pending.add(() => {})
p.finally(() => pending.delete(() => {}))
return p
}
async function shutdown(code: number) {
if (shuttingDown) process.exit(1) // 第二次信号:强制走
shuttingDown = true
console.log('正在优雅关机…')
server.close(() => { // 停止接新请求
console.log('已退出'); process.exit(code)
})
setTimeout(() => process.exit(code), 5000).unref() // 5 秒超时兜底
}
process.on('SIGTERM', () => shutdown(0))
process.on('SIGINT', () => shutdown(130))
return { track, shutdown }
}
流程四步:停新请求 → 等在途任务(带超时兜底)→ 清理资源 → 退出。unref() 让超时定时器不阻止进程退出。
与其他主题的联系
- dsh 的
runProfile(第 2 篇时序)注册的就是这套逻辑:首次信号开始优雅 dispose(上限 5 秒)、第二次强制退出;dispose 的内容是 Cordis 插件树的级联卸载(第 4 篇); - headless 模式(第 13 篇)的退出码直接取自最终轮次的原因枚举——completed→0、aborted/error→1,CI 与脚本据此判断成败;
- new-api 的多机部署(new-api 专栏第 10 篇)同样依赖优雅关机把看板缓存落库(
SaveQuotaDataCache)。
常见踩坑
- 进程不退出——还有活跃的定时器/连接没清理(
unref()或显式 clear); - 关机时限内干不完——超时兜底的时长要按最长的在途任务估算(dsh 给 SSE 留 120 秒);
- 第二次信号没处理——卡死的进程只能
kill -9,在途数据全丢; - CI 里退出码永远是 0——脚本吞了错误码或没把失败映射到非零退出。
随堂练习(带验收标准)
- 跑通上面的服务,
Ctrl+C观察日志顺序(停止接新 → 退出)。验收:退出码正确(echo $?显示 130); - 发一个慢请求(10 秒),期间
kill <pid>。验收:请求完成后进程才退出;再发一次kill -9对比; - 读
codex-rs或 dsh 里处理信号的那段源码(dsh 在apps/cli/src/process-shutdown.ts),对照你的实现找差距。