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:
+43
-8
@@ -16,7 +16,7 @@ mod utils;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
use tauri::Manager;
|
||||
use tauri::{Emitter, Manager};
|
||||
use log::{error, info};
|
||||
#[cfg(target_os = "android")]
|
||||
use log::warn;
|
||||
@@ -120,8 +120,9 @@ use download::cache::{CacheConfig as SmartCacheConfig, SmartCache};
|
||||
use download::DownloadManager;
|
||||
use jellyfin::{HttpClient, HttpConfig};
|
||||
use player::{MediaSessionManager, PlayerBackend, PlayerController, TauriEventEmitter};
|
||||
// NullBackend fallback for platforms without native backends (not Linux or Android)
|
||||
#[cfg(not(any(target_os = "linux", target_os = "android")))]
|
||||
// NullBackend is used both for platforms without a native backend AND as a graceful
|
||||
// fallback when a native backend (MPV/ExoPlayer) fails to initialize, so the app can
|
||||
// still launch (browse library, manage downloads, see an error) instead of crashing.
|
||||
use player::NullBackend;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -225,13 +226,39 @@ impl RemoteVolumeHandler for RemoteVolumeSessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/// Payload emitted to the frontend when a native player backend fails to
|
||||
/// initialize and the app falls back to a no-op backend.
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
struct BackendInitError {
|
||||
platform: &'static str,
|
||||
backend: &'static str,
|
||||
message: String,
|
||||
}
|
||||
|
||||
/// Log a backend-initialization failure and notify the frontend, so the UI can
|
||||
/// surface "playback unavailable" instead of the app hard-crashing.
|
||||
fn emit_backend_init_failed(app_handle: &tauri::AppHandle, backend: &'static str, message: String) {
|
||||
error!(
|
||||
"[INIT] Player backend '{}' failed to initialize: {}. Falling back to NullBackend (playback disabled).",
|
||||
backend, message
|
||||
);
|
||||
let _ = app_handle.emit(
|
||||
"backend-init-failed",
|
||||
BackendInitError {
|
||||
platform: std::env::consts::OS,
|
||||
backend,
|
||||
message,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Create the appropriate player backend for the current platform.
|
||||
fn create_player_backend(
|
||||
app_handle: tauri::AppHandle,
|
||||
playback_reporter: Arc<tokio::sync::Mutex<Option<playback_reporting::PlaybackReporter>>>,
|
||||
position_throttler: Arc<playback_reporting::EventThrottler>,
|
||||
) -> Box<dyn PlayerBackend> {
|
||||
let _event_emitter = Arc::new(TauriEventEmitter::new(app_handle));
|
||||
let _event_emitter = Arc::new(TauriEventEmitter::new(app_handle.clone()));
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
@@ -255,17 +282,21 @@ fn create_player_backend(
|
||||
return Box::new(backend);
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("FATAL: Failed to initialize ExoPlayer backend on Android: {}. This is a critical error - playback will not work.", e);
|
||||
// Degrade gracefully instead of crashing the app.
|
||||
emit_backend_init_failed(&app_handle, "exoplayer", e.to_string());
|
||||
return Box::new(NullBackend::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("FATAL: Failed to attach JNI thread on Android: {}. This is a critical error - playback will not work.", e);
|
||||
emit_backend_init_failed(&app_handle, "exoplayer", format!("attach JNI thread failed: {}", e));
|
||||
return Box::new(NullBackend::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("FATAL: Failed to create JavaVM on Android: {}. This is a critical error - playback will not work.", e);
|
||||
emit_backend_init_failed(&app_handle, "exoplayer", format!("create JavaVM failed: {}", e));
|
||||
return Box::new(NullBackend::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,7 +329,11 @@ fn create_player_backend(
|
||||
error!("\nAudio playback will NOT work until this is fixed.");
|
||||
error!("========================================\n");
|
||||
|
||||
panic!("Cannot start application: MPV backend initialization failed. See error message above.");
|
||||
// Degrade gracefully: launch with a no-op backend so the user can
|
||||
// still browse the library and manage downloads, and the frontend
|
||||
// can show a "playback unavailable" notice via this event.
|
||||
emit_backend_init_failed(&app_handle, "mpv", e.to_string());
|
||||
return Box::new(NullBackend::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user