// Runtime bootstrap. Called once from Kotlin's StrawApp.onCreate via // init_logging(), and again before every strawcore call. Wires the // strawcore-core Downloader + Localization singleton so the extractor // has an HTTP client to use. // // Round-4 audit HIGH-1: the prior shape used `Once::call_once` and // silently swallowed errors. If the FIRST call ran while the network // stack wasn't ready (cold boot in airplane mode, SELinux denial on // first TLS init, transient resolver failure), the Once slot was // consumed, NewPipe::init_full never ran, and every subsequent // search/streamInfo/channelInfo returned DownloaderMissing for the // rest of the process lifetime. // // New shape: use an AtomicBool to track success. Only "success" closes // the door. On failure we retry — rate-limited so a persistently-broken // network doesn't hammer reqwest::Client::new() on every call. use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{SystemTime, UNIX_EPOCH}; use strawcore_core::downloader::ReqwestDownloader; use strawcore_core::localization::{ContentCountry, Localization}; use strawcore_core::newpipe::NewPipe; static INITIALIZED: AtomicBool = AtomicBool::new(false); static LAST_ATTEMPT_MS: AtomicU64 = AtomicU64::new(0); // Guards the actual init attempt so concurrent calls don't all try // to build the downloader in parallel; serial retry is the goal. static INIT_LOCK: Mutex<()> = Mutex::new(()); /// Min ms between retries when init has failed. 5s — enough that a /// hot loop of failed searches doesn't pin a CPU on reqwest setup, /// short enough that a user who toggled airplane mode off recovers /// within one tap. const RETRY_BACKOFF_MS: u64 = 5_000; fn now_ms() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_millis() as u64) .unwrap_or(0) } pub fn ensure_initialized() { // Fast path: already initialized. Single Acquire load. if INITIALIZED.load(Ordering::Acquire) { return; } // Backoff check BEFORE the lock — a recent failure shouldn't // make N concurrent callers queue on a mutex they'll all skip // out of anyway. let last = LAST_ATTEMPT_MS.load(Ordering::Acquire); let now = now_ms(); if last != 0 && now.saturating_sub(last) < RETRY_BACKOFF_MS { return; } // try_lock — if another thread is already mid-init, return // immediately rather than block. The caller will get // DownloaderMissing once from the extractor and recover on // the next user action; the alternative (blocking N tokio // workers for the full duration of a slow init) freezes the // UI. Round-6 audit HIGH-2 was the regression on round-5's // mutex-first ordering. let _guard = match INIT_LOCK.try_lock() { Ok(g) => g, Err(_) => return, }; // Re-check under the lock — another thread may have just succeeded. if INITIALIZED.load(Ordering::Acquire) { return; } match ReqwestDownloader::new() { Ok(dl) => { NewPipe::init_full( Arc::new(dl), Localization::default(), ContentCountry::default(), ); INITIALIZED.store(true, Ordering::Release); // Clear LAST_ATTEMPT_MS so a future hypothetical // re-init path (none today) wouldn't see cooldown // bleed from this success. LAST_ATTEMPT_MS.store(0, Ordering::Release); log::info!("strawcore-core: downloader + localization initialized"); } Err(e) => { // Stamp the timestamp on FAILURE only, so the next // caller within RETRY_BACKOFF_MS skips, but a successful // attempt isn't reflected in the backoff state. LAST_ATTEMPT_MS.store(now, Ordering::Release); log::error!("strawcore-core: downloader init failed (will retry on next call)"); let _ = e; } } }