Merge pull request #391 from appergb/fix/audit-async-hygiene
fix(coordinator): wrap audio-mute shellouts in spawn_blocking [中]
This commit is contained in:
2 files changed
+72
-28
No files matched your search
@@ -235,7 +235,7 @@ pub(super) async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
current_session_id,
|
||||
ActiveAsr::Bailian(Arc::clone(&asr)),
|
||||
);
|
||||
start_recorder_for_starting(inner, current_session_id, &active_asr, consumer)?;
|
||||
start_recorder_for_starting(inner, current_session_id, &active_asr, consumer).await?;
|
||||
|
||||
if let Err(e) = asr.open_session().await {
|
||||
log::error!("[coord] open Bailian ASR session failed: {e}");
|
||||
@@ -334,7 +334,7 @@ pub(super) async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
current_session_id,
|
||||
ActiveAsr::Volcengine(Arc::clone(&asr)),
|
||||
);
|
||||
start_recorder_for_starting(inner, current_session_id, &active_asr, consumer)?;
|
||||
start_recorder_for_starting(inner, current_session_id, &active_asr, consumer).await?;
|
||||
|
||||
if let Err(e) = asr.open_session().await {
|
||||
log::error!("[coord] open ASR session failed: {e}");
|
||||
@@ -403,7 +403,7 @@ pub(super) async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn start_recorder_for_starting(
|
||||
pub(super) async fn start_recorder_for_starting(
|
||||
inner: &Arc<Inner>,
|
||||
session_id: SessionId,
|
||||
active_asr: &str,
|
||||
@@ -448,7 +448,7 @@ pub(super) fn start_recorder_for_starting(
|
||||
|
||||
let microphone_device_name = selected_microphone_device_name(inner);
|
||||
stop_microphone_preview_monitor(inner, "dictation recorder");
|
||||
acquire_recording_mute(inner, "dictation");
|
||||
acquire_recording_mute(inner, "dictation").await;
|
||||
match Recorder::start(microphone_device_name, consumer, level_handler) {
|
||||
Ok((rec, runtime_errors)) => {
|
||||
store_recorder_for_session(inner, session_id, rec);
|
||||
@@ -555,7 +555,7 @@ pub(super) async fn start_recorder_and_enter_listening(
|
||||
active_asr: &str,
|
||||
consumer: Arc<dyn crate::recorder::AudioConsumer>,
|
||||
) -> Result<(), String> {
|
||||
start_recorder_for_starting(inner, session_id, active_asr, consumer)?;
|
||||
start_recorder_for_starting(inner, session_id, active_asr, consumer).await?;
|
||||
finish_starting_session(inner, session_id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -110,37 +110,81 @@ pub(super) fn stop_microphone_preview_monitor(inner: &Arc<Inner>, owner: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn acquire_recording_mute(inner: &Arc<Inner>, owner: &str) {
|
||||
/// Acquire system-output mute for the duration of a recording session.
|
||||
///
|
||||
/// `AudioMuteGuard::activate()` on macOS shells out to `osascript` (~100–300 ms)
|
||||
/// and on Linux to `wpctl`/`pactl` (similar). When called from the async
|
||||
/// `begin_session` path that blocks the tokio worker thread for the entire
|
||||
/// duration, delaying the recorder start by exactly that much. Wrap the
|
||||
/// activate + bookkeeping in `spawn_blocking` so the tokio worker is freed
|
||||
/// while the shell-out runs. Parking-lot `Mutex` guards never cross an await
|
||||
/// (they live entirely inside the blocking task). Audit 3.2.4.
|
||||
pub(super) async fn acquire_recording_mute(inner: &Arc<Inner>, owner: &'static str) {
|
||||
if !inner.prefs.get().mute_during_recording {
|
||||
return;
|
||||
}
|
||||
let mut mute = inner.recording_mute.lock();
|
||||
if mute.holders == 0 {
|
||||
match crate::audio_mute::AudioMuteGuard::activate() {
|
||||
Ok(guard) => {
|
||||
mute.guard = Some(guard);
|
||||
log::info!("[audio-mute] system output muted for recording");
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("[audio-mute] failed to mute output for {owner}: {err}");
|
||||
return;
|
||||
let inner = Arc::clone(inner);
|
||||
let join_result = tokio::task::spawn_blocking(move || {
|
||||
let mut mute = inner.recording_mute.lock();
|
||||
if mute.holders == 0 {
|
||||
match crate::audio_mute::AudioMuteGuard::activate() {
|
||||
Ok(guard) => {
|
||||
mute.guard = Some(guard);
|
||||
log::info!("[audio-mute] system output muted for recording");
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("[audio-mute] failed to mute output for {owner}: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
mute.holders = mute.holders.saturating_add(1);
|
||||
log::info!("[audio-mute] acquired by {owner}; holders={}", mute.holders);
|
||||
})
|
||||
.await;
|
||||
// 显式记录 spawn_blocking 任务的 panic(之前是 `let _ = .await` 静默吞掉)。
|
||||
// holders/guard 状态本身在 panic 路径下仍然一致 —— 因为 panic 只能发生在
|
||||
// activate() 抛 / lock 抛,前者会让 holders 不增 + guard 仍 None,后者根本
|
||||
// 进不到 mutate 阶段;但用户碰到 system audio 在录音时漏出系统声却找不到
|
||||
// 任何 [audio-mute] 日志,没法 debug。pr_agent feedback on PR #391。
|
||||
if let Err(join_err) = join_result {
|
||||
log::error!(
|
||||
"[audio-mute] acquire task panicked for {owner}: {join_err}; mute did not activate"
|
||||
);
|
||||
}
|
||||
mute.holders = mute.holders.saturating_add(1);
|
||||
log::info!("[audio-mute] acquired by {owner}; holders={}", mute.holders);
|
||||
}
|
||||
|
||||
pub(super) fn release_recording_mute(inner: &Arc<Inner>, owner: &str) {
|
||||
let mut mute = inner.recording_mute.lock();
|
||||
if mute.holders == 0 {
|
||||
return;
|
||||
}
|
||||
mute.holders -= 1;
|
||||
log::info!("[audio-mute] released by {owner}; holders={}", mute.holders);
|
||||
if mute.holders == 0 {
|
||||
mute.guard.take();
|
||||
log::info!("[audio-mute] system output mute restored after recording");
|
||||
/// Release the recording-mute guard. The Drop impl on `AudioMuteGuard` shells
|
||||
/// out to `osascript` / `wpctl` again, so when holders reaches 0 we hand the
|
||||
/// drop off to a blocking task to keep the tokio worker free. Audit 3.2.4.
|
||||
///
|
||||
/// Fire-and-forget (no await): callers — `cancel_session`, `end_session`,
|
||||
/// recorder error monitor — don't need the mute restoration to complete
|
||||
/// before they continue. The user has already stopped recording; system audio
|
||||
/// recovery happening 100 ms later is fine.
|
||||
///
|
||||
/// `release_recording_mute` is also called from non-tokio threads (the recorder
|
||||
/// error monitor uses `std::thread::spawn`), so fall back to a synchronous
|
||||
/// run when there's no current tokio handle — running synchronously on a std
|
||||
/// thread blocks nothing.
|
||||
pub(super) fn release_recording_mute(inner: &Arc<Inner>, owner: &'static str) {
|
||||
let inner = Arc::clone(inner);
|
||||
let work = move || {
|
||||
let mut mute = inner.recording_mute.lock();
|
||||
if mute.holders == 0 {
|
||||
return;
|
||||
}
|
||||
mute.holders -= 1;
|
||||
log::info!("[audio-mute] released by {owner}; holders={}", mute.holders);
|
||||
if mute.holders == 0 {
|
||||
mute.guard.take();
|
||||
log::info!("[audio-mute] system output mute restored after recording");
|
||||
}
|
||||
};
|
||||
if let Ok(handle) = tokio::runtime::Handle::try_current() {
|
||||
handle.spawn_blocking(work);
|
||||
} else {
|
||||
work();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user