fix(coordinator): wrap audio-mute shellouts in spawn_blocking [中]#391
Conversation
AudioMuteGuard::activate() shells out to osascript on macOS (~100–300 ms AppleScript runtime startup) and to wpctl/pactl on Linux (similar cost). Both implementations execute synchronously through std::process::Command. acquire_recording_mute and release_recording_mute were called from the async begin_session path, so the tokio worker thread was blocked for the full shellout duration on every dictation start and stop. Fix: - Make acquire_recording_mute async; wrap the existing Mutex/holders/activate body in tokio::task::spawn_blocking. The parking_lot Mutex never crosses an await — it lives entirely inside the blocking task. Caller (start_recorder_for_starting) awaits. - release_recording_mute stays a sync entry point (some call sites are on plain std::thread workers, e.g. the recorder error monitor) but delegates to spawn_blocking when a tokio Handle::try_current is available. Falls back to running synchronously when there's no tokio context — that case is already on a non-async OS thread, so blocking there blocks nothing. - start_recorder_for_starting becomes async to thread the acquire_recording_mute().await up. Three call sites (Bailian, generic Volcengine, the start_recorder_and_enter_listening helper) get matching .await? sequels. Net effect: dictation-start latency on macOS no longer absorbs the ~100–300 ms osascript startup before Recorder::start can run; the work runs on a tokio blocking pool thread instead. Same on Linux. Windows path uses native COM (IAudioEndpointVolume::SetMute), already fast — spawn_blocking is a small extra hop but harmless. Audit ID 3.2.4 (CONFIRMED 中). Audit IDs 3.2.3 (sync inserter.insert) and 3.2.5 (Linux-only 120 ms thread::sleep in permission probe) deferred to follow-up — both are smaller-impact perf concerns and don't share code with the audio-mute path. Test: 183/183 lib tests pass. Manual verification (start dictation, measure time-to-first-mic-capture before/after) requires the running app and is expected to drop ~150 ms on macOS.
PR Reviewer Guide 🔍(Review updated until commit 1889b79)Here are some key observations to aid the review process:
|
|
@claude opus 4.7 请审核这个 PR,确保它和前文的 PR 对应,且没有其他问题。 |
|
Claude finished @appergb's task in 2m 8s —— View job PR #391 代码审核
总体评估代码改动与 PR 描述完全对应,逻辑正确,Audit 3.2.4 的目标(解除 tokio worker 在 shellout 上的阻塞)已实现。以下是具体发现: 问题一:
|
Addresses pr_agent "Possible Issue" review on PR #391. The original `let _ = tokio::task::spawn_blocking(...).await` silently swallowed JoinError, which means a panic inside AudioMuteGuard::activate() (or the parking_lot lock) would leave the user with system audio leaking during recording — and zero [audio-mute] log to explain why. Match the JoinResult and log::error! the panic. State (holders/guard) stays consistent under panic — activate panic means no guard stored and holders not incremented; lock panic can't reach the mutate. The fix is purely about observability.
pr_agent "Possible Issue" 已修1889b79:把 State 一致性补充说明:原本 panic 路径下 holders/guard 也是一致的(activate 抛 → guard 没存 / holders 没增;lock 抛 → 进不到 mutate);这次修的是可观察性 —— 用户碰到「录音时系统声漏出」找不到任何 |
|
Persistent review updated to latest commit 1889b79 |
Two real bugs surfaced by the 2026-05-10 end-to-end logic review of beta after the 8-PR audit-fix wave landed. 1. coordinator.rs:2313 — `acquire_recording_mute(inner, "qa");` was missing `.await`. PR Open-Less#391 (`6171df61`) made the function `async fn` and updated the dictation call site at coordinator/dictation.rs:451, but the QA call site was missed. Effect: when a user has `mute_during_recording = true` and triggers QA via Right Option, the returned Future is dropped on the next line, `spawn_blocking` is never scheduled, holders never increments, and system audio is NOT muted (e.g. YouTube playback continues into the QA recording). Both the matching `release_recording_mute(inner, "qa")` calls become no-ops (early return at holders == 0). The compiler was emitting `unused_must_use` for this site (verified before this commit). Fix: add `.await`. 2. coordinator/dictation.rs:843-849 — PR Open-Less#387 (`ce82fcd9`) was framed as "clear focus_target on cancel regardless of phase", but the only code path that gained the unconditional clear was `finish_cancel_session_state`. cancel_session deliberately skips that helper for Processing (so end_session can drive its own teardown), and end_session's "ASR-finished, cancelled" exit at dictation.rs:843-849 set phase=Idle but never touched focus_target. Result: cancel-during-Processing leaves a stale `usize` focus slot in state.focus_target until the next begin_session_state overwrites it. Today the leak is bounded (no documented reader between cancel and next begin), but it violates PR Open-Less#387's stated contract and is a silent footgun for any future reader of focus_target on that interval. Fix: in the cancelled-after-ASR exit branch, take the state lock once and clear both phase and focus_target together. Source: docs/logic-review-2026-05-10.md (subagent end-to-end review). 185/185 lib tests pass; cargo check clean (unused_must_use gone). Manual verification checklist for both fixes in the review doc.
10 PRs landed on beta this cycle: - Open-Less#377 paste shortcut configurable (issue Open-Less#360) - Open-Less#386 TS UserPreferences updateChannel alignment - Open-Less#387 focus_target leak on Processing-phase cancel - Open-Less#388 [严重] MacHotkeyAdapter::shutdown stops CFRunLoop + tap - Open-Less#389 emit_capsule window.show/hide off audio thread - Open-Less#390 QA / dictation hotkey routing race - Open-Less#391 audio-mute spawn_blocking (async hygiene) - Open-Less#392 hotkey supervisor + global dispatcher exit signal - Open-Less#393 post-audit logic-review hotfixes (QA mute .await + focus_target Processing branch) - Open-Less#394 in-process credentials cache (kills repeated Keychain prompts) Bump 4 files: package.json, tauri.conf.json, Cargo.toml, Cargo.lock.
User description
Summary
`AudioMuteGuard::activate()` shells out to `osascript` on macOS (~100–300 ms AppleScript runtime startup) and to `wpctl`/`pactl` on Linux (similar). Both go through synchronous `std::process::Command`. `acquire_recording_mute` was called inline from the async `begin_session` path, so the tokio worker thread was blocked on the shellout for every dictation start. Net effect: every macOS dictation absorbed ~150 ms of osascript startup before `Recorder::start` could run.
Change
Why one PR
Originally scoped as "async hygiene" covering 3.2.3 (sync inserter), 3.2.4 (audio-mute), and 3.2.5 (Linux probe sleep). Trimmed to just 3.2.4 because:
Audit linkage
Audit ID 3.2.4 (CONFIRMED 中). 3.2.3 + 3.2.5 tracked separately. See `docs/audit-2026-05-10-validated.md` (local).
Test plan
PR Type
Bug fix, Enhancement
Description
Wrap audio-mute shell-outs in
spawn_blockingto avoid blocking tokio worker threads.Convert
start_recorder_for_startingto async to integrate with new async mute.Update three call sites in
begin_sessionwith.await?for async recorder start.Adapt
release_recording_muteto offload mute restoration when a tokio handle is present.Diagram Walkthrough
flowchart LR A["begin_session (async)"] -- ".await" --> B["start_recorder_for_starting (async)"] B -- ".await" --> C["acquire_recording_mute (async)"] C -- "spawn_blocking" --> D["AudioMuteGuard::activate() (osascript / wpctl)"] E["release_recording_mute (sync)"] -- "if tokio handle" --> F["spawn_blocking (drop guard)"] E -- "no handle" --> G["synchronous drop"]File Walkthrough
dictation.rs
Asyncify dictation recording start to integrate non-blocking muteopenless-all/app/src-tauri/src/coordinator/dictation.rs
start_recorder_for_startingan async function.acquire_recording_muteinsidestart_recorder_for_starting..await?tostart_recorder_for_startingcalls inbegin_sessionforBailian and Volcengine paths, and in
start_recorder_and_enter_listening.resources.rs
Offload audio mute shell-outs to blocking thread poolopenless-all/app/src-tauri/src/coordinator/resources.rs
acquire_recording_mutemoved to async and wrapsAudioMuteGuard::activate()intokio::task::spawn_blocking.spawn_blockingpanic (instead ofsilently dropping).
release_recording_muteremains sync but delegates tospawn_blockingifa tokio runtime handle is available; otherwise runs inline for
non‑tokio threads.
safety.