test(e2e): 子进程统一登记 + 退出兜底 SIGKILL —— 修连跑偶发失败

套件有 8 处 spawn,各自 finally 只发 kill()(SIGTERM)就 process.exit(),
被控端可能来不及退出 → 下一轮固定端口仍被占用,表现为「连跑第二次偶发失败、
重跑又绿」这种最难查的形态。

做法:把 spawn 包一层统一登记(同名函数遮蔽,调用点零改动),进程退出时对
仍存活的子进程 SIGKILL。单点收口,将来新增 spawn 也不会漏。

实测:连跑两次 34/34,退出后 pgrep 无残留 agent 进程。
This commit is contained in:
jkooe committed 2026-10-27 08:04:27 +08:00
1 parent eebfb45f73
commit 5e32d7fa55
1 file changed
+29 -1
+29 -1
View File
@@ -6,7 +6,7 @@
*
* 运行:node tests/e2e/run.mjs
*/
import { spawn } from 'node:child_process';
import { spawn as rawSpawn } from 'node:child_process';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
@@ -25,6 +25,34 @@ const RawWebSocket = requireFromClient('ws');
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '../..');
/**
* 子进程登记表 + 兜底清理。
*
* 为什么需要:本套件有 8 处 spawn(主 agent / TLS / ed25519 / hub / fs / monitor…),
* 各自的 finally 只做 `child.kill()`(SIGTERM),随后立刻 process.exit —— 被控端
* 可能还没来得及退出就被父进程带走,**下一轮连跑时固定端口仍被占用**,表现为
* 「连跑第二次偶发失败」(历史真机现象,重跑又绿,最难查)。
*
* 做法:包一层 spawn,把所有子进程登记下来;进程退出时统一 SIGKILL。
* 调用点无需改动(同名函数遮蔽),单点收口也不会漏掉将来新增的 spawn。
*/
const liveChildren = new Set();
function spawn(...args) {
const child = rawSpawn(...args);
liveChildren.add(child);
child.once('exit', () => liveChildren.delete(child));
return child;
}
process.on('exit', () => {
for (const child of liveChildren) {
try {
child.kill('SIGKILL');
} catch {
/* 已退出或不可杀:忽略 */
}
}
});
const PORT = 18765;
const TLS_PORT = 18772;
const ED25519_PORT = 18781;