fix(audio): replace Mutex unwrap with poisoned-mutex recovery (TODO-006)
Replace 42 .lock().unwrap() calls with .unwrap_or_else(|e| e.into_inner()) across 7 files. Poisoned mutex recovery prevents panics in realtime audio callbacks. Add SAFETY comment to WebRtcFallbackVad Send impl (TODO-007).
This commit is contained in:
@@ -481,7 +481,7 @@ impl AudioEngine {
|
||||
crate::android_voice_unit::chanora_android_abandon_audio_focus();
|
||||
crate::android_voice_unit::clear_global_event_sender();
|
||||
|
||||
if let Some(mut unit) = android_voice_unit.lock().unwrap().take() {
|
||||
if let Some(mut unit) = android_voice_unit.lock().unwrap_or_else(|e| e.into_inner()).take() {
|
||||
use crate::mobile_voice_backend::MobileVoiceAudioBackend;
|
||||
if let Err(e) = unit.close() {
|
||||
warn!(
|
||||
@@ -524,7 +524,7 @@ impl AudioEngine {
|
||||
),
|
||||
}
|
||||
|
||||
let cfg_av = android_voice_stream_config.lock().unwrap().clone();
|
||||
let cfg_av = android_voice_stream_config.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let params = crate::mobile_voice_backend::VoiceAudioParams {
|
||||
voice_out_tx: voice_out_tx.clone(),
|
||||
transmit_active: transmit_gate.flag_arc(),
|
||||
@@ -577,7 +577,7 @@ impl AudioEngine {
|
||||
"android: bluetooth route re-engaged after backend disconnect"
|
||||
);
|
||||
}
|
||||
*android_voice_unit.lock().unwrap() = Some(reopened);
|
||||
*android_voice_unit.lock().unwrap_or_else(|e| e.into_inner()) = Some(reopened);
|
||||
}
|
||||
Err(e) => warn!(
|
||||
target: "chanora_audio",
|
||||
@@ -941,7 +941,7 @@ impl AudioEngine {
|
||||
match item {
|
||||
Some(v) => {
|
||||
let id = SessionAudioId(v.from_client);
|
||||
let mut h = handler_for_task.lock().unwrap();
|
||||
let mut h = handler_for_task.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Err(e) = h.handle_packet(id, v.packet) {
|
||||
debug!(target: "chanora_audio", error = %e, "decode failed");
|
||||
} else {
|
||||
@@ -1283,7 +1283,7 @@ impl AudioEngine {
|
||||
match item {
|
||||
Some(v) => {
|
||||
let id = SessionAudioId(v.from_client);
|
||||
let mut h = handler_for_task.lock().unwrap();
|
||||
let mut h = handler_for_task.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let res = h.handle_packet(id, v.packet);
|
||||
drop(h);
|
||||
match res {
|
||||
@@ -1371,12 +1371,12 @@ impl AudioEngine {
|
||||
not(target_os = "android")
|
||||
))]
|
||||
{
|
||||
let _ = self._input_stream.lock().unwrap().take();
|
||||
let _ = self._output_stream.lock().unwrap().take();
|
||||
let _ = self._input_stream.lock().unwrap_or_else(|e| e.into_inner()).take();
|
||||
let _ = self._output_stream.lock().unwrap_or_else(|e| e.into_inner()).take();
|
||||
}
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
{
|
||||
let _ = self._ios_voice_backend.lock().unwrap().take();
|
||||
let _ = self._ios_voice_backend.lock().unwrap_or_else(|e| e.into_inner()).take();
|
||||
}
|
||||
// SDD-115 reverse-order teardown on Android:
|
||||
// 1) stop Bluetooth SCO + abandon audio focus;
|
||||
@@ -1390,7 +1390,7 @@ impl AudioEngine {
|
||||
crate::android_voice_unit::chanora_android_stop_bluetooth_sco();
|
||||
crate::android_voice_unit::chanora_android_abandon_audio_focus();
|
||||
crate::android_voice_unit::clear_global_event_sender();
|
||||
if let Some(mut unit) = self._android_voice_unit.lock().unwrap().take() {
|
||||
if let Some(mut unit) = self._android_voice_unit.lock().unwrap_or_else(|e| e.into_inner()).take() {
|
||||
use crate::mobile_voice_backend::MobileVoiceAudioBackend;
|
||||
if let Err(e) = unit.close() {
|
||||
warn!(
|
||||
@@ -1456,7 +1456,7 @@ impl AudioEngine {
|
||||
pub fn ios_restart_voice_unit(&self) -> Result<(), AudioError> {
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
{
|
||||
let mut guard = self._ios_voice_backend.lock().unwrap();
|
||||
let mut guard = self._ios_voice_backend.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let backend = guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| AudioError::Backend("ios voice backend not running".to_string()))?;
|
||||
@@ -1479,7 +1479,7 @@ impl AudioEngine {
|
||||
crate::android_voice_unit::chanora_android_abandon_audio_focus();
|
||||
crate::android_voice_unit::clear_global_event_sender();
|
||||
|
||||
if let Some(mut unit) = self._android_voice_unit.lock().unwrap().take() {
|
||||
if let Some(mut unit) = self._android_voice_unit.lock().unwrap_or_else(|e| e.into_inner()).take() {
|
||||
unit.close().map_err(|e| {
|
||||
AudioError::Backend(format!(
|
||||
"android: failed to close Oboe voice unit during route restart: {e}"
|
||||
@@ -1501,7 +1501,7 @@ impl AudioEngine {
|
||||
);
|
||||
}
|
||||
|
||||
let cfg_av = self.android_voice_stream_config.lock().unwrap().clone();
|
||||
let cfg_av = self.android_voice_stream_config.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
let params = crate::mobile_voice_backend::VoiceAudioParams {
|
||||
voice_out_tx: self.voice_out_tx.clone(),
|
||||
transmit_active: self.transmit_gate.flag_arc(),
|
||||
@@ -1549,7 +1549,7 @@ impl AudioEngine {
|
||||
);
|
||||
}
|
||||
|
||||
let mut guard = self._android_voice_unit.lock().unwrap();
|
||||
let mut guard = self._android_voice_unit.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*guard = Some(reopened);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1563,7 +1563,7 @@ impl AudioEngine {
|
||||
pub fn ios_pause_voice_unit(&self) -> Result<(), AudioError> {
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
{
|
||||
let mut guard = self._ios_voice_backend.lock().unwrap();
|
||||
let mut guard = self._ios_voice_backend.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let unit = guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| AudioError::Backend("ios voice backend not running".to_string()))?;
|
||||
@@ -1579,7 +1579,7 @@ impl AudioEngine {
|
||||
pub fn ios_resume_voice_unit(&self) -> Result<(), AudioError> {
|
||||
#[cfg(any(target_os = "ios", target_os = "macos"))]
|
||||
{
|
||||
let mut guard = self._ios_voice_backend.lock().unwrap();
|
||||
let mut guard = self._ios_voice_backend.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let unit = guard
|
||||
.as_mut()
|
||||
.ok_or_else(|| AudioError::Backend("ios voice backend not running".to_string()))?;
|
||||
@@ -1639,7 +1639,7 @@ impl AudioEngine {
|
||||
|
||||
/// Current audio-processing config snapshot.
|
||||
pub fn audio_processing_config_snapshot(&self) -> crate::AudioProcessingConfig {
|
||||
self.audio_processing_config.lock().unwrap().clone()
|
||||
self.audio_processing_config.lock().unwrap_or_else(|e| e.into_inner()).clone()
|
||||
}
|
||||
|
||||
/// Apply a voice-processing config after validating iOS invariants.
|
||||
@@ -1655,7 +1655,7 @@ impl AudioEngine {
|
||||
}
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
self.apply_desktop_vad_backend(&config);
|
||||
let mut guard = self.audio_processing_config.lock().unwrap();
|
||||
let mut guard = self.audio_processing_config.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*guard = config;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1681,7 +1681,7 @@ impl AudioEngine {
|
||||
|
||||
/// Current voice-processing stats snapshot.
|
||||
pub fn audio_processing_stats(&self) -> crate::AudioProcessingStats {
|
||||
let config = self.audio_processing_config.lock().unwrap().clone();
|
||||
let config = self.audio_processing_config.lock().unwrap_or_else(|e| e.into_inner()).clone();
|
||||
self.audio_processing_stats.snapshot(&config)
|
||||
}
|
||||
|
||||
@@ -1816,7 +1816,7 @@ fn apply_desktop_vad_backend_to_worker(
|
||||
audio_processing_stats: &crate::SharedAudioProcessingStats,
|
||||
) {
|
||||
if config.vad_backend != crate::VadBackend::SileroOnnx {
|
||||
let mut worker_guard = silero_vad_worker.lock().unwrap();
|
||||
let mut worker_guard = silero_vad_worker.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if worker_guard.is_some() {
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
@@ -1841,12 +1841,12 @@ fn apply_desktop_vad_backend_to_worker(
|
||||
);
|
||||
let new_worker = crate::vad::silero_onnx::SileroOnnxVadWorker::try_new(&model_path);
|
||||
{
|
||||
let mut worker_guard = silero_vad_worker.lock().unwrap();
|
||||
let mut worker_guard = silero_vad_worker.lock().unwrap_or_else(|e| e.into_inner());
|
||||
*worker_guard = new_worker;
|
||||
}
|
||||
// Check result after releasing the lock. Re-acquire is cheap and
|
||||
// ensures we log the correct state without holding the mutex.
|
||||
let worker_installed = silero_vad_worker.lock().unwrap().is_some();
|
||||
let worker_installed = silero_vad_worker.lock().unwrap_or_else(|e| e.into_inner()).is_some();
|
||||
if worker_installed {
|
||||
info!(
|
||||
target: "chanora_audio",
|
||||
@@ -1926,21 +1926,21 @@ mod tests {
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
#[test]
|
||||
fn desktop_startup_audio_processing_state_applies_default_silero_fallback() {
|
||||
let _guard = crate::vad::SILERO_MODEL_PATH_TEST_LOCK.lock().unwrap();
|
||||
let _guard = crate::vad::SILERO_MODEL_PATH_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
crate::vad::clear_silero_model_path_for_test();
|
||||
|
||||
let (config, stats, worker) = new_desktop_audio_processing_state();
|
||||
|
||||
assert_eq!(
|
||||
config.lock().unwrap().vad_backend,
|
||||
config.lock().unwrap_or_else(|e| e.into_inner()).vad_backend,
|
||||
crate::VadBackend::SileroOnnx,
|
||||
"desktop startup config should keep the default Silero backend selected"
|
||||
);
|
||||
assert!(
|
||||
worker.lock().unwrap().is_none(),
|
||||
worker.lock().unwrap_or_else(|e| e.into_inner()).is_none(),
|
||||
"missing startup model should not create an ONNX worker"
|
||||
);
|
||||
let snapshot = stats.snapshot(&config.lock().unwrap());
|
||||
let snapshot = stats.snapshot(&config.lock().unwrap_or_else(|e| e.into_inner()));
|
||||
assert!(
|
||||
snapshot.vad_fallback_active,
|
||||
"desktop startup should mark WebRTC fallback active when the default Silero worker cannot load"
|
||||
@@ -1983,7 +1983,7 @@ mod tests {
|
||||
#[cfg(not(any(target_os = "ios", target_os = "macos", target_os = "android")))]
|
||||
#[test]
|
||||
fn desktop_capture_silero_selection_reports_fallback_when_worker_unavailable() {
|
||||
let _guard = crate::vad::SILERO_MODEL_PATH_TEST_LOCK.lock().unwrap();
|
||||
let _guard = crate::vad::SILERO_MODEL_PATH_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
crate::vad::clear_silero_model_path_for_test();
|
||||
|
||||
let gate = crate::ptt::AudioTransmitGate::new(false);
|
||||
@@ -2025,7 +2025,7 @@ mod tests {
|
||||
}
|
||||
capture.ingest(&voiced);
|
||||
|
||||
let snapshot = stats.snapshot(&config.lock().unwrap());
|
||||
let snapshot = stats.snapshot(&config.lock().unwrap_or_else(|e| e.into_inner()));
|
||||
assert_eq!(snapshot.vad_backend, crate::VadBackend::SileroOnnx);
|
||||
assert!(
|
||||
snapshot.vad_fallback_active,
|
||||
@@ -2132,7 +2132,7 @@ mod tests {
|
||||
|
||||
capture.process_10ms_capture_frame(&voiced);
|
||||
|
||||
let snapshot = stats.snapshot(&config.lock().unwrap());
|
||||
let snapshot = stats.snapshot(&config.lock().unwrap_or_else(|e| e.into_inner()));
|
||||
assert!(
|
||||
snapshot.vad_fallback_active,
|
||||
"stale Silero worker output should report active WebRTC fallback"
|
||||
@@ -2725,7 +2725,7 @@ where
|
||||
.build_input_stream(
|
||||
*config,
|
||||
move |data: &[T], _: &cpal::InputCallbackInfo| {
|
||||
let mut s = state.lock().unwrap();
|
||||
let mut s = state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
s.ingest(data);
|
||||
},
|
||||
move |e| {
|
||||
@@ -2816,7 +2816,7 @@ where
|
||||
// only for the duration of fill_buffer; the inbound
|
||||
// forwarder uses handle_packet which is queue-fast.
|
||||
{
|
||||
let mut h = handler.lock().unwrap();
|
||||
let mut h = handler.lock().unwrap_or_else(|e| e.into_inner());
|
||||
h.fill_buffer(&mut scratch[..needed]);
|
||||
}
|
||||
|
||||
@@ -2835,7 +2835,7 @@ where
|
||||
// Resample 48 kHz stereo → device-rate ×
|
||||
// device-channels with continuity across
|
||||
// callback boundaries.
|
||||
let mut state = resample_state.lock().unwrap();
|
||||
let mut state = resample_state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut pos = state.pos;
|
||||
let mut last_l = state.last_l;
|
||||
let mut last_r = state.last_r;
|
||||
|
||||
Reference in New Issue
Block a user