Files
qbz/crates/qbz-offline-cache/src/secret_vault.rs
T
vicrodh 4041499114 fix(offline-cache): run the secret-vault open off the async pool (no zbus panic)
The CMAF download opened the SecretBox (OS keyring -> secret-service -> a
synchronous zbus D-Bus round-trip) directly on a tokio async worker, which
panicked ("Cannot start a runtime from within a runtime") and poisoned the
vault mutex — every later attempt then failed and fell back to the legacy
plain-FLAC path. Playback never hit this because load_cmaf_bundle already runs
inside spawn_blocking.

- downloader: wrap the vault open + key wrap in tokio::task::spawn_blocking
  (off the async pool), mirroring the playback decrypt path.
- secret_vault: get_or_init recovers a poisoned mutex (into_inner) instead of
  wedging the vault for the rest of the session.

Verified at runtime: CMAF bundle now persists (tracks-cmaf/<id>/) and plays
back decrypted; no panic, no legacy fallback.

cc
2026-05-28 08:28:46 -06:00

38 lines
1.5 KiB
Rust

//! Lazy singleton access to the `qbz-secrets::SecretBox` for the offline
//! cache.
//!
//! The vault is opened on first use (with the OS-keyring + KDF-fallback
//! selection happening inside `qbz-secrets`) and then shared across all
//! offline-cache calls. The storage directory is the app config dir — the
//! install UUID for the KDF fallback lives there so it survives upgrades.
use std::path::Path;
use std::sync::OnceLock;
use qbz_secrets::{SecretBox, SecretError};
const SERVICE_NAME: &str = "qbz";
static VAULT: OnceLock<std::sync::Mutex<Option<SecretBox>>> = OnceLock::new();
/// Get-or-init the vault. `storage_dir` is used only on the first call;
/// subsequent calls return the cached handle regardless.
pub fn get_or_init(storage_dir: &Path) -> Result<SecretBox, SecretError> {
let cell = VAULT.get_or_init(|| std::sync::Mutex::new(None));
// Recover from a poisoned mutex rather than wedging the vault for the rest
// of the session: a panic during a prior `SecretBox::open` (e.g. the zbus
// "runtime within a runtime" panic this guards against) leaves the inner
// `Option` as `None`, so recovering simply retries the open.
let mut guard = cell.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(existing) = guard.as_ref() {
return Ok(existing.clone());
}
let vault = SecretBox::open(SERVICE_NAME, storage_dir)?;
log::info!(
"[OfflineCache/Vault] Opened SecretBox backend={:?}",
vault.backend_kind()
);
*guard = Some(vault.clone());
Ok(vault)
}