fix: 修正合成超时分支遗漏杀死已 spawn 播放器导致的残留泄漏

真机用 kill -STOP 验证完 v9.0.2 的看门狗修复后,拿同样的批量测试
场景复测,发现 aplay 残留还在发生:25 个存活进程,10 秒内还在
25→27 继续增长,且意外出现了第二个以 root 身份启动的 pulseaudio
实例(可能是前几轮调试过程中触发的系统自动拉起,已清理,和这次
代码修复无直接关系)。

排查后发现 v9.0.2 的修复漏了一条泄漏路径:speakWithVits 里"合成
卡死"分支(60 秒没有新分段到达)只杀了 vitsWorker,从没杀过已经
spawn 出来、stdin 还开着干等数据的 currentPlayer——这个 aplay 会
永远卡在等一个再也不会来的 EOF,跟 PLAYER_EXIT_TIMEOUT_MS 要解决
的是同一类问题,只是触发路径不同,上一版没覆盖到。

修复:不再满足于在每个 done() 调用点各自记得去杀 currentPlayer,
而是在 cleanup() 这个所有路径共用的收尾出口统一兜底——无论从哪条
分支走到这里,都先用 killWithEscalation() 尝试杀掉还活着的
currentProcess/currentPlayer,再清空引用。正常退出路径下进程已经
退出,再次 kill() 是安全的 no-op,不影响现有行为。

验证:清理现场后用同样的批量测试场景(12 道题连续生成+播报)跑
3 分钟、每 10 秒采样一次 aplay 存活数,全程稳定在 0~1,没有任何
堆积趋势(对比修复前 10 秒内 25→27 的失控状态)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QeoAy5KmkHuKWHAktpEsaN
This commit is contained in:
ssurui 2026-10-06 11:15:45 +08:00
parent 95ee66e836
commit 1e84d21702

View File

@ -1190,7 +1190,23 @@ function speakWithPiper(
}) })
} }
/**
* 每轮播报结束时统一调用。之前只是把 currentProcess/currentPlayer 引用置空,
* 没有保证对应的系统进程真的退出——真机复测时发现合成卡死分支(见上面
* "synth timeout" 处)只杀了 vitsWorker,没杀已经 spawn 出来、stdin 还开着
* 干等数据的 aplay,这个进程会永远卡在那等一个再也不会来的 EOF,形成和
* PLAYER_EXIT_TIMEOUT_MS 要解决的问题同一性质的泄漏,只是触发路径不同。
* 与其在每个 done() 调用点各自记得去杀 currentPlayer,不如在 cleanup()
* 这个唯一的收尾出口统一兜底:无论从哪条路径走到这里,都先尝试杀掉还活着
* 的进程,再清空引用。
*/
function cleanup(): void { function cleanup(): void {
if (currentProcess) {
killWithEscalation(currentProcess)
}
if (currentPlayer) {
killWithEscalation(currentPlayer)
}
currentProcess = null currentProcess = null
currentPlayer = null currentPlayer = null
isSpeaking = false isSpeaking = false