From 0d2164e1afbbc69a2a385df5778f3f2eacd36632 Mon Sep 17 00:00:00 2001 From: ssurui Date: Tue, 6 Oct 2026 11:15:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E5=90=88=E6=88=90?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E5=88=86=E6=94=AF=E9=81=97=E6=BC=8F=E6=9D=80?= =?UTF-8?q?=E6=AD=BB=E5=B7=B2=20spawn=20=E6=92=AD=E6=94=BE=E5=99=A8?= =?UTF-8?q?=E5=AF=BC=E8=87=B4=E7=9A=84=E6=AE=8B=E7=95=99=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真机用 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 Claude-Session: https://claude.ai/code/session_01QeoAy5KmkHuKWHAktpEsaN --- src/main/tts/systemTts.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/tts/systemTts.ts b/src/main/tts/systemTts.ts index 98e44c6..a09b59f 100644 --- a/src/main/tts/systemTts.ts +++ b/src/main/tts/systemTts.ts @@ -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 { + if (currentProcess) { + killWithEscalation(currentProcess) + } + if (currentPlayer) { + killWithEscalation(currentPlayer) + } currentProcess = null currentPlayer = null isSpeaking = false