feat(downloads): add adaptive mirror reliability

This commit is contained in:
NimBold
2026-08-09 04:06:05 +03:30
parent c3755ce886
commit 3b7c454ec7
23 changed files with 1381 additions and 76 deletions
+137 -2
View File
@@ -3073,7 +3073,7 @@ fn push_unique_path(paths: &mut Vec<std::path::PathBuf>, path: std::path::PathBu
}
}
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Arc, Mutex, RwLock};
struct Aria2DaemonGuard {
@@ -3081,6 +3081,7 @@ struct Aria2DaemonGuard {
startup_error: Mutex<Option<String>>,
last_stderr: Mutex<String>,
config_path: Mutex<Option<tempfile::TempPath>>,
shutdown_state: AtomicU8,
}
impl Aria2DaemonGuard {
@@ -3090,8 +3091,63 @@ impl Aria2DaemonGuard {
startup_error: Mutex::new(None),
last_stderr: Mutex::new(String::new()),
config_path: Mutex::new(None),
shutdown_state: AtomicU8::new(0),
}
}
fn exit_allowed(&self) -> bool {
self.shutdown_state.load(Ordering::SeqCst) == 2
}
fn begin_shutdown(&self) -> bool {
self.shutdown_state
.compare_exchange(0, 1, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
}
fn allow_exit(&self) {
self.shutdown_state.store(2, Ordering::SeqCst);
}
}
async fn shutdown_aria2_daemon(app_handle: tauri::AppHandle) {
let guard = app_handle.state::<Aria2DaemonGuard>();
if let Some(state) = app_handle.try_state::<AppState>() {
let port = state.aria2_port.load(Ordering::Relaxed);
if port != 0 {
let shutdown = tokio::time::timeout(
std::time::Duration::from_secs(2),
rpc_call(port, &state.aria2_secret, "aria2.shutdown", serde_json::json!([])),
)
.await;
match shutdown {
Ok(Ok(_)) => log::info!("aria2 graceful shutdown requested"),
Ok(Err(error)) => log::warn!("aria2 graceful shutdown failed: {error}"),
Err(_) => log::warn!("aria2 graceful shutdown timed out"),
}
}
}
let child = guard.child.lock().ok().and_then(|mut child| child.take());
if let Some(mut child) = child {
let _ = tokio::task::spawn_blocking(move || {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
loop {
match child.try_wait() {
Ok(Some(_)) => return,
Ok(None) if std::time::Instant::now() < deadline => {
std::thread::sleep(std::time::Duration::from_millis(50));
}
_ => {
let _ = child.kill();
let _ = child.wait();
return;
}
}
}
})
.await;
}
}
impl Drop for Aria2DaemonGuard {
@@ -8762,6 +8818,9 @@ async fn verify_torrent_data(
mirrors: None,
user_agent: None,
max_tries: Some(0),
minimum_normal_download_speed_kib: None,
retry_not_found_errors: None,
adaptive_mirror_selection: None,
proxy: None,
format_selector: None,
cookie_source: None,
@@ -9240,6 +9299,19 @@ fn apply_aria2_torrent_dht_options(
command.arg(format!("--dht-message-timeout={timeout}"));
}
fn apply_aria2_server_stat_options(
command: &mut std::process::Command,
path: Option<&std::path::Path>,
) {
let Some(path) = path else {
return;
};
command
.arg(format!("--server-stat-if={}", path.display()))
.arg(format!("--server-stat-of={}", path.display()))
.arg("--server-stat-timeout=86400");
}
fn apply_aria2_torrent_peer_identity_options(
command: &mut std::process::Command,
peer_id_prefix: &str,
@@ -10741,6 +10813,7 @@ mod tests {
apply_aria2_torrent_peer_discovery_options,
apply_aria2_torrent_dht_paths,
apply_aria2_torrent_dht_options,
apply_aria2_server_stat_options,
aria2_rpc_port_is_occupied,
parse_firelink_deep_link, parse_ffmpeg_version, parse_media_progress_line,
collect_opened_torrent_paths,
@@ -10762,6 +10835,7 @@ mod tests {
retained_torrent_id_from_persisted_record,
retained_torrent_info_hash_from_persisted_record,
merge_durable_torrent_telemetry, torrent_identity_magnet, torrent_move_path_pair,
Aria2DaemonGuard,
};
#[cfg(target_os = "macos")]
use super::should_apply_dock_badge_update;
@@ -11013,6 +11087,42 @@ mod tests {
);
}
#[test]
fn aria2_adaptive_mirror_history_is_private_and_launch_scoped() {
let root = tempfile::tempdir().unwrap();
let path = root.path().join("server-stat.txt");
let mut command = std::process::Command::new("aria2c");
apply_aria2_server_stat_options(&mut command, Some(&path));
assert_eq!(
command
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect::<Vec<_>>(),
vec![
format!("--server-stat-if={}", path.display()),
format!("--server-stat-of={}", path.display()),
"--server-stat-timeout=86400".to_string(),
]
);
let mut disabled = std::process::Command::new("aria2c");
apply_aria2_server_stat_options(&mut disabled, None);
assert_eq!(disabled.get_args().count(), 0);
}
#[test]
fn aria2_shutdown_blocks_repeated_exit_requests_until_cleanup_finishes() {
let guard = Aria2DaemonGuard::new();
assert!(!guard.exit_allowed());
assert!(guard.begin_shutdown());
assert!(!guard.begin_shutdown());
assert!(!guard.exit_allowed());
guard.allow_exit();
assert!(guard.exit_allowed());
assert!(!guard.begin_shutdown());
}
#[test]
fn aria2_torrent_global_options_are_bounded_and_explicit() {
let mut command = std::process::Command::new("aria2c");
@@ -13765,6 +13875,15 @@ pub fn run() {
// conflict must fail startup; silently allowing Aria2 to fall back
// to a user-global dht.dat would escape the storage boundary.
let aria2_dht_paths = storage_layout.prepare_aria2_dht_paths()?;
let aria2_server_stat_path = match storage_layout.prepare_aria2_server_stat_path() {
Ok(path) => Some(path),
Err(error) => {
log::warn!(
"adaptive mirror history is disabled for this session: {error}"
);
None
}
};
if let Err(error) = crate::torrent::remove_orphaned_probe_dirs(app.handle()) {
log::warn!("could not remove orphaned torrent probes: {error}");
}
@@ -14045,6 +14164,10 @@ pub fn run() {
&mut cmd,
torrent_startup_settings.dht_message_timeout,
);
apply_aria2_server_stat_options(
&mut cmd,
aria2_server_stat_path.as_deref(),
);
apply_aria2_torrent_peer_discovery_options(
&mut cmd,
@@ -15008,9 +15131,21 @@ pub fn run() {
restore_main_window(app_handle);
}
}
tauri::RunEvent::ExitRequested { .. } => {
tauri::RunEvent::ExitRequested { code, api, .. } => {
let state = app_handle.state::<AppState>();
let _ = state.extension_server_shutdown.send(true);
let guard = app_handle.state::<Aria2DaemonGuard>();
if !guard.exit_allowed() {
api.prevent_exit();
if guard.begin_shutdown() {
let app = app_handle.clone();
tauri::async_runtime::spawn(async move {
shutdown_aria2_daemon(app.clone()).await;
app.state::<Aria2DaemonGuard>().allow_exit();
app.exit(code.unwrap_or(0));
});
}
}
}
_ => {}
});