From 1bb38ef0f7e66a0d9cffe39c83f01d935c0b83bb Mon Sep 17 00:00:00 2001 From: baiqing Date: Sat, 9 May 2026 14:06:20 +0800 Subject: [PATCH] =?UTF-8?q?fix(recorder):=20watchdog=20=E5=9C=A8=20sleep?= =?UTF-8?q?=20=E9=86=92=E6=9D=A5=E5=90=8E=E9=87=8D=E6=A3=80=20stop=5Fflag?= =?UTF-8?q?=EF=BC=8C=E6=B6=88=E9=99=A4=E5=81=9C=E9=87=87=E8=AF=AF=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当用户松开 hotkey 进入 Listening → Processing 转移时,coordinator 调 rec.stop() 设置 stop_flag 并 pause cpal Stream。但 watchdog 线程此时 可能正卡在 1 秒的 sleep 里:sleep 结束后旧逻辑直接读 last_callback_time, 看到"已 >3 秒未更新"就触发 EngineFailed("录音回调静默停止 N 秒"), 让 coordinator 把正常停采当成引擎崩溃,胶囊弹出 "audio engine failed"。 复现日志(~/Library/Logs/OpenLess/openless.log): 27.011 [recorder] cb#4250 ... <- 最后一次回调 27.054 [asr] server JSON ... <- ASR 仍在收尾 ~27.5 end_session 调 rec.stop() 设置 stop_flag + pause cpal 31.667 [ERROR] [recorder] watchdog: 录音回调已停止 4 秒,触发错误恢复 31.667 [ERROR] [coord] recorder runtime error: audio engine failed 修复:watchdog sleep 醒来后立即再 load 一次 stop_flag,flag 已置位则 直接 break 退出,不再做 elapsed 判定。这样: - 主动停采路径(hotkey-release / cancel):watchdog 静默退出,无误报 - 真故障路径(CoreAudio 设备掉线、OS 音频复位):stop_flag 仍为 false, watchdog 照常按 3 秒静默阈值触发 EngineFailed,行为不变 附 18 行注释解释 race,避免后续重构时被无意识回退。 --- openless-all/app/src-tauri/src/recorder.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/openless-all/app/src-tauri/src/recorder.rs b/openless-all/app/src-tauri/src/recorder.rs index eb1b1d49..a599fa3f 100644 --- a/openless-all/app/src-tauri/src/recorder.rs +++ b/openless-all/app/src-tauri/src/recorder.rs @@ -192,6 +192,23 @@ fn run_audio_thread( while !stop_flag_for_watchdog.load(Ordering::SeqCst) { thread::sleep(std::time::Duration::from_millis(WATCHDOG_CHECK_INTERVAL_MS)); + // 关键:sleep 醒来后必须重新检查 stop_flag,再去看 elapsed。 + // + // 否则会与 hotkey-release 停采路径产生竞态: + // 1. 用户松开 hotkey → end_session 调 rec.stop() → 设置 stop_flag + // → audio 线程 pause 了 cpal Stream → 回调真的静默 + // 2. 但 watchdog 此时正卡在上面的 1 秒 sleep 里 + // 3. sleep 结束后,若不重新检查 stop_flag, + // 就会读到 last_callback_time 已经"老 4 秒", + // 把"我们主动停掉的录音"错报成 EngineFailed("录音回调静默停止 N 秒"), + // coordinator 收到错误后会终止 session、胶囊弹错。 + // + // 修复方式是 sleep 后立即再 load 一次:进入 stop 流程后 watchdog 静默退出, + // 不影响 watchdog 在真正活动期捕获 CoreAudio 设备掉线等真故障。 + if stop_flag_for_watchdog.load(Ordering::SeqCst) { + break; + } + let last_callback = *state_for_watchdog.last_callback_time.lock(); match last_callback { Some(last_time) => {