closes Codex audit blockers on PR #78 ## HIGH #1 — begin_session 内 await 期间 cancel 被覆盖 背景:volcengine open_session().await + Recorder::start 都是异步,期间用户按 Esc 调用 cancel_session 把 phase 改回 Idle 并 cancelled=true。但原代码后续无 条件 `state.phase = SessionPhase::Listening`,把 Idle 又翻回 Listening → 用户的 cancel 被吞掉,session 仍然继续。 修: - 新增 `cancel_raced_during_starting(inner)` helper:持锁查 cancelled or phase != Starting - ASR open_session().await 后调用:如已 race,asr.cancel() + 回 Idle - Recorder::start Ok 分支用 BeginOutcome { Started / PendingStop / CancelRaced } 原子在同一 lock 内决定,CancelRaced 时清理 recorder + asr 资源不进 Listening ## HIGH #2 — cancelled check 与 inserter.insert 之间的窗口 背景:end_session 检查 cancelled 后到调用 inserter.insert 之间释放了 lock, 此时 Esc 触发 cancel_session 设 cancelled=true 已经晚了 — Cmd+V 即将发出, 撤销不掉。但 cancel_session 仍然 emit "已取消" → UI 与实际行为相反(已插入但 显示已取消)。 修: - 新增 SessionPhase::Inserting:表示「已过最后一次 cancel 检查、即将/正在 调用 inserter.insert」的窗口 - end_session:把 polish 后的 cancel check + phase 转换 atomic 在同一 lock 内:cancelled → Idle + return;否则 → phase=Inserting 后 release lock 走 insert - cancel_session:phase==Inserting → 直接 return(不设 cancelled、不 emit 取 消)。理由:物理上无法撤销 Cmd+V,硬装"已取消"只会让 UI 撒谎 cargo check 通过,13 个 warnings 全部是 pre-existing。 Co-authored-by: baiqing <lbx12309@icloud.com>
This commit is contained in:
1 parent
d0d2da27a5
commit
bcd4d815c8
1 file changed
+91
-14
@@ -37,6 +37,11 @@ enum SessionPhase {
|
||||
Starting,
|
||||
Listening,
|
||||
Processing,
|
||||
/// 已经过了最后一次 cancel 检查、即将 / 正在调用 inserter.insert 的窗口。
|
||||
/// cancel_session 在此阶段拒绝介入:Cmd+V 模拟点击已开始或已发出,
|
||||
/// 无法撤销,硬把 cancelled=true 也救不回来,只会让 UI 出现 cancelled
|
||||
/// 但实际还是插入了的诡异状态。详见 PR 修 Codex audit HIGH #2。
|
||||
Inserting,
|
||||
}
|
||||
|
||||
enum ActiveAsr {
|
||||
@@ -364,6 +369,15 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS);
|
||||
return Err(e.to_string());
|
||||
}
|
||||
// open_session.await 期间用户可能按了 Esc / 改变心意。如果 cancel_session
|
||||
// 已触发(cancelled=true 或 phase 被改回 Idle),别再装 ASR,直接善后。
|
||||
// audit HIGH #1。
|
||||
if cancel_raced_during_starting(inner) {
|
||||
log::info!("[coord] cancel raced during ASR open_session — aborting begin");
|
||||
asr.cancel();
|
||||
inner.state.lock().phase = SessionPhase::Idle;
|
||||
return Ok(());
|
||||
}
|
||||
let c: Arc<dyn crate::recorder::AudioConsumer> = Arc::new(AsrBridge {
|
||||
asr: Arc::clone(&asr),
|
||||
});
|
||||
@@ -410,18 +424,43 @@ async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
|
||||
match Recorder::start(consumer, level_handler) {
|
||||
Ok(rec) => {
|
||||
*inner.recorder.lock() = Some(rec);
|
||||
// 转 Listening 同时检查 Starting 期间是否积累了 pending_stop 边沿。
|
||||
// hold 模式快速松开 / toggle 快速双击会到这里:握手刚完就要立即停。
|
||||
let should_stop_immediately = {
|
||||
// audit HIGH #1:转 Listening 之前在同一 lock 内检查 cancel race。
|
||||
// 之前是无条件 phase=Listening,会把 cancel_session 在 await 期间设的 Idle
|
||||
// 反向覆盖回 Listening → 用户的 cancel 边沿被吞掉。
|
||||
let outcome = {
|
||||
let mut state = inner.state.lock();
|
||||
state.phase = SessionPhase::Listening;
|
||||
std::mem::replace(&mut state.pending_stop, false)
|
||||
if state.cancelled || state.phase != SessionPhase::Starting {
|
||||
BeginOutcome::CancelRaced
|
||||
} else {
|
||||
state.phase = SessionPhase::Listening;
|
||||
let pending = std::mem::replace(&mut state.pending_stop, false);
|
||||
if pending {
|
||||
BeginOutcome::PendingStop
|
||||
} else {
|
||||
BeginOutcome::Started
|
||||
}
|
||||
}
|
||||
};
|
||||
log::info!("[coord] session started (asr={})", active_asr);
|
||||
if should_stop_immediately {
|
||||
log::info!("[coord] applying pending_stop edge → end_session immediately");
|
||||
let _ = end_session(inner).await;
|
||||
match outcome {
|
||||
BeginOutcome::CancelRaced => {
|
||||
log::info!("[coord] cancel raced during recorder start — aborting begin");
|
||||
rec.stop();
|
||||
if let Some(asr) = inner.asr.lock().take() {
|
||||
match asr {
|
||||
ActiveAsr::Volcengine(v) => v.cancel(),
|
||||
ActiveAsr::Whisper(w) => w.cancel(),
|
||||
}
|
||||
}
|
||||
inner.state.lock().phase = SessionPhase::Idle;
|
||||
}
|
||||
BeginOutcome::Started | BeginOutcome::PendingStop => {
|
||||
*inner.recorder.lock() = Some(rec);
|
||||
log::info!("[coord] session started (asr={})", active_asr);
|
||||
if matches!(outcome, BeginOutcome::PendingStop) {
|
||||
log::info!("[coord] applying pending_stop edge → end_session immediately");
|
||||
let _ = end_session(inner).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -530,10 +569,22 @@ async fn end_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
let hotword_strs = enabled_phrases(inner);
|
||||
let polished = polish_or_passthrough(&raw, mode, &hotword_strs).await;
|
||||
|
||||
// Polish 完成后再 check 一次:即使 polish 已经返回,只要还没插入,仍可丢弃。
|
||||
if inner.state.lock().cancelled {
|
||||
log::info!("[coord] cancel detected after polish — discarding output (chars={})", polished.chars().count());
|
||||
inner.state.lock().phase = SessionPhase::Idle;
|
||||
// 原子化最后一次 cancel 检查 + 转 Inserting:
|
||||
// 在同一 lock 内决定「丢弃」还是「进入 Inserting」。一旦设到 Inserting,
|
||||
// cancel_session 就拒绝介入(Cmd+V 已发出,撤销不掉)。这是 audit HIGH #2 的修复,
|
||||
// 之前 check 与 inserter.insert 之间有窗口期。
|
||||
let proceed_to_insert = {
|
||||
let mut state = inner.state.lock();
|
||||
if state.cancelled {
|
||||
state.phase = SessionPhase::Idle;
|
||||
false
|
||||
} else {
|
||||
state.phase = SessionPhase::Inserting;
|
||||
true
|
||||
}
|
||||
};
|
||||
if !proceed_to_insert {
|
||||
log::info!("[coord] cancel detected before insert — discarding output (chars={})", polished.chars().count());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -606,6 +657,13 @@ fn cancel_session(inner: &Arc<Inner>) {
|
||||
if phase == SessionPhase::Idle {
|
||||
return;
|
||||
}
|
||||
// Inserting 阶段已经过了最后一次 cancel 检查 + 锁内转换,inserter.insert 即将
|
||||
// 或正在执行 → Cmd+V 已发出无法撤销。这里硬设 cancelled=true 只会让 UI 显示
|
||||
// "已取消" 但文本仍被插入,与用户预期相反。直接拒绝,让本次 session 走完。
|
||||
if phase == SessionPhase::Inserting {
|
||||
log::info!("[coord] cancel ignored — already in Inserting phase, can't undo paste");
|
||||
return;
|
||||
}
|
||||
// Processing 阶段 cancel 不能直接干掉 in-flight polish task(已经 await 了),
|
||||
// 但可以打 cancelled 标记,让 end_session 在插入前检查并丢弃结果。
|
||||
inner.state.lock().cancelled = true;
|
||||
@@ -786,6 +844,25 @@ fn enabled_phrases(inner: &Arc<Inner>) -> Vec<String> {
|
||||
/// 用户点 ✕ / ✓ / 中途出错 / 按 Esc 都走这里,统一 2 秒。
|
||||
const CAPSULE_AUTO_HIDE_DELAY_MS: u64 = 2000;
|
||||
|
||||
/// begin_session 中各 await 之间的 cancel race 检查结果。
|
||||
enum BeginOutcome {
|
||||
/// 正常进入 Listening。
|
||||
Started,
|
||||
/// Starting 阶段积累了 pending_stop 边沿,应立即 end_session(hold 快速松开 / toggle 快速双击)。
|
||||
PendingStop,
|
||||
/// 期间 cancel_session 触发(cancelled=true 或 phase 被外部改回 Idle)。
|
||||
/// 必须回滚 recorder + ASR 资源,不进 Listening。
|
||||
CancelRaced,
|
||||
}
|
||||
|
||||
/// 检查 begin_session 的 await 间隙是否被 cancel_session 打断。
|
||||
/// 必须在持有 state lock 的瞬间读,结果一拿就过期,所以用 helper 名字提醒只在
|
||||
/// 「准备做下一步副作用前」用。
|
||||
fn cancel_raced_during_starting(inner: &Arc<Inner>) -> bool {
|
||||
let state = inner.state.lock();
|
||||
state.cancelled || state.phase != SessionPhase::Starting
|
||||
}
|
||||
|
||||
fn schedule_capsule_idle(inner: &Arc<Inner>, delay_ms: u64) {
|
||||
let inner_clone = Arc::clone(inner);
|
||||
async_runtime::spawn(async move {
|
||||
|
||||
Reference in new issue
Block a user