//! Stop signalling for in-flight downloads. //! //! TRACES: UR-055 | DR-168 //! //! Pausing and cancelling used to be database-only: `pause_download` wrote //! `status = 'paused'` and nothing else. No cancellation existed anywhere in the //! download stack — no token, no flag, no abort — so the streaming task kept //! running, kept writing bytes, and on finishing overwrote the row with //! `completed` or `failed`. The row flicked to "paused" and then undid itself, //! which is precisely the reported "pause does not work". //! //! This is the missing half: a flag per in-flight download that the worker reads //! between chunks. Setting it makes the worker return [`Stopped`] promptly and //! leave the `.part` file **intact**, which is what lets a resume pick up from //! where it stopped via the existing HTTP Range request. //! //! Kept as a module-level registry rather than on `DownloadManager` because the //! two sides never meet: the command handler holds the manager's lock, while the //! worker runs detached inside `tauri::async_runtime::spawn` with no access to //! Tauri state. A registry both can reach is the smallest thing that works. //! //! [`Stopped`]: crate::download::worker::DownloadError::Stopped use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, OnceLock}; use crate::utils::lock::MutexSafe; /// download id → its stop flag, for downloads currently in flight. fn registry() -> &'static Mutex>> { static REGISTRY: OnceLock>>> = OnceLock::new(); REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) } /// Register `download_id` as in-flight and hand back its stop flag. /// /// Called by the worker as it starts. A previous flag for the same id is /// replaced, so a download that is paused and later resumed does not inherit the /// set flag from its last run and stop immediately. pub fn register(download_id: i64) -> Arc { let flag = Arc::new(AtomicBool::new(false)); registry().lock_safe().insert(download_id, flag.clone()); flag } /// Ask an in-flight download to stop. /// /// Returns whether one was actually in flight — the caller uses this to tell a /// running download (which will stop shortly) from a merely queued one (which /// the database update alone has already handled). pub fn signal(download_id: i64) -> bool { match registry().lock_safe().get(&download_id) { Some(flag) => { flag.store(true, Ordering::SeqCst); true } None => false, } } /// Forget a download's flag. Called when its task finishes, however it ended. pub fn clear(download_id: i64) { registry().lock_safe().remove(&download_id); } /// Whether a stop has been requested for `download_id`. /// /// The worker reads its own `Arc` directly rather than looking the id /// up, so this exists for the tests that assert the registry's behaviour. #[cfg(test)] pub fn is_stopping(download_id: i64) -> bool { registry() .lock_safe() .get(&download_id) .map(|f| f.load(Ordering::SeqCst)) .unwrap_or(false) } #[cfg(test)] mod tests { use super::*; /// Ids are per-test so the shared registry cannot leak between them. fn unique_id(seed: i64) -> i64 { 900_000 + seed } #[test] fn test_a_registered_download_starts_unflagged() { let id = unique_id(1); let flag = register(id); assert!(!flag.load(Ordering::SeqCst)); assert!(!is_stopping(id)); clear(id); } #[test] fn test_signal_sets_the_flag_the_worker_reads() { let id = unique_id(2); let flag = register(id); assert!(signal(id), "a registered download reports as in flight"); assert!( flag.load(Ordering::SeqCst), "the worker's own handle sees it" ); assert!(is_stopping(id)); clear(id); } /// The pump only needs to abort a task that exists; a queued row is handled /// by its database status alone. #[test] fn test_signalling_an_unregistered_download_reports_not_in_flight() { assert!(!signal(unique_id(3))); } #[test] fn test_clear_forgets_the_download() { let id = unique_id(4); register(id); signal(id); clear(id); assert!(!is_stopping(id)); assert!(!signal(id), "a cleared download is no longer in flight"); } /// The bug this guards: pause sets the flag, and resume re-runs the same /// download id. If registering reused the old flag, the resumed run would see /// a set flag and stop instantly — a download that could never be resumed. #[test] fn test_reregistering_clears_a_previous_stop() { let id = unique_id(5); register(id); signal(id); assert!(is_stopping(id)); let fresh = register(id); assert!(!fresh.load(Ordering::SeqCst)); assert!( !is_stopping(id), "a resumed download must not inherit the pause" ); clear(id); } }