From 807e16a1fe8e9314f6180bafa6cabd38e1057beb Mon Sep 17 00:00:00 2001 From: NimBold Date: Sun, 9 Aug 2026 04:56:06 +0330 Subject: [PATCH] fix(downloads): allow aria2 graceful shutdown delay --- scripts/smoke-aria2-transfers.js | 7 +- src-tauri/src/lib.rs | 30 +++---- src-tauri/src/storage.rs | 133 ------------------------------- 3 files changed, 14 insertions(+), 156 deletions(-) diff --git a/scripts/smoke-aria2-transfers.js b/scripts/smoke-aria2-transfers.js index cc429bd..6f66a23 100644 --- a/scripts/smoke-aria2-transfers.js +++ b/scripts/smoke-aria2-transfers.js @@ -153,7 +153,7 @@ function childExited(child) { return child.exitCode !== null || child.signalCode !== null; } -async function waitForChildExit(child, timeoutMs = 3000) { +async function waitForChildExit(child, timeoutMs = 8000) { if (childExited(child)) return true; return new Promise(resolve => { let settled = false; @@ -216,7 +216,6 @@ const payload = Buffer.alloc(4 * 1024 * 1024, 0x5a); const checksum = crypto.createHash('sha256').update(payload).digest('hex'); const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'firelink-aria2-transfers-')); const serverStatPath = path.join(tempRoot, 'server-stat.txt'); -const serverStatOutputPath = path.join(tempRoot, 'server-stat.next'); fs.writeFileSync(serverStatPath, '', { mode: 0o600 }); let finalRequests = 0; let finalCredentials = []; @@ -322,7 +321,7 @@ const child = spawn(binaryPath, [ '--console-log-level=error', '--quiet=true', `--server-stat-if=${serverStatPath}`, - `--server-stat-of=${serverStatOutputPath}`, + `--server-stat-of=${serverStatPath}`, ], { env: environment, stdio: ['ignore', 'ignore', 'pipe'] }); let stderr = ''; child.stderr.on('data', chunk => { stderr += chunk.toString(); }); @@ -463,7 +462,7 @@ try { try { await stop(child, rpcPort, secret); if (smokePassed) { - const stat = fs.readFileSync(serverStatOutputPath, 'utf8'); + const stat = fs.readFileSync(serverStatPath, 'utf8'); if (!stat.includes('host=127.0.0.1')) { throw new Error(`Aria2 did not persist adaptive mirror statistics: ${JSON.stringify(stat)}`); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 27561f0..75c3c14 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -3131,7 +3131,10 @@ async fn shutdown_aria2_daemon(app_handle: tauri::AppHandle) { 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); + // Aria2 1.37.0 intentionally schedules an RPC-requested shutdown + // three seconds after replying. Leave a bounded margin for that + // timer and the subsequent state flush before forcing termination. + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(8); loop { match child.try_wait() { Ok(Some(_)) => return, @@ -3148,11 +3151,6 @@ async fn shutdown_aria2_daemon(app_handle: tauri::AppHandle) { }) .await; } - if let Some(state) = app_handle.try_state::() { - if let Err(error) = state.storage_layout.promote_aria2_server_stat_output() { - log::warn!("adaptive mirror history could not be promoted: {error}"); - } - } } impl Drop for Aria2DaemonGuard { @@ -9306,15 +9304,14 @@ fn apply_aria2_torrent_dht_options( fn apply_aria2_server_stat_options( command: &mut std::process::Command, - input_path: Option<&std::path::Path>, - output_path: Option<&std::path::Path>, + path: Option<&std::path::Path>, ) { - let (Some(input_path), Some(output_path)) = (input_path, output_path) else { + let Some(path) = path else { return; }; command - .arg(format!("--server-stat-if={}", input_path.display())) - .arg(format!("--server-stat-of={}", output_path.display())) + .arg(format!("--server-stat-if={}", path.display())) + .arg(format!("--server-stat-of={}", path.display())) .arg("--server-stat-timeout=86400"); } @@ -11097,9 +11094,8 @@ mod tests { fn aria2_adaptive_mirror_history_is_private_and_launch_scoped() { let root = tempfile::tempdir().unwrap(); let path = root.path().join("server-stat.txt"); - let output_path = root.path().join("server-stat.next"); let mut command = std::process::Command::new("aria2c"); - apply_aria2_server_stat_options(&mut command, Some(&path), Some(&output_path)); + apply_aria2_server_stat_options(&mut command, Some(&path)); assert_eq!( command .get_args() @@ -11107,13 +11103,13 @@ mod tests { .collect::>(), vec![ format!("--server-stat-if={}", path.display()), - format!("--server-stat-of={}", output_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, None); + apply_aria2_server_stat_options(&mut disabled, None); assert_eq!(disabled.get_args().count(), 0); } @@ -13891,9 +13887,6 @@ pub fn run() { None } }; - let aria2_server_stat_output_path = aria2_server_stat_path - .as_ref() - .map(|_| storage_layout.aria2_server_stat_output_path()); if let Err(error) = crate::torrent::remove_orphaned_probe_dirs(app.handle()) { log::warn!("could not remove orphaned torrent probes: {error}"); } @@ -14177,7 +14170,6 @@ pub fn run() { apply_aria2_server_stat_options( &mut cmd, aria2_server_stat_path.as_deref(), - aria2_server_stat_output_path.as_deref(), ); apply_aria2_torrent_peer_discovery_options( diff --git a/src-tauri/src/storage.rs b/src-tauri/src/storage.rs index 9a8061a..4c82bde 100644 --- a/src-tauri/src/storage.rs +++ b/src-tauri/src/storage.rs @@ -9,7 +9,6 @@ const ARIA2_DATA_DIR: &str = "aria2"; const ARIA2_DHT_FILE: &str = "dht.dat"; const ARIA2_DHT6_FILE: &str = "dht6.dat"; const ARIA2_SERVER_STAT_FILE: &str = "server-stat.txt"; -const ARIA2_SERVER_STAT_OUTPUT_FILE: &str = "server-stat.next"; const MAX_ARIA2_SERVER_STAT_BYTES: u64 = 1024 * 1024; #[derive(Debug, Clone, PartialEq, Eq)] @@ -125,12 +124,6 @@ impl StorageLayout { .join(ARIA2_SERVER_STAT_FILE) } - pub fn aria2_server_stat_output_path(&self) -> PathBuf { - self.data_dir - .join(ARIA2_DATA_DIR) - .join(ARIA2_SERVER_STAT_OUTPUT_FILE) - } - /// Create and validate only Firelink's Aria2 state directory. Aria2 owns /// the table contents; Firelink owns this exact location and must never /// fall back to a user-global default when it cannot establish it. @@ -233,100 +226,8 @@ impl StorageLayout { std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) .map_err(|error| format!("failed to protect Aria2 server-stat cache: {error}"))?; } - let output_path = self.aria2_server_stat_output_path(); - match std::fs::symlink_metadata(&output_path) { - Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => { - return Err("Aria2 server-stat session output is not a regular file".to_string()); - } - Ok(_) => std::fs::remove_file(&output_path).map_err(|error| { - format!("failed to reset Aria2 server-stat session output: {error}") - })?, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => { - return Err(format!( - "failed to inspect Aria2 server-stat session output: {error}" - )); - } - } - Ok(path) } - - /// Promote Aria2's session-only output after the daemon has exited. Keeping - /// the input and output paths distinct avoids the locked MSVC engine's - /// inability to replace an existing destination with C `rename`. - pub fn promote_aria2_server_stat_output(&self) -> Result<(), String> { - let path = self.aria2_server_stat_path(); - let output_path = self.aria2_server_stat_output_path(); - let directory = self.data_dir.join(ARIA2_DATA_DIR); - if crate::path_has_symlink_component(&directory) { - return Err("Aria2 server-stat directory contains a symlink".to_string()); - } - - let output_metadata = match std::fs::symlink_metadata(&output_path) { - Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => { - return Err("Aria2 server-stat session output is not a regular file".to_string()); - } - Ok(metadata) => metadata, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), - Err(error) => { - return Err(format!( - "failed to inspect Aria2 server-stat session output: {error}" - )); - } - }; - let output = if output_metadata.len() <= MAX_ARIA2_SERVER_STAT_BYTES { - std::fs::read_to_string(&output_path).ok() - } else { - None - }; - if !output - .as_deref() - .is_some_and(aria2_server_stat_is_valid) - { - std::fs::remove_file(&output_path).map_err(|error| { - format!("failed to discard invalid Aria2 server-stat session output: {error}") - })?; - return Ok(()); - } - - match std::fs::symlink_metadata(&path) { - Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => { - return Err( - "Aria2 server-stat cache replacement target is not a regular file".to_string(), - ); - } - Ok(_) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} - Err(error) => { - return Err(format!( - "failed to inspect Aria2 server-stat replacement target: {error}" - )); - } - } - - std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&path) - .and_then(|mut file| { - use std::io::Write; - file.write_all(output.as_deref().unwrap_or_default().as_bytes())?; - file.sync_all() - }) - .map_err(|error| { - format!("failed to promote Aria2 server-stat session output: {error}") - })?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) - .map_err(|error| format!("failed to protect Aria2 server-stat cache: {error}"))?; - } - std::fs::remove_file(&output_path) - .map_err(|error| format!("failed to remove Aria2 server-stat session output: {error}")) - } } fn aria2_server_stat_is_valid(contents: &str) -> bool { @@ -494,34 +395,6 @@ mod tests { } } - #[test] - fn aria2_server_stat_output_is_promoted_only_after_validation() { - let root = TempDir::new().unwrap(); - let layout = test_layout(root.path()); - layout.prepare_aria2_dht_paths().unwrap(); - let path = layout.prepare_aria2_server_stat_path().unwrap(); - let output_path = layout.aria2_server_stat_output_path(); - fs::write(&path, "").unwrap(); - fs::write( - &output_path, - "host=mirror.example, protocol=https, dl_speed=1, last_updated=1, status=OK\n", - ) - .unwrap(); - - layout.promote_aria2_server_stat_output().unwrap(); - assert!(fs::read_to_string(&path) - .unwrap() - .contains("host=mirror.example")); - assert!(!output_path.exists()); - - fs::write(&output_path, "invalid\n").unwrap(); - layout.promote_aria2_server_stat_output().unwrap(); - assert!(fs::read_to_string(&path) - .unwrap() - .contains("host=mirror.example")); - assert!(!output_path.exists()); - } - #[cfg(unix)] #[test] fn aria2_server_stat_cache_rejects_symlink_output() { @@ -538,12 +411,6 @@ mod tests { .unwrap(); assert!(layout.prepare_aria2_server_stat_path().is_err()); - fs::write( - layout.aria2_server_stat_output_path(), - "host=mirror.example, protocol=https, dl_speed=1, last_updated=1, status=OK\n", - ) - .unwrap(); - assert!(layout.promote_aria2_server_stat_output().is_err()); } #[cfg(unix)]