QoL Download Manager (#108)

* feat: retry specific download errors

* fix: potential fix for cmd window on Windows

* feat: add disk space check for download

* fix: update game fix formatting

* fix: clippy
This commit is contained in:
DecDuck
2025-08-04 16:30:45 +10:00
committed by GitHub
parent 339d707092
commit 75a4b73ee1
10 changed files with 196 additions and 48 deletions

View File

@ -26,6 +26,7 @@ pub struct ProgressObject {
rolling: RollingProgressWindow<250>,
}
#[derive(Clone)]
pub struct ProgressHandle {
progress: Arc<AtomicUsize>,
progress_object: Arc<ProgressObject>,

View File

@ -4,6 +4,7 @@ use std::{
};
use serde_with::SerializeDisplay;
use humansize::{format_size, BINARY};
use super::remote_access_error::RemoteAccessError;
@ -11,6 +12,7 @@ use super::remote_access_error::RemoteAccessError;
#[derive(Debug, SerializeDisplay)]
pub enum ApplicationDownloadError {
Communication(RemoteAccessError),
DiskFull(u64, u64),
Checksum,
Lock,
IoError(io::ErrorKind),
@ -20,11 +22,25 @@ pub enum ApplicationDownloadError {
impl Display for ApplicationDownloadError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ApplicationDownloadError::DiskFull(required, available) => write!(
f,
"Game requires {}, {} remaining left on disk.",
format_size(*required, BINARY),
format_size(*available, BINARY),
),
ApplicationDownloadError::Communication(error) => write!(f, "{error}"),
ApplicationDownloadError::Lock => write!(f, "failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
ApplicationDownloadError::Checksum => write!(f, "checksum failed to validate for download"),
ApplicationDownloadError::Lock => write!(
f,
"failed to acquire lock. Something has gone very wrong internally. Please restart the application"
),
ApplicationDownloadError::Checksum => {
write!(f, "checksum failed to validate for download")
}
ApplicationDownloadError::IoError(error) => write!(f, "io error: {error}"),
ApplicationDownloadError::DownloadError => write!(f, "download failed. See Download Manager status for specific error"),
ApplicationDownloadError::DownloadError => write!(
f,
"download failed. See Download Manager status for specific error"
),
}
}
}

View File

@ -5,10 +5,10 @@ use std::{
use crate::{
database::{db::borrow_db_checked, models::data::GameDownloadStatus},
download_manager::{
download_manager_frontend::DownloadManagerSignal, downloadable::Downloadable,
},
error::download_manager_error::DownloadManagerError,
download_manager::
downloadable::Downloadable
,
error::application_download_error::ApplicationDownloadError,
AppState,
};
@ -20,26 +20,28 @@ pub fn download_game(
game_version: String,
install_dir: usize,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
) -> Result<(), ApplicationDownloadError> {
let sender = state.lock().unwrap().download_manager.get_sender();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new_from_index(
let game_download_agent = GameDownloadAgent::new_from_index(
game_id,
game_version,
install_dir,
sender,
)) as Box<dyn Downloadable + Send + Sync>);
Ok(state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent)?)
)?;
let game_download_agent = Arc::new(Box::new(game_download_agent) as Box<dyn Downloadable + Send + Sync>);
state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent).unwrap();
Ok(())
}
#[tauri::command]
pub fn resume_download(
game_id: String,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), DownloadManagerError<DownloadManagerSignal>> {
) -> Result<(), ApplicationDownloadError> {
let s = borrow_db_checked()
.applications
.game_statuses
@ -56,17 +58,21 @@ pub fn resume_download(
install_dir,
} => (version_name, install_dir),
};
let sender = state.lock().unwrap().download_manager.get_sender();
let parent_dir: PathBuf = install_dir.into();
let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new(
game_id,
version_name.clone(),
parent_dir.parent().unwrap().to_path_buf(),
sender,
)) as Box<dyn Downloadable + Send + Sync>);
Ok(state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent)?)
)?) as Box<dyn Downloadable + Send + Sync>);
state
.lock()
.unwrap()
.download_manager
.queue_download(game_download_agent).unwrap();
Ok(())
}

View File

@ -13,13 +13,12 @@ use crate::error::application_download_error::ApplicationDownloadError;
use crate::error::remote_access_error::RemoteAccessError;
use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
use crate::games::downloads::validate::validate_game_chunk;
use crate::games::library::{
on_game_complete, push_game_update, set_partially_installed,
};
use crate::games::library::{on_game_complete, push_game_update, set_partially_installed};
use crate::games::state::GameStatusManager;
use crate::process::utils::get_disk_available;
use crate::remote::requests::make_request;
use crate::remote::utils::DROP_CLIENT_SYNC;
use log::{debug, error, info};
use log::{debug, error, info, warn};
use rayon::ThreadPoolBuilder;
use std::collections::HashMap;
use std::fs::{OpenOptions, create_dir_all};
@ -35,6 +34,8 @@ use rustix::fs::{FallocateFlags, fallocate};
use super::download_logic::download_game_chunk;
use super::drop_data::DropData;
static RETRY_COUNT: usize = 3;
pub struct GameDownloadAgent {
pub id: String,
pub version: String,
@ -54,7 +55,7 @@ impl GameDownloadAgent {
version: String,
target_download_dir: usize,
sender: Sender<DownloadManagerSignal>,
) -> Self {
) -> Result<Self, ApplicationDownloadError> {
let db_lock = borrow_db_checked();
let base_dir = db_lock.applications.install_dirs[target_download_dir].clone();
drop(db_lock);
@ -66,7 +67,7 @@ impl GameDownloadAgent {
version: String,
base_dir: PathBuf,
sender: Sender<DownloadManagerSignal>,
) -> Self {
) -> Result<Self, ApplicationDownloadError> {
// Don't run by default
let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop);
@ -76,7 +77,7 @@ impl GameDownloadAgent {
let stored_manifest =
DropData::generate(id.clone(), version.clone(), data_base_dir_path.clone());
Self {
let result = Self {
id,
version,
control_flag,
@ -87,7 +88,31 @@ impl GameDownloadAgent {
sender,
dropdata: stored_manifest,
status: Mutex::new(DownloadStatus::Queued),
};
result.ensure_manifest_exists()?;
let required_space = result
.manifest
.lock()
.unwrap()
.as_ref()
.unwrap()
.values()
.map(|e| e.lengths.iter().sum::<usize>())
.sum::<usize>()
as u64;
let available_space = get_disk_available(data_base_dir_path)? as u64;
if required_space > available_space {
return Err(ApplicationDownloadError::DiskFull(
required_space,
available_space,
));
}
Ok(result)
}
// Blocking
@ -315,15 +340,40 @@ impl GameDownloadAgent {
};
scope.spawn(move |_| {
match download_game_chunk(context, &self.control_flag, progress_handle, request)
{
Ok(true) => {
completed_indexes.push(context.checksum.clone());
}
Ok(false) => {}
Err(e) => {
error!("{e}");
sender.send(DownloadManagerSignal::Error(e)).unwrap();
// 3 attempts
for i in 0..RETRY_COUNT {
let loop_progress_handle = progress_handle.clone();
match download_game_chunk(
context,
&self.control_flag,
loop_progress_handle,
request.try_clone().unwrap(),
) {
Ok(true) => {
completed_indexes.push(context.checksum.clone());
return;
}
Ok(false) => return,
Err(e) => {
warn!("game download agent error: {e}");
let retry = match &e {
ApplicationDownloadError::Communication(
_remote_access_error,
) => true,
ApplicationDownloadError::Checksum => true,
ApplicationDownloadError::Lock => true,
ApplicationDownloadError::IoError(_error_kind) => false,
ApplicationDownloadError::DownloadError => false,
ApplicationDownloadError::DiskFull(_, _) => false,
};
if i == RETRY_COUNT - 1 || !retry {
warn!("retry logic failed, not re-attempting.");
sender.send(DownloadManagerSignal::Error(e)).unwrap();
return;
}
}
}
}
});
@ -452,8 +502,6 @@ impl GameDownloadAgent {
);
self.dropdata.write();
}
}

View File

@ -1 +0,0 @@

View File

@ -1,6 +1,5 @@
pub mod commands;
#[cfg(target_os = "linux")]
pub mod compat;
pub mod process_manager;
pub mod process_handlers;
pub mod format;
pub mod format;
pub mod utils;

View File

@ -336,9 +336,9 @@ impl ProcessManager<'_> {
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
let mut command = Command::new("cmd");
let mut command = Command::new("start");
#[cfg(target_os = "windows")]
command.raw_arg(format!("/C \"{}\"", &launch_string));
command.raw_arg(format!("/min cmd /C \"{}\"", &launch_string));
info!("launching (in {install_dir}): {launch_string}",);

View File

@ -0,0 +1,27 @@
use std::path::PathBuf;
use futures_lite::io;
use sysinfo::{Disk, DiskRefreshKind, Disks};
use crate::error::application_download_error::ApplicationDownloadError;
pub fn get_disk_available(mount_point: PathBuf) -> Result<u64, ApplicationDownloadError> {
let disks = Disks::new_with_refreshed_list_specifics(DiskRefreshKind::nothing().with_storage());
let mut disk_iter = disks.into_iter().collect::<Vec<&Disk>>();
disk_iter.sort_by(|a, b| {
b.mount_point()
.to_string_lossy()
.len()
.cmp(&a.mount_point().to_string_lossy().len())
});
for disk in disk_iter {
if mount_point.starts_with(disk.mount_point()) {
return Ok(disk.available_space());
}
}
Err(ApplicationDownloadError::IoError(io::Error::other(
"could not find disk of path",
).kind()))
}