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,
|
current_session_id,
|
||||||
ActiveAsr::Bailian(Arc::clone(&asr)),
|
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 {
|
if let Err(e) = asr.open_session().await {
|
||||||
log::error!("[coord] open Bailian ASR session failed: {e}");
|
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,
|
current_session_id,
|
||||||
ActiveAsr::Volcengine(Arc::clone(&asr)),
|
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 {
|
if let Err(e) = asr.open_session().await {
|
||||||
log::error!("[coord] open ASR session failed: {e}");
|
log::error!("[coord] open ASR session failed: {e}");
|
||||||
@@ -403,7 +403,7 @@ pub(super) async fn begin_session(inner: &Arc<Inner>) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn start_recorder_for_starting(
|
pub(super) async fn start_recorder_for_starting(
|
||||||
inner: &Arc<Inner>,
|
inner: &Arc<Inner>,
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
active_asr: &str,
|
active_asr: &str,
|
||||||
@@ -448,7 +448,7 @@ pub(super) fn start_recorder_for_starting(
|
|||||||
|
|
||||||
let microphone_device_name = selected_microphone_device_name(inner);
|
let microphone_device_name = selected_microphone_device_name(inner);
|
||||||
stop_microphone_preview_monitor(inner, "dictation recorder");
|
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) {
|
match Recorder::start(microphone_device_name, consumer, level_handler) {
|
||||||
Ok((rec, runtime_errors)) => {
|
Ok((rec, runtime_errors)) => {
|
||||||
store_recorder_for_session(inner, session_id, rec);
|
store_recorder_for_session(inner, session_id, rec);
|
||||||
@@ -555,7 +555,7 @@ pub(super) async fn start_recorder_and_enter_listening(
|
|||||||
active_asr: &str,
|
active_asr: &str,
|
||||||
consumer: Arc<dyn crate::recorder::AudioConsumer>,
|
consumer: Arc<dyn crate::recorder::AudioConsumer>,
|
||||||
) -> Result<(), String> {
|
) -> 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;
|
finish_starting_session(inner, session_id).await;
|
||||||
Ok(())
|
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 {
|
if !inner.prefs.get().mute_during_recording {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let mut mute = inner.recording_mute.lock();
|
let inner = Arc::clone(inner);
|
||||||
if mute.holders == 0 {
|
let join_result = tokio::task::spawn_blocking(move || {
|
||||||
match crate::audio_mute::AudioMuteGuard::activate() {
|
let mut mute = inner.recording_mute.lock();
|
||||||
Ok(guard) => {
|
if mute.holders == 0 {
|
||||||
mute.guard = Some(guard);
|
match crate::audio_mute::AudioMuteGuard::activate() {
|
||||||
log::info!("[audio-mute] system output muted for recording");
|
Ok(guard) => {
|
||||||
}
|
mute.guard = Some(guard);
|
||||||
Err(err) => {
|
log::info!("[audio-mute] system output muted for recording");
|
||||||
log::warn!("[audio-mute] failed to mute output for {owner}: {err}");
|
}
|
||||||
return;
|
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) {
|
/// Release the recording-mute guard. The Drop impl on `AudioMuteGuard` shells
|
||||||
let mut mute = inner.recording_mute.lock();
|
/// out to `osascript` / `wpctl` again, so when holders reaches 0 we hand the
|
||||||
if mute.holders == 0 {
|
/// drop off to a blocking task to keep the tokio worker free. Audit 3.2.4.
|
||||||
return;
|
///
|
||||||
}
|
/// Fire-and-forget (no await): callers — `cancel_session`, `end_session`,
|
||||||
mute.holders -= 1;
|
/// recorder error monitor — don't need the mute restoration to complete
|
||||||
log::info!("[audio-mute] released by {owner}; holders={}", mute.holders);
|
/// before they continue. The user has already stopped recording; system audio
|
||||||
if mute.holders == 0 {
|
/// recovery happening 100 ms later is fine.
|
||||||
mute.guard.take();
|
///
|
||||||
log::info!("[audio-mute] system output mute restored after recording");
|
/// `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