73 lines
2.8 KiB
JavaScript
73 lines
2.8 KiB
JavaScript
// scripts/shutdown.cjs
|
|
//
|
|
// audit r40 (2026-10-15):抽出 start.cjs 的子进程树清理逻辑,便于 vitest
|
|
// 单元测试(start.cjs 自身是顶层脚本,直接 require 会副作用跑整个 dev
|
|
// 链路,不便测)。
|
|
//
|
|
// 设计目标:
|
|
// - 输入 child (Node ChildProcess-like,需要 .pid 字段) + 可选 signal
|
|
// - Windows 走 taskkill /F /T /PID;POSIX 走 process.kill(-pid, ...)
|
|
// - 1500ms 后 SIGKILL 兜底
|
|
// - 任意抛错 swallow(启动期容错,本 helper 不该再抛出)
|
|
//
|
|
// 注意:Windows 上 Node 不支持 process.kill(-pid) 的 process group 语义,
|
|
// POSIX 分支仅在非 win32 跑;Windows 必须走 taskkill,负 PID 在 win32 上
|
|
// 会 ESRCH,这是已知差异。
|
|
|
|
const { spawnSync } = require('child_process');
|
|
|
|
/**
|
|
* 递归杀进程树。
|
|
* @param {{ pid?: number }} child : 需要 .pid;可无(空对象 → no-op)
|
|
* @param {string} [signalName] : POSIX 下首次发的信号;默认 'SIGTERM'
|
|
* @param {object} [opts]
|
|
* @param {string} [opts.platform] : 默认 process.platform;测时 mock
|
|
* @param {(pid: number) => unknown} [opts.posixGroupKill] : POSIX 杀进程的 helper;默认 process.kill
|
|
* @param {() => void} [opts.posixEscalate] : 1500ms 升级到 SIGKILL 的 schedule;默认 setTimeout
|
|
* @returns {{ killed: boolean, method: 'taskkill'|'posix'|'noop' }}
|
|
*/
|
|
function shutdownTree(child, signalName, opts) {
|
|
const pid = child && child.pid;
|
|
// PID 0 是 kernel idle process,不可能是 user-space child;统一视为 noop
|
|
if (typeof pid !== 'number' || !Number.isFinite(pid) || pid <= 0) {
|
|
return { killed: false, method: 'noop' };
|
|
}
|
|
|
|
const platform = (opts && opts.platform) || process.platform;
|
|
const posixGroupKill = (opts && opts.posixGroupKill) || ((p) => process.kill(p));
|
|
const posixEscalate = (opts && opts.posixEscalate) || ((fn) => setTimeout(fn, 1500));
|
|
|
|
if (platform === 'win32') {
|
|
try {
|
|
// windowsHide: 子进程在父 console handle 内隐藏,不弹黑色 cmd 窗口
|
|
spawnSync('taskkill', ['/F', '/T', '/PID', String(pid)], {
|
|
stdio: 'ignore',
|
|
windowsHide: true,
|
|
});
|
|
return { killed: true, method: 'taskkill' };
|
|
} catch {
|
|
// taskkill 自身失败 —— child 大概率已死,fallthrough
|
|
return { killed: false, method: 'taskkill' };
|
|
}
|
|
}
|
|
|
|
// POSIX:负 PID = process group(shell:true 时 child.pid 是 cmd 的 PID,
|
|
// cmd 自己起的 electron + vite 子进程都在同一 group)
|
|
try {
|
|
posixGroupKill(-pid, signalName || 'SIGTERM');
|
|
posixEscalate(() => {
|
|
try {
|
|
posixGroupKill(-pid, 'SIGKILL');
|
|
} catch {
|
|
// already exited
|
|
}
|
|
});
|
|
return { killed: true, method: 'posix' };
|
|
} catch {
|
|
// ESRCH = group 已不存在
|
|
return { killed: false, method: 'posix' };
|
|
}
|
|
}
|
|
|
|
module.exports = { shutdownTree };
|