Architecture remediation A/B/F: poison-tolerant locks, graceful backend init, doc fixes
Workstream A — poison-tolerant locking: - Add utils/lock.rs with MutexSafe/RwLockSafe extension traits that recover a poisoned std::sync lock instead of panicking, plus unit tests. - Replace all 153 .lock().unwrap() and 4 .read()/.write().unwrap() production sites with _safe variants across 14 files, eliminating the player crash-cascade class. Tokio async mutexes are unchanged. Workstream B — graceful backend init: - create_player_backend no longer panics when MPV/ExoPlayer fail to initialize; it falls back to NullBackend and emits a backend-init-failed event so the UI can show "playback unavailable" instead of the app crashing. Fatal DB-setup panics are kept. Workstream F — doc reconciliation: - Rewrite software-architecture.md's inaccurate "thin UI / ~800 lines" claims to reflect reality (~20.5k non-test frontend) and document the events+polling hybrid plus the new locking/backend-init behavior.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
//!
|
||||
//! TRACES: UR-010 | JA-021
|
||||
|
||||
use crate::utils::lock::{MutexSafe, RwLockSafe};
|
||||
use log::{debug, info, warn};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
@@ -60,7 +61,7 @@ impl SessionPollerManager {
|
||||
|
||||
/// Set event emitter for broadcasting session updates
|
||||
pub fn set_event_emitter(&self, emitter: Arc<dyn PlayerEventEmitter>) {
|
||||
*self.event_emitter.lock().unwrap() = Some(emitter);
|
||||
*self.event_emitter.lock_safe() = Some(emitter);
|
||||
}
|
||||
|
||||
/// Start the background polling thread
|
||||
@@ -88,7 +89,7 @@ impl SessionPollerManager {
|
||||
// Calculate poll interval based on mode and hint
|
||||
let new_interval = Self::calculate_interval(
|
||||
&mode_manager.get_mode(),
|
||||
*hint.read().unwrap(),
|
||||
*hint.read_safe(),
|
||||
);
|
||||
|
||||
interval_ms.store(new_interval, Ordering::Relaxed);
|
||||
@@ -97,7 +98,7 @@ impl SessionPollerManager {
|
||||
|
||||
// Fetch sessions
|
||||
let sessions_result = rt.block_on(async {
|
||||
let client_opt = client.lock().unwrap().clone();
|
||||
let client_opt = client.lock_safe().clone();
|
||||
match client_opt {
|
||||
Some(c) => c.get_sessions().await,
|
||||
None => {
|
||||
@@ -111,7 +112,7 @@ impl SessionPollerManager {
|
||||
match sessions_result {
|
||||
Ok(sessions) => {
|
||||
debug!("[SessionPoller] Fetched {} sessions", sessions.len());
|
||||
if let Some(em) = emitter.lock().unwrap().as_ref() {
|
||||
if let Some(em) = emitter.lock_safe().as_ref() {
|
||||
em.emit(crate::player::PlayerStatusEvent::SessionsUpdated {
|
||||
sessions,
|
||||
});
|
||||
@@ -129,7 +130,7 @@ impl SessionPollerManager {
|
||||
info!("[SessionPoller] Polling thread stopped");
|
||||
});
|
||||
|
||||
*self.thread_handle.lock().unwrap() = Some(handle);
|
||||
*self.thread_handle.lock_safe() = Some(handle);
|
||||
}
|
||||
|
||||
/// Stop the polling thread
|
||||
@@ -138,7 +139,7 @@ impl SessionPollerManager {
|
||||
self.is_running.store(false, Ordering::Relaxed);
|
||||
|
||||
// Join the thread if possible (don't block indefinitely)
|
||||
if let Some(handle) = self.thread_handle.lock().unwrap().take() {
|
||||
if let Some(handle) = self.thread_handle.lock_safe().take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
@@ -146,7 +147,7 @@ impl SessionPollerManager {
|
||||
/// Set UI hint for polling frequency adjustment
|
||||
pub fn set_polling_hint(&self, hint: PollingHint) {
|
||||
debug!("[SessionPoller] Setting polling hint: {:?}", hint);
|
||||
*self.current_hint.write().unwrap() = hint;
|
||||
*self.current_hint.write_safe() = hint;
|
||||
}
|
||||
|
||||
/// Calculate polling interval based on mode and hint
|
||||
@@ -165,7 +166,7 @@ impl SessionPollerManager {
|
||||
|
||||
/// Manually trigger a poll (for frontend refresh button)
|
||||
pub async fn poll_now(&self) -> Result<Vec<crate::jellyfin::client::SessionInfo>, String> {
|
||||
let client = self.jellyfin_client.lock().unwrap().clone()
|
||||
let client = self.jellyfin_client.lock_safe().clone()
|
||||
.ok_or("Jellyfin client not configured")?;
|
||||
|
||||
client.get_sessions().await
|
||||
|
||||
Reference in New Issue
Block a user