/** * cold-restarter.ts — Cold Restart 子进程管理器(Phase 2A) * * 当配置文件、插件、`.env`、`package.json` 或 `tsconfig.json` 变更时(Tier 3), * 执行完整的进程替换。这是必要的,因为这些文件影响全局行为 * (端口、DB URI、插件 `setup()` 副作用等),无法在进程内安全热替换。 * * 子进程生命周期: * 1. `restart(reason)` — fork 新子进程(先 safeKill 旧进程) * 2. `waitForReady()` — 等待子进程通过 IPC 发送 `{ type: 'ready' }` 消息 * 3. `sendToChild(msg)` — 向子进程发送 IPC 消息(如 soft reload 指令) * 4. `kill()` — 终止子进程(优雅退出时调用) * * safeKill 流程: * 1. 发送 SIGTERM(触发子进程的优雅关闭流程,见 06c-lifecycle.md) * 2. 等待子进程退出(最多 killTimeout ms) * 3. 超时后发送 SIGKILL 强制终止 * * 设计约束: * - 子进程入口是纯 JS(esbuild 已编译),无需 tsx/ts-node * - IPC 通道通过 fork 的 stdio 配置自动创建 * - restart 有 isRestarting guard,防止快速连续触发导致并行 restart * * @module lib/dev/cold-restarter * @see 11d-bootstrap-cli.md §1(Cold Restart 详细设计) * @see 06c-lifecycle.md §2(优雅关闭执行流程) * @see IMPLEMENTATION-PLAN.md 任务 2.4 */ /** * ColdRestarter 构造选项 */ export interface ColdRestarterOptions { /** * dev 子进程入口脚本路径(绝对路径) * * 指向 esbuild 编译后的 JS 文件(如 dev-entry.js), * 由 ColdRestarter 通过 `child_process.fork()` 执行。 */ entryScript: string; /** * safeKill 超时时间(毫秒),默认 5000ms * * 发送 SIGTERM 后等待子进程退出的最大时间。 * 超时后发送 SIGKILL 强制终止。 * * 建议值: * - 开发环境:5000ms(默认) * - 大型项目(DB 连接池较大):10000ms */ killTimeout?: number; /** * waitForReady 超时时间(毫秒),默认 30000ms * * 等待子进程发送 `{ type: 'ready' }` 消息的最大时间。 * 超时视为启动失败。 */ readyTimeout?: number; /** * 传递给子进程的环境变量(合并到 process.env 上) * * 可用于传递 VEXT_DEV_MODE / VEXT_ROOT 等标识。 */ env?: Record; /** * 子进程的工作目录(默认继承当前进程 cwd) */ cwd?: string; /** * 额外的 Node.js 运行时参数,附加到子进程 execArgv 末尾 * * 格式:["--import", "file:///abs/path/a.js", "--import", "file:///abs/path/b.js"] * * 用于注入预加载模块(如 vextjs-opentelemetry SDK 初始化文件)。 * Cold Restart 时每次 fork 自动复用,无需重新计算。 * * 由 cli/dev.ts 通过 resolvePreloads() 填充。 */ extraExecArgv?: string[]; } /** * 子进程事件监听器 * * 允许外部(如 cli/dev.ts)监听子进程的特定事件, * 例如子进程请求 cold restart(级联检测过大时)。 */ export interface ColdRestarterEvents { /** * 子进程发送的 IPC 消息 * * 用于捕获子进程的 `request-cold-restart` 等消息。 */ onChildMessage?: (msg: unknown) => void; /** * 子进程异常退出 * * code 为退出码(可能为 null),signal 为终止信号(可能为 null)。 * 仅在非预期退出时触发(restart 期间的退出不触发)。 */ onChildExit?: (code: number | null, signal: string | null) => void; } export declare class ColdRestarter { /** * 当前活跃的子进程引用 * * 初始为 null,restart() 调用后持有子进程引用。 * kill() 后重置为 null。 */ private child; /** * 重启中 guard — 防止并行 restart * * 当 restart() 正在执行时,后续的 restart() 调用直接返回。 * 这避免了用户快速连续修改多个配置文件时触发多次 fork。 */ private isRestarting; /** * 标记是否由 restart 发起的 kill(区分预期退出和异常退出) */ private isExpectedKill; private readonly entryScript; private readonly killTimeout; private readonly readyTimeout; private readonly env; private readonly cwd; private extraExecArgv; private events; constructor(options: ColdRestarterOptions); /** * 设置事件监听器 * * 允许外部模块(如 cli/dev.ts)监听子进程事件。 * 每次调用会覆盖之前的监听器。 * * @param events 事件回调集合 */ setEvents(events: ColdRestarterEvents): void; /** * 更新冷重启子进程的额外 Node.js 运行参数 * * 主要用于 dev 模式在项目级 preload 发生变化后, * 重新计算 `--import file:///...` 列表并在下一次 cold restart 中生效。 */ setExtraExecArgv(extraExecArgv: string[]): void; /** * restart — 执行 Cold Restart * * 完整流程: * 1. 检查 isRestarting guard(防并行) * 2. safeKill 旧子进程(SIGTERM → 超时 SIGKILL) * 3. fork 新子进程(纯 JS 入口,无需 tsx) * 4. 注册 IPC 消息监听 + 异常退出监听 * 5. waitForReady(等待子进程 `{ type: 'ready' }` 消息,超时 30s) * * @param reason 重启原因(用于日志输出,如 "initial start" 或文件路径) * @throws 子进程启动超时或退出非零码时抛出错误 */ restart(_reason: string): Promise; /** * 向子进程发送 IPC 消息 * * 用于向子进程传递指令,如: * - `{ type: 'reload', files: [...] }` — soft reload 指令 * - `{ type: 'shutdown' }` — 优雅关闭指令 * * 如果子进程不存在或 IPC 通道已断开,静默忽略(不抛出错误)。 * * @param msg 要发送的消息(可序列化的对象) */ sendToChild(msg: unknown): void; /** * kill — 终止子进程(用于进程退出清理) * * 在 CLI 退出时调用,确保子进程被正确清理。 * 使用与 restart 相同的 safeKill 流程(SIGTERM → 超时 SIGKILL)。 */ kill(): Promise; /** * 获取当前子进程的 PID(调试/日志用) * * @returns 子进程 PID,如果无活跃子进程返回 null */ getChildPid(): number | null; /** * 检查是否正在重启中 */ getIsRestarting(): boolean; /** * 检查子进程是否存活 */ isChildAlive(): boolean; /** * safeKill — 安全终止子进程 * * 流程: * 1. 通知子进程关闭: * - Windows: 优先通过 IPC 发送 { type: 'shutdown' } 消息(触发子进程优雅关闭) * IPC 不可用时降级为 child.kill('SIGTERM') * - Unix: 发送 SIGTERM(触发子进程 process.on('SIGTERM') 处理器) * 2. 等待子进程退出(最多 killTimeout ms) * 3. 超时后发送 SIGKILL 强制终止 * * 🐛 修复 BUG-014:Windows 上 child.kill('SIGTERM') 不会触发子进程的 * process.on('SIGTERM') 处理器——Node.js 在 Windows 上直接调用 * TerminateProcess API 杀死进程,导致 onClose hooks 不执行。 * * 使用 Promise + 双重退出监听确保在任何情况下都能 resolve: * - 正常退出:'exit' 事件触发 → resolve * - 超时强制终止:SIGKILL → 'exit' 事件 → resolve * * @param child 要终止的子进程 */ private safeKill; /** * waitForReady — 等待子进程发送 ready 消息 * * 子进程在 devBootstrap 完成初始化后发送 `{ type: 'ready' }` IPC 消息。 * 本方法等待该消息,超时则视为启动失败。 * * 异常处理: * - 超时(readyTimeout)→ reject + Error * - 子进程 error 事件 → reject + 原始 Error * - 子进程退出(非零码)→ reject + Error * * @param child 要等待的子进程 * @throws 超时或子进程异常退出时抛出错误 */ private waitForReady; /** * setupChildListeners — 为新 fork 的子进程注册持久监听器 * * 这些监听器在 waitForReady 完成后继续存活, * 用于处理运行期间的 IPC 消息和异常退出。 * * @param child 新 fork 的子进程 */ private setupChildListeners; }