// 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() { if INITIALIZED.load(Ordering::Acquire) { return; } // Backoff check OUTSIDE the lock — avoids serializing every // already-throttled caller on a single mutex. let last = LAST_ATTEMPT_MS.load(Ordering::Acquire); let now = now_ms(); if last != 0 && now.saturating_sub(last) < RETRY_BACKOFF_MS { return; } let _guard = match INIT_LOCK.lock() { Ok(g) => g, Err(p) => p.into_inner(), }; // Re-check under the lock — another thread may have just // succeeded while we were waiting. if INITIALIZED.load(Ordering::Acquire) { return; } LAST_ATTEMPT_MS.store(now_ms(), Ordering::Release); match ReqwestDownloader::new() { Ok(dl) => { NewPipe::init_full( Arc::new(dl), Localization::default(), ContentCountry::default(), ); INITIALIZED.store(true, Ordering::Release); log::info!("strawcore-core: downloader + localization initialized"); } Err(e) => { // Don't surface the underlying error string verbatim — // it can embed URLs / hosts. log::error!("strawcore-core: downloader init failed (will retry on next call)"); let _ = e; } } }