vc=40: loop round 2/5 — round-1 misses + new HIGHs from round-5 audits
Three parallel Opus round-5 audits ran on vc=39. The big finds were
regressions on vc=39's own fixes — the retry-init wiring was incomplete,
the URL-fence pattern missed CancellationException swallowing, and the
SettingsStore idempotency branch was dead code. Real round-5 work.
HIGH
R5-1 Rust `ensure_initialized` was only called from `init_logging`
— extractor entry points never invoked it, so the 5s-backoff
retry from vc=39 was unreachable. A cold-boot init failure
still bricked extraction for the whole process. Now every
`search()` / `stream_info()` / `channel_info()` calls
ensure_initialized at entry; cheap when INITIALIZED is true.
R5-2 runtime.rs in-progress race: prior shape stamped
LAST_ATTEMPT_MS at the *start* of an attempt, then let
concurrent callers short-circuit via the backoff check
and proceed to call into the extractor before init
completed. New: lock FIRST (mutex IS the in-progress
queue), re-check INITIALIZED under lock, only THEN check
backoff (and only stamp it on failure).
R5-3 `runCatching` in VideoDetailViewModel + SubscriptionFeed
swallows CancellationException — when a job was cancelled
mid-suspend inside channelInfo/RYD/SB, the inner cancellation
was eaten, the lambda returned its default, the job carried
past the runCatching to its terminal write, and the URL fence
let it through because same-URL races can't be distinguished
by string equality. New util.runCatchingCancellable
re-throws CancellationException; all coroutine-body
runCatching sites in the affected VMs migrated.
R5-4 SearchViewModel.submit post-network fence only guarded
`_ui.update`. SearchCache.record + pool rebuild proceeded
for a cancelled query → ghost cache entries for queries
the user abandoned mid-stroke. Now: re-check the query
AFTER the IO suspend and before the cache write.
R5-5 ChannelViewModel.load / VideoDetailViewModel.load now gate
the extractor entry on isAllowedYtUrl(channelUrl/streamUrl).
Prior shape only gated recordWatch persistence — extractor
invocation for poisoned uploaderUrl still happened.
MED
R5-6 SettingsStore.set{MaxResolution,ThemeMode}: vc=39 used
`updateAndGet { r } == r` which is unconditionally true
(lambda ignores prior) — the in-memory equality check was
dead code. SP-side check still gated the disk write so the
feature worked, but the dead branch was a comment-vs-code
liar. Rewrote with explicit before-capture + equality
gate.
R5-7 SponsorBlockSkipLoop polled `controller.currentPosition`
every 150ms even when paused — paused-overnight playback
ate ~24k binder calls/hour. Now: when `!isPlaying`, sleep
1s and continue.
R5-8 StrawApp.appScope had no CoroutineExceptionHandler — an
uncaught throwable in a top-level launch could crash on
cold start even with SupervisorJob (top-level failure
still propagates to default handler). Added handler that
logs via strawLogW.
R5-9 YtUrl.isAllowedYtUrl now requires http/https scheme
(schemeless `//host/...` URLs no longer pass) and strips
a single trailing dot from host (RFC FQDN form). Defense
in depth.
LOW
R5-10 NowPlaying.set() removed — non-CAS setter footgun
alongside the race-free claim()/CAS path. No external
callers (grep clean). Doc-comment updated.
Deferred (later round / different scope):
- PlaylistsStore URL canonicalization (round-5 MED-1 — needs a
shared YT-id-extract util; not blocking).
- Release R8 + Nav rememberSaveable (still deferred).
- LazyColumn keys + collectAsStateWithLifecycle (cosmetic).
- SponsorBlockSkipLoop currentMediaItem binding (round-5
deferred).
This commit is contained in:
parent
b8325d1726
commit
da48109a4d
15 changed files with 141 additions and 45 deletions
|
|
@ -24,6 +24,7 @@ pub struct ChannelInfo {
|
|||
#[uniffi::export(async_runtime = "tokio")]
|
||||
pub async fn channel_info(input: String) -> Result<ChannelInfo, StrawcoreError> {
|
||||
log::info!("strawcore::channel_info input_len={}", input.len());
|
||||
crate::runtime::ensure_initialized();
|
||||
let identifier = resolve_channel_identifier(&input)?;
|
||||
let core = tokio::task::spawn_blocking(move || core_channel_info(identifier))
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -43,26 +43,32 @@ fn now_ms() -> u64 {
|
|||
}
|
||||
|
||||
pub fn ensure_initialized() {
|
||||
// Fast path: already initialized. Just an Acquire load.
|
||||
if INITIALIZED.load(Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
// Backoff check OUTSIDE the lock — avoids serializing every
|
||||
// already-throttled caller on a single mutex.
|
||||
// Acquire the lock FIRST. The mutex is the in-progress queue —
|
||||
// concurrent callers wait here while one thread does init.
|
||||
// Round-5 audit HIGH-1: the prior shape stamped LAST_ATTEMPT_MS
|
||||
// at the start of an attempt then let concurrent callers
|
||||
// short-circuit-out via the backoff check; they'd then call into
|
||||
// the extractor before init completed → DownloaderMissing.
|
||||
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.
|
||||
if INITIALIZED.load(Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
// Now the backoff check — but ONLY skips when a *prior* failed
|
||||
// attempt is still in cooldown. If LAST_ATTEMPT_MS is zero, no
|
||||
// attempt has been made yet; the lock fall-through proceeds.
|
||||
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(
|
||||
|
|
@ -71,11 +77,17 @@ pub fn ensure_initialized() {
|
|||
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) => {
|
||||
// Don't surface the underlying error string verbatim —
|
||||
// it can embed URLs / hosts.
|
||||
// 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ pub async fn search(query: String) -> Result<Vec<SearchItem>, StrawcoreError> {
|
|||
// Settings → Export Logs path straight into a user's chat. Log
|
||||
// shape, not content. vc=36 audit CVE HIGH-2.
|
||||
log::info!("strawcore::search query_len={}", query.len());
|
||||
// Round-5 audit MED-1: ensure_initialized was only wired into
|
||||
// init_logging() so the 5s-backoff retry path never fired from
|
||||
// the hot entry points. Now every extractor entry re-asserts
|
||||
// — cheap when INITIALIZED is true (single Acquire load).
|
||||
crate::runtime::ensure_initialized();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
search_extractor::search(&query, SearchFilter::Videos)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ pub struct AudioStreamItem {
|
|||
#[uniffi::export(async_runtime = "tokio")]
|
||||
pub async fn stream_info(input: String) -> Result<StreamInfo, StrawcoreError> {
|
||||
log::info!("strawcore::stream_info input_len={}", input.len());
|
||||
crate::runtime::ensure_initialized();
|
||||
let video_id = resolve_video_id(&input)?;
|
||||
let video_id_for_call = video_id.clone();
|
||||
let core = tokio::task::spawn_blocking(move || core_stream_info(&video_id_for_call))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue