From dbe8c8df4d6313b7ca85133a169faebab299f2b9 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sat, 2 Aug 2025 20:17:27 +1000 Subject: [PATCH] Process manager templating & game importing (#96) * feat: add new template options, asahi support, and refactoring * feat: install dir scanning, validation fixes, progress fixes, download manager refactor This kind of ballooned out of scope, but I implemented some much needed fixes for the download manager. First off, I cleanup the Downloadable trait, there was some duplication of function. Second, I refactored the "validate" into the GameDownloadAgent, which calls a 'validate_chunk_logic' yada, same structure as downloading. Third, I fixed the progress and validation issues. Fourth, I added game scanning * feat: out of box support for Asahi Linux * fix: clippy * fix: don't break database --- app.vue | 1 + components/GameStatusButton.vue | 118 ++++++---- components/LibrarySearch.vue | 4 +- drop-base | 2 +- package.json | 2 +- pages/library/[id]/index.vue | 7 + src-tauri/Cargo.lock | 13 +- src-tauri/Cargo.toml | 3 +- src-tauri/src/database/commands.rs | 4 +- src-tauri/src/database/db.rs | 30 +-- src-tauri/src/database/mod.rs | 1 + src-tauri/src/database/models.rs | 45 ++-- src-tauri/src/database/scan.rs | 52 +++++ .../download_manager_builder.rs | 6 +- .../src/download_manager/downloadable.rs | 4 +- .../util/download_thread_control_flag.rs | 4 +- .../download_manager/util/progress_object.rs | 18 +- .../util/rolling_progress_updates.rs | 2 +- src-tauri/src/error/process_error.rs | 2 - src-tauri/src/games/commands.rs | 15 +- .../src/games/downloads/download_agent.rs | 207 ++++++++++++----- src-tauri/src/games/downloads/drop_data.rs | 37 +-- src-tauri/src/games/downloads/mod.rs | 2 +- src-tauri/src/games/downloads/validate.rs | 93 +------- src-tauri/src/games/library.rs | 210 +++++++++--------- src-tauri/src/games/state.rs | 15 +- src-tauri/src/lib.rs | 33 +++ src-tauri/src/process/commands.rs | 2 +- src-tauri/src/process/format.rs | 33 +++ src-tauri/src/process/mod.rs | 2 + src-tauri/src/process/process_handlers.rs | 109 +++++++++ src-tauri/src/process/process_manager.rs | 149 +++++++------ src-tauri/src/remote/cache.rs | 9 +- src-tauri/tauri.conf.json | 2 +- tsconfig.json | 3 +- types.ts | 3 +- 36 files changed, 764 insertions(+), 478 deletions(-) create mode 100644 src-tauri/src/database/scan.rs create mode 100644 src-tauri/src/process/format.rs create mode 100644 src-tauri/src/process/process_handlers.rs diff --git a/app.vue b/app.vue index dce71d9..46a38ab 100644 --- a/app.vue +++ b/app.vue @@ -27,6 +27,7 @@ try { console.error("failed to parse state", e); } +// This is inefficient but apparently we do it lol router.beforeEach(async () => { try { state.value = JSON.parse(await invoke("fetch_state")); diff --git a/components/GameStatusButton.vue b/components/GameStatusButton.vue index 65b0ab1..1dea40b 100644 --- a/components/GameStatusButton.vue +++ b/components/GameStatusButton.vue @@ -1,52 +1,78 @@ + Result<(), DownloadManagerError<()> lock.applications.install_dirs.push(new_dir); drop(lock); + scan_install_dirs(); + Ok(()) } diff --git a/src-tauri/src/database/db.rs b/src-tauri/src/database/db.rs index 39229c8..bc08f7a 100644 --- a/src-tauri/src/database/db.rs +++ b/src-tauri/src/database/db.rs @@ -10,7 +10,7 @@ use chrono::Utc; use log::{debug, error, info, warn}; use native_model::{Decode, Encode}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use url::Url; use crate::DB; @@ -67,17 +67,18 @@ impl DatabaseImpls for DatabaseInterface { let exists = fs::exists(db_path.clone()).unwrap(); - if exists { match PathDatabase::load_from_path(db_path.clone()) { - Ok(db) => db, - Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir), - } } else { + if exists { + match PathDatabase::load_from_path(db_path.clone()) { + Ok(db) => db, + Err(e) => handle_invalid_database(e, db_path, games_base_dir, cache_dir), + } + } else { let default = Database::new(games_base_dir, None, cache_dir); debug!( "Creating database at path {}", db_path.as_os_str().to_str().unwrap() ); - PathDatabase::create_at_path(db_path, default) - .expect("Database could not be created") + PathDatabase::create_at_path(db_path, default).expect("Database could not be created") } } @@ -121,24 +122,24 @@ fn handle_invalid_database( pub struct DBRead<'a>(RwLockReadGuard<'a, Database>); pub struct DBWrite<'a>(ManuallyDrop>); impl<'a> Deref for DBWrite<'a> { - type Target = RwLockWriteGuard<'a, Database>; + type Target = Database; fn deref(&self) -> &Self::Target { &self.0 } } +impl<'a> DerefMut for DBWrite<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} impl<'a> Deref for DBRead<'a> { - type Target = RwLockReadGuard<'a, Database>; + type Target = Database; fn deref(&self) -> &Self::Target { &self.0 } } -impl DerefMut for DBWrite<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} impl Drop for DBWrite<'_> { fn drop(&mut self) { unsafe { @@ -154,6 +155,7 @@ impl Drop for DBWrite<'_> { } } } + pub fn borrow_db_checked<'a>() -> DBRead<'a> { match DB.borrow_data() { Ok(data) => DBRead(data), diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index edc3061..6c77e25 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -2,3 +2,4 @@ pub mod commands; pub mod db; pub mod debug; pub mod models; +pub mod scan; \ No newline at end of file diff --git a/src-tauri/src/database/models.rs b/src-tauri/src/database/models.rs index b039c9b..51c3004 100644 --- a/src-tauri/src/database/models.rs +++ b/src-tauri/src/database/models.rs @@ -1,8 +1,14 @@ -use crate::database::models::data::Database; - +/** + * NEXT BREAKING CHANGE + * + * UPDATE DATABASE TO USE RPMSERDENAMED + * + * WE CAN'T DELETE ANY FIELDS + */ pub mod data { use std::path::PathBuf; + use native_model::native_model; use serde::{Deserialize, Serialize}; @@ -18,16 +24,14 @@ pub mod data { pub type DatabaseApplications = v2::DatabaseApplications; pub type DatabaseCompatInfo = v2::DatabaseCompatInfo; - use std::{collections::HashMap, process::Command}; - - use crate::process::process_manager::UMU_LAUNCHER_EXECUTABLE; + use std::collections::HashMap; pub mod v1 { use crate::process::process_manager::Platform; use serde_with::serde_as; use std::{collections::HashMap, path::PathBuf}; - use super::{Serialize, Deserialize, native_model}; + use super::{Deserialize, Serialize, native_model}; fn default_template() -> String { "{}".to_owned() @@ -115,6 +119,7 @@ pub mod data { Downloading { version_name: String }, Uninstalling {}, Updating { version_name: String }, + Validating { version_name: String }, Running {}, } @@ -174,7 +179,10 @@ pub mod data { use serde_with::serde_as; - use super::{Serialize, Deserialize, native_model, Settings, DatabaseAuth, v1, GameVersion, DownloadableMetadata, ApplicationTransientStatus}; + use super::{ + ApplicationTransientStatus, DatabaseAuth, Deserialize, DownloadableMetadata, + GameVersion, Serialize, Settings, native_model, v1, + }; #[native_model(id = 1, version = 2, with = native_model::rmp_serde_1_3::RmpSerde)] #[derive(Serialize, Deserialize, Clone, Default)] @@ -206,7 +214,7 @@ pub mod data { applications: value.applications, prev_database: value.prev_database, cache_dir: value.cache_dir, - compat_info: crate::database::models::Database::create_new_compat_info(), + compat_info: None, } } } @@ -283,7 +291,10 @@ pub mod data { mod v3 { use std::path::PathBuf; - use super::{Serialize, Deserialize, native_model, Settings, DatabaseAuth, DatabaseApplications, DatabaseCompatInfo, v2}; + use super::{ + DatabaseApplications, DatabaseAuth, DatabaseCompatInfo, Deserialize, Serialize, + Settings, native_model, v2, + }; #[native_model(id = 1, version = 3, with = native_model::rmp_serde_1_3::RmpSerde)] #[derive(Serialize, Deserialize, Clone, Default)] pub struct Database { @@ -297,6 +308,7 @@ pub mod data { pub cache_dir: PathBuf, pub compat_info: Option, } + impl From for Database { fn from(value: v2::Database) -> Self { Self { @@ -306,22 +318,13 @@ pub mod data { applications: value.applications.into(), prev_database: value.prev_database, cache_dir: value.cache_dir, - compat_info: Database::create_new_compat_info(), + compat_info: None, } } } } + impl Database { - fn create_new_compat_info() -> Option { - #[cfg(target_os = "windows")] - return None; - - let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE).spawn().is_ok(); - Some(DatabaseCompatInfo { - umu_installed: has_umu_installed, - }) - } - pub fn new>( games_base_dir: T, prev_database: Option, @@ -340,7 +343,7 @@ pub mod data { auth: None, settings: Settings::default(), cache_dir, - compat_info: Database::create_new_compat_info(), + compat_info: None, } } } diff --git a/src-tauri/src/database/scan.rs b/src-tauri/src/database/scan.rs new file mode 100644 index 0000000..326aee5 --- /dev/null +++ b/src-tauri/src/database/scan.rs @@ -0,0 +1,52 @@ +use std::fs; + +use log::warn; + +use crate::{ + database::{ + db::borrow_db_mut_checked, + models::data::v1::{DownloadType, DownloadableMetadata}, + }, + games::{ + downloads::drop_data::{v1::DropData, DROP_DATA_PATH}, + library::set_partially_installed_db, + }, +}; + +pub fn scan_install_dirs() { + let mut db_lock = borrow_db_mut_checked(); + for install_dir in db_lock.applications.install_dirs.clone() { + let Ok(files) = fs::read_dir(install_dir) else { + continue; + }; + for game in files.into_iter().flatten() { + let drop_data_file = game.path().join(DROP_DATA_PATH); + if !drop_data_file.exists() { + continue; + } + let game_id = game.file_name().into_string().unwrap(); + let Ok(drop_data) = DropData::read(&game.path()) else { + warn!( + ".dropdata exists for {}, but couldn't read it. is it corrupted?", + game.file_name().into_string().unwrap() + ); + continue; + }; + if db_lock.applications.game_statuses.contains_key(&game_id) { + continue; + } + + let metadata = DownloadableMetadata::new( + drop_data.game_id, + Some(drop_data.game_version), + DownloadType::Game, + ); + set_partially_installed_db( + &mut db_lock, + &metadata, + drop_data.base_path.to_str().unwrap().to_string(), + None, + ); + } + } +} diff --git a/src-tauri/src/download_manager/download_manager_builder.rs b/src-tauri/src/download_manager/download_manager_builder.rs index 54c2d88..7d8aeed 100644 --- a/src-tauri/src/download_manager/download_manager_builder.rs +++ b/src-tauri/src/download_manager/download_manager_builder.rs @@ -254,13 +254,13 @@ impl DownloadManagerBuilder { } }; - // If the download gets cancel + // If the download gets cancelled + // immediately return, on_cancelled gets called for us earlier if !download_result { - download_agent.on_incomplete(&app_handle); return; } - let validate_result = match download_agent.validate() { + let validate_result = match download_agent.validate(&app_handle) { Ok(v) => v, Err(e) => { error!( diff --git a/src-tauri/src/download_manager/downloadable.rs b/src-tauri/src/download_manager/downloadable.rs index fff8011..9a74077 100644 --- a/src-tauri/src/download_manager/downloadable.rs +++ b/src-tauri/src/download_manager/downloadable.rs @@ -14,14 +14,14 @@ use super::{ pub trait Downloadable: Send + Sync { fn download(&self, app_handle: &AppHandle) -> Result; + fn validate(&self, app_handle: &AppHandle) -> Result; + fn progress(&self) -> Arc; fn control_flag(&self) -> DownloadThreadControl; - fn validate(&self) -> Result; fn status(&self) -> DownloadStatus; fn metadata(&self) -> DownloadableMetadata; fn on_initialised(&self, app_handle: &AppHandle); fn on_error(&self, app_handle: &AppHandle, error: &ApplicationDownloadError); fn on_complete(&self, app_handle: &AppHandle); - fn on_incomplete(&self, app_handle: &AppHandle); fn on_cancelled(&self, app_handle: &AppHandle); } diff --git a/src-tauri/src/download_manager/util/download_thread_control_flag.rs b/src-tauri/src/download_manager/util/download_thread_control_flag.rs index 247ab58..222f5e8 100644 --- a/src-tauri/src/download_manager/util/download_thread_control_flag.rs +++ b/src-tauri/src/download_manager/util/download_thread_control_flag.rs @@ -38,9 +38,9 @@ impl DownloadThreadControl { } } pub fn get(&self) -> DownloadThreadControlFlag { - self.inner.load(Ordering::Relaxed).into() + self.inner.load(Ordering::Acquire).into() } pub fn set(&self, flag: DownloadThreadControlFlag) { - self.inner.store(flag.into(), Ordering::Relaxed); + self.inner.store(flag.into(), Ordering::Release); } } diff --git a/src-tauri/src/download_manager/util/progress_object.rs b/src-tauri/src/download_manager/util/progress_object.rs index 4953704..9f67db5 100644 --- a/src-tauri/src/download_manager/util/progress_object.rs +++ b/src-tauri/src/download_manager/util/progress_object.rs @@ -39,20 +39,20 @@ impl ProgressHandle { } } pub fn set(&self, amount: usize) { - self.progress.store(amount, Ordering::Relaxed); + self.progress.store(amount, Ordering::Release); } pub fn add(&self, amount: usize) { self.progress - .fetch_add(amount, std::sync::atomic::Ordering::Relaxed); + .fetch_add(amount, std::sync::atomic::Ordering::AcqRel); calculate_update(&self.progress_object); } pub fn skip(&self, amount: usize) { self.progress - .fetch_add(amount, std::sync::atomic::Ordering::Relaxed); + .fetch_add(amount, std::sync::atomic::Ordering::Acquire); // Offset the bytes at last offset by this amount self.progress_object .bytes_last_update - .fetch_add(amount, Ordering::Relaxed); + .fetch_add(amount, Ordering::Acquire); // Dont' fire update } } @@ -60,7 +60,6 @@ impl ProgressHandle { impl ProgressObject { pub fn new(max: usize, length: usize, sender: Sender) -> Self { let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect()); - // TODO: consolidate this calculation with the set_max function below Self { max: Arc::new(Mutex::new(max)), progress_instances: Arc::new(arr), @@ -81,19 +80,18 @@ impl ProgressObject { .lock() .unwrap() .iter() - .map(|instance| instance.load(Ordering::Relaxed)) + .map(|instance| instance.load(Ordering::Acquire)) .sum() } - pub fn reset(&self, size: usize) { + pub fn reset(&self) { self.set_time_now(); - self.set_size(size); self.bytes_last_update.store(0, Ordering::Release); self.rolling.reset(); self.progress_instances .lock() .unwrap() .iter() - .for_each(|x| x.store(0, Ordering::Release)); + .for_each(|x| x.store(0, Ordering::SeqCst)); } pub fn get_max(&self) -> usize { *self.max.lock().unwrap() @@ -127,7 +125,7 @@ pub fn calculate_update(progress: &ProgressObject) { let max = progress.get_max(); let bytes_at_last_update = progress .bytes_last_update - .swap(current_bytes_downloaded, Ordering::Relaxed); + .swap(current_bytes_downloaded, Ordering::Acquire); let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update; diff --git a/src-tauri/src/download_manager/util/rolling_progress_updates.rs b/src-tauri/src/download_manager/util/rolling_progress_updates.rs index 420f601..fadae99 100644 --- a/src-tauri/src/download_manager/util/rolling_progress_updates.rs +++ b/src-tauri/src/download_manager/util/rolling_progress_updates.rs @@ -26,7 +26,7 @@ impl RollingProgressWindow { .iter() .enumerate() .filter(|(i, _)| i < ¤t) - .map(|(_, x)| x.load(Ordering::Relaxed)) + .map(|(_, x)| x.load(Ordering::Acquire)) .sum::() / S } diff --git a/src-tauri/src/error/process_error.rs b/src-tauri/src/error/process_error.rs index 0cda82f..fa4ddb5 100644 --- a/src-tauri/src/error/process_error.rs +++ b/src-tauri/src/error/process_error.rs @@ -6,7 +6,6 @@ use serde_with::SerializeDisplay; pub enum ProcessError { NotInstalled, AlreadyRunning, - NotDownloaded, InvalidID, InvalidVersion, IOError(Error), @@ -20,7 +19,6 @@ impl Display for ProcessError { let s = match self { ProcessError::NotInstalled => "Game not installed", ProcessError::AlreadyRunning => "Game already running", - ProcessError::NotDownloaded => "Game not downloaded", ProcessError::InvalidID => "Invalid game ID", ProcessError::InvalidVersion => "Invalid game version", ProcessError::IOError(error) => &error.to_string(), diff --git a/src-tauri/src/games/commands.rs b/src-tauri/src/games/commands.rs index 8a95132..c26e4c6 100644 --- a/src-tauri/src/games/commands.rs +++ b/src-tauri/src/games/commands.rs @@ -3,19 +3,23 @@ use std::sync::Mutex; use tauri::AppHandle; use crate::{ - database::models::data::GameVersion, + AppState, + database::{ + db::borrow_db_checked, + models::data::GameVersion, + }, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{ fetch_game_logic_offline, fetch_library_logic_offline, get_current_meta, uninstall_game_logic, }, - offline, AppState, + offline, }; use super::{ library::{ - fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct, - Game, + FetchGameStruct, Game, fetch_game_logic, fetch_game_verion_options_logic, + fetch_library_logic, }, state::{GameStatusManager, GameStatusWithTransient}, }; @@ -48,7 +52,8 @@ pub fn fetch_game( #[tauri::command] pub fn fetch_game_status(id: String) -> GameStatusWithTransient { - GameStatusManager::fetch_state(&id) + let db_handle = borrow_db_checked(); + GameStatusManager::fetch_state(&id, &db_handle) } #[tauri::command] diff --git a/src-tauri/src/games/downloads/download_agent.rs b/src-tauri/src/games/downloads/download_agent.rs index 0f4912b..3ace071 100644 --- a/src-tauri/src/games/downloads/download_agent.rs +++ b/src-tauri/src/games/downloads/download_agent.rs @@ -12,8 +12,11 @@ use crate::download_manager::util::progress_object::{ProgressHandle, ProgressObj 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::game_validate_logic; -use crate::games::library::{on_game_complete, on_game_incomplete, push_game_update}; +use crate::games::downloads::validate::validate_game_chunk; +use crate::games::library::{ + on_game_complete, push_game_update, set_partially_installed, +}; +use crate::games::state::GameStatusManager; use crate::remote::requests::make_request; use crate::remote::utils::DROP_CLIENT_SYNC; use log::{debug, error, info}; @@ -41,7 +44,7 @@ pub struct GameDownloadAgent { pub manifest: Mutex>, pub progress: Arc, sender: Sender, - pub stored_manifest: DropData, + pub dropdata: DropData, status: Mutex, } @@ -82,38 +85,43 @@ impl GameDownloadAgent { context_map: Mutex::new(HashMap::new()), progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), sender, - stored_manifest, + dropdata: stored_manifest, status: Mutex::new(DownloadStatus::Queued), } } // Blocking - pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> { + pub fn setup_download(&self, app_handle: &AppHandle) -> Result<(), ApplicationDownloadError> { self.ensure_manifest_exists()?; self.ensure_contexts()?; self.control_flag.set(DownloadThreadControlFlag::Go); + let mut db_lock = borrow_db_mut_checked(); + db_lock.applications.transient_statuses.insert( + self.metadata(), + ApplicationTransientStatus::Downloading { + version_name: self.version.clone(), + }, + ); + push_game_update( + app_handle, + &self.metadata().id, + None, + GameStatusManager::fetch_state(&self.metadata().id, &db_lock), + ); + Ok(()) } // Blocking pub fn download(&self, app_handle: &AppHandle) -> Result { - self.setup_download()?; - self.set_progress_object_params(); + self.setup_download(app_handle)?; let timer = Instant::now(); - push_game_update( - app_handle, - &self.metadata().id, - None, - ( - None, - Some(ApplicationTransientStatus::Downloading { - version_name: self.version.clone(), - }), - ), - ); + + info!("beginning download for {}...", self.metadata().id); + let res = self .run() .map_err(|()| ApplicationDownloadError::DownloadError); @@ -166,12 +174,8 @@ impl GameDownloadAgent { Err(ApplicationDownloadError::Lock) } - fn set_progress_object_params(&self) { - // Avoid re-setting it - if self.progress.get_max() != 0 { - return; - } - + // Sets it up for both download and validate + fn setup_progress(&self) { let contexts = self.contexts.lock().unwrap(); let length = contexts.len(); @@ -180,7 +184,7 @@ impl GameDownloadAgent { self.progress.set_max(chunk_count); self.progress.set_size(length); - self.progress.set_time_now(); + self.progress.reset(); } pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> { @@ -188,7 +192,7 @@ impl GameDownloadAgent { self.generate_contexts()?; } - *self.context_map.lock().unwrap() = self.stored_manifest.get_contexts(); + *self.context_map.lock().unwrap() = self.dropdata.get_contexts(); Ok(()) } @@ -198,7 +202,7 @@ impl GameDownloadAgent { let game_id = self.id.clone(); let mut contexts = Vec::new(); - let base_path = Path::new(&self.stored_manifest.base_path); + let base_path = Path::new(&self.dropdata.base_path); create_dir_all(base_path).unwrap(); for (raw_path, chunk) in manifest { @@ -207,11 +211,12 @@ impl GameDownloadAgent { let container = path.parent().unwrap(); create_dir_all(container).unwrap(); + let already_exists = path.exists(); let file = OpenOptions::new() .read(true) .write(true) - .truncate(true) .create(true) + .truncate(false) .open(path.clone()) .unwrap(); let mut running_offset = 0; @@ -232,12 +237,12 @@ impl GameDownloadAgent { } #[cfg(target_os = "linux")] - if running_offset > 0 { + if running_offset > 0 && !already_exists { let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset); } } - let existing_contexts = self.stored_manifest.get_completed_contexts(); - self.stored_manifest.set_contexts( + let existing_contexts = self.dropdata.get_completed_contexts(); + self.dropdata.set_contexts( &contexts .iter() .map(|x| (x.checksum.clone(), existing_contexts.contains(&x.checksum))) @@ -249,8 +254,8 @@ impl GameDownloadAgent { Ok(()) } - // TODO: Change return value on Err - pub fn run(&self) -> Result { + fn run(&self) -> Result { + self.setup_progress(); let max_download_threads = borrow_db_checked().settings.max_download_threads; debug!( @@ -266,7 +271,6 @@ impl GameDownloadAgent { let completed_indexes_loop_arc = completed_contexts.clone(); let contexts = self.contexts.lock().unwrap(); - debug!("{contexts:#?}"); pool.scope(|scope| { let client = &DROP_CLIENT_SYNC.clone(); let context_map = self.context_map.lock().unwrap(); @@ -278,7 +282,10 @@ impl GameDownloadAgent { let progress_handle = ProgressHandle::new(progress, self.progress.clone()); // If we've done this one already, skip it - if Some(&true) == context_map.get(&context.checksum) { + // Note to future DecDuck, DropData gets loaded into context_map + if let Some(v) = context_map.get(&context.checksum) + && *v + { progress_handle.skip(context.length); continue; } @@ -345,8 +352,8 @@ impl GameDownloadAgent { .collect::>(); drop(context_map_lock); - self.stored_manifest.set_contexts(&contexts); - self.stored_manifest.write(); + self.dropdata.set_contexts(&contexts); + self.dropdata.write(); // If there are any contexts left which are false if !contexts.iter().all(|x| x.1) { @@ -361,6 +368,93 @@ impl GameDownloadAgent { Ok(true) } + + fn setup_validate(&self, app_handle: &AppHandle) { + self.setup_progress(); + + self.control_flag.set(DownloadThreadControlFlag::Go); + + let mut db_lock = borrow_db_mut_checked(); + db_lock.applications.transient_statuses.insert( + self.metadata(), + ApplicationTransientStatus::Validating { + version_name: self.version.clone(), + }, + ); + push_game_update( + app_handle, + &self.metadata().id, + None, + GameStatusManager::fetch_state(&self.metadata().id, &db_lock), + ); + } + + pub fn validate(&self, app_handle: &AppHandle) -> Result { + self.setup_validate(app_handle); + + let contexts = self.contexts.lock().unwrap(); + let max_download_threads = borrow_db_checked().settings.max_download_threads; + + debug!( + "validating game: {} with {} threads", + self.dropdata.game_id, max_download_threads + ); + let pool = ThreadPoolBuilder::new() + .num_threads(max_download_threads) + .build() + .unwrap(); + + let invalid_chunks = Arc::new(boxcar::Vec::new()); + pool.scope(|scope| { + for (index, context) in contexts.iter().enumerate() { + let current_progress = self.progress.get(index); + let progress_handle = ProgressHandle::new(current_progress, self.progress.clone()); + let invalid_chunks_scoped = invalid_chunks.clone(); + let sender = self.sender.clone(); + + scope.spawn(move |_| { + match validate_game_chunk(context, &self.control_flag, progress_handle) { + Ok(true) => {} + Ok(false) => { + invalid_chunks_scoped.push(context.checksum.clone()); + } + Err(e) => { + error!("{e}"); + sender.send(DownloadManagerSignal::Error(e)).unwrap(); + } + } + }); + } + }); + + // If there are any contexts left which are false + if !invalid_chunks.is_empty() { + info!("validation of game id {} failed", self.id); + + for context in invalid_chunks.iter() { + self.dropdata.set_context(context.1.clone(), false); + } + + self.dropdata.write(); + + return Ok(false); + } + + Ok(true) + } + + pub fn cancel(&self, app_handle: &AppHandle) { + // See docs on usage + set_partially_installed( + &self.metadata(), + self.dropdata.base_path.to_str().unwrap().to_string(), + Some(app_handle), + ); + + self.dropdata.write(); + + + } } impl Downloadable for GameDownloadAgent { @@ -369,6 +463,11 @@ impl Downloadable for GameDownloadAgent { self.download(app_handle) } + fn validate(&self, app_handle: &AppHandle) -> Result { + *self.status.lock().unwrap() = DownloadStatus::Validating; + self.validate(app_handle) + } + fn progress(&self) -> Arc { self.progress.clone() } @@ -407,37 +506,25 @@ impl Downloadable for GameDownloadAgent { fn on_complete(&self, app_handle: &tauri::AppHandle) { on_game_complete( &self.metadata(), - self.stored_manifest.base_path.to_string_lossy().to_string(), + self.dropdata.base_path.to_string_lossy().to_string(), app_handle, ) .unwrap(); } - // TODO: fix this function. It doesn't restart the download properly, nor does it reset the state properly - fn on_incomplete(&self, app_handle: &tauri::AppHandle) { - on_game_incomplete( - &self.metadata(), - self.stored_manifest.base_path.to_string_lossy().to_string(), - app_handle, - ) - .unwrap(); - println!("Attempting to redownload"); + fn on_cancelled(&self, app_handle: &tauri::AppHandle) { + self.cancel(app_handle); + /* + on_game_incomplete( + &self.metadata(), + self.dropdata.base_path.to_string_lossy().to_string(), + app_handle, + ) + .unwrap(); + */ } - fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {} - fn status(&self) -> DownloadStatus { self.status.lock().unwrap().clone() } - - fn validate(&self) -> Result { - *self.status.lock().unwrap() = DownloadStatus::Validating; - game_validate_logic( - &self.stored_manifest, - self.contexts.lock().unwrap().clone(), - self.progress.clone(), - self.sender.clone(), - &self.control_flag, - ) - } } diff --git a/src-tauri/src/games/downloads/drop_data.rs b/src-tauri/src/games/downloads/drop_data.rs index a614624..38ee575 100644 --- a/src-tauri/src/games/downloads/drop_data.rs +++ b/src-tauri/src/games/downloads/drop_data.rs @@ -1,13 +1,13 @@ use std::{ - collections::HashMap, fs::File, io::{Read, Write}, path::PathBuf + collections::HashMap, fs::File, io::{self, Read, Write}, path::{Path, PathBuf} }; -use log::{debug, error, info, warn}; +use log::error; use native_model::{Decode, Encode}; pub type DropData = v1::DropData; -static DROP_DATA_PATH: &str = ".dropdata"; +pub static DROP_DATA_PATH: &str = ".dropdata"; pub mod v1 { use std::{collections::HashMap, path::PathBuf, sync::Mutex}; @@ -38,27 +38,18 @@ pub mod v1 { impl DropData { pub fn generate(game_id: String, game_version: String, base_path: PathBuf) -> Self { - let mut file = if let Ok(file) = File::open(base_path.join(DROP_DATA_PATH)) { file } else { - debug!("Generating new dropdata for game {game_id}"); - return DropData::new(game_id, game_version, base_path); - }; + match DropData::read(&base_path) { + Ok(v) => v, + Err(_) => DropData::new(game_id, game_version, base_path), + } + } + pub fn read(base_path: &Path) -> Result { + let mut file = File::open(base_path.join(DROP_DATA_PATH))?; let mut s = Vec::new(); - match file.read_to_end(&mut s) { - Ok(_) => {} - Err(e) => { - error!("{e}"); - return DropData::new(game_id, game_version, base_path); - } - } + file.read_to_end(&mut s)?; - match native_model::rmp_serde_1_3::RmpSerde::decode(s) { - Ok(manifest) => manifest, - Err(e) => { - warn!("{e}"); - DropData::new(game_id, game_version, base_path) - } - } + Ok(native_model::rmp_serde_1_3::RmpSerde::decode(s).unwrap()) } pub fn write(&self) { let manifest_raw = match native_model::rmp_serde_1_3::RmpSerde::encode(&self) { @@ -94,10 +85,6 @@ impl DropData { .collect() } pub fn get_contexts(&self) -> HashMap { - info!( - "Any contexts which are complete? {}", - self.contexts.lock().unwrap().iter().any(|x| *x.1) - ); self.contexts.lock().unwrap().clone() } } diff --git a/src-tauri/src/games/downloads/mod.rs b/src-tauri/src/games/downloads/mod.rs index 5854c55..dc9e277 100644 --- a/src-tauri/src/games/downloads/mod.rs +++ b/src-tauri/src/games/downloads/mod.rs @@ -1,6 +1,6 @@ pub mod commands; pub mod download_agent; mod download_logic; -mod drop_data; +pub mod drop_data; mod manifest; pub mod validate; diff --git a/src-tauri/src/games/downloads/validate.rs b/src-tauri/src/games/downloads/validate.rs index dad9035..90e9e41 100644 --- a/src-tauri/src/games/downloads/validate.rs +++ b/src-tauri/src/games/downloads/validate.rs @@ -1,99 +1,22 @@ use std::{ fs::File, io::{self, BufWriter, Read, Seek, SeekFrom, Write}, - sync::{Arc, mpsc::Sender}, }; -use log::{debug, error, info}; +use log::debug; use md5::Context; -use rayon::ThreadPoolBuilder; use crate::{ - database::db::borrow_db_checked, - download_manager::{ - download_manager_frontend::DownloadManagerSignal, + download_manager:: util::{ download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, - progress_object::{ProgressHandle, ProgressObject}, - }, - }, + progress_object::ProgressHandle, + } + , error::application_download_error::ApplicationDownloadError, - games::downloads::{drop_data::DropData, manifest::DropDownloadContext}, + games::downloads::manifest::DropDownloadContext, }; -pub fn game_validate_logic( - dropdata: &DropData, - contexts: Vec, - progress: Arc, - sender: Sender, - control_flag: &DownloadThreadControl, -) -> Result { - progress.reset(contexts.len()); - let max_download_threads = borrow_db_checked().settings.max_download_threads; - - debug!( - "validating game: {} with {} threads", - dropdata.game_id, max_download_threads - ); - let pool = ThreadPoolBuilder::new() - .num_threads(max_download_threads) - .build() - .unwrap(); - - debug!("{contexts:#?}"); - let invalid_chunks = Arc::new(boxcar::Vec::new()); - pool.scope(|scope| { - for (index, context) in contexts.iter().enumerate() { - let current_progress = progress.get(index); - let progress_handle = ProgressHandle::new(current_progress, progress.clone()); - let invalid_chunks_scoped = invalid_chunks.clone(); - let sender = sender.clone(); - - scope.spawn(move |_| { - match validate_game_chunk(context, control_flag, progress_handle) { - Ok(true) => { - debug!( - "Finished context #{} with checksum {}", - index, context.checksum - ); - } - Ok(false) => { - debug!( - "Didn't finish context #{} with checksum {}", - index, &context.checksum - ); - invalid_chunks_scoped.push(context.checksum.clone()); - } - Err(e) => { - error!("{e}"); - sender.send(DownloadManagerSignal::Error(e)).unwrap(); - } - } - }); - } - }); - - - // If there are any contexts left which are false - if !invalid_chunks.is_empty() { - info!( - "validation of game id {} failed for chunks {:?}", - dropdata.game_id.clone(), - invalid_chunks - ); - - for context in invalid_chunks.iter() { - dropdata.set_context(context.1.clone(), false); - } - - dropdata.write(); - - return Ok(false); - } - - Ok(true) -} - pub fn validate_game_chunk( ctx: &DropDownloadContext, control_flag: &DownloadThreadControl, @@ -129,10 +52,6 @@ pub fn validate_game_chunk( let res = hex::encode(hasher.compute().0); if res != ctx.checksum { - println!( - "Checksum failed. Correct: {}, actual: {}", - &ctx.checksum, &res - ); return Ok(false); } diff --git a/src-tauri/src/games/library.rs b/src-tauri/src/games/library.rs index 0842959..c32d750 100644 --- a/src-tauri/src/games/library.rs +++ b/src-tauri/src/games/library.rs @@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize}; use tauri::AppHandle; use tauri::Emitter; +use crate::AppState; use crate::database::db::{borrow_db_checked, borrow_db_mut_checked}; +use crate::database::models::data::Database; use crate::database::models::data::{ ApplicationTransientStatus, DownloadableMetadata, GameDownloadStatus, GameVersion, }; @@ -16,11 +18,11 @@ use crate::error::library_error::LibraryError; use crate::error::remote_access_error::RemoteAccessError; use crate::games::state::{GameStatusManager, GameStatusWithTransient}; use crate::remote::auth::generate_authorization_header; +use crate::remote::cache::cache_object_db; use crate::remote::cache::{cache_object, get_cached_object, get_cached_object_db}; use crate::remote::requests::make_request; use crate::remote::utils::DROP_CLIENT_SYNC; -use crate::AppState; -use bitcode::{Encode, Decode}; +use bitcode::{Decode, Encode}; #[derive(Serialize, Deserialize, Debug)] pub struct FetchGameStruct { @@ -146,27 +148,22 @@ pub fn fetch_game_logic( ) -> Result { let mut state_handle = state.lock().unwrap(); - let handle = borrow_db_checked(); + let db_lock = borrow_db_checked(); - let metadata_option = handle.applications.installed_game_version.get(&id); + let metadata_option = db_lock.applications.installed_game_version.get(&id); let version = match metadata_option { None => None, - Some(metadata) => Some( - handle - .applications - .game_versions - .get(&metadata.id) - .unwrap() - .get(metadata.version.as_ref().unwrap()) - .unwrap() - .clone(), - ), + Some(metadata) => db_lock + .applications + .game_versions + .get(&metadata.id) + .map(|v| v.get(metadata.version.as_ref().unwrap()).unwrap()) + .cloned(), }; - drop(handle); let game = state_handle.games.get(&id); if let Some(game) = game { - let status = GameStatusManager::fetch_state(&id); + let status = GameStatusManager::fetch_state(&id, &db_lock); let data = FetchGameStruct { game: game.clone(), @@ -174,10 +171,12 @@ pub fn fetch_game_logic( version, }; - cache_object(&id, game)?; + cache_object_db(&id, game, &db_lock)?; return Ok(data); } + drop(db_lock); + let client = DROP_CLIENT_SYNC.clone(); let response = make_request(&client, &["/api/v1/client/game/", &id], &[], |r| { r.header("Authorization", generate_authorization_header()) @@ -203,9 +202,10 @@ pub fn fetch_game_logic( .game_statuses .entry(id.clone()) .or_insert(GameDownloadStatus::Remote {}); - drop(db_handle); - let status = GameStatusManager::fetch_state(&id); + let status = GameStatusManager::fetch_state(&id, &db_handle); + + drop(db_handle); let data = FetchGameStruct { game: game.clone(), @@ -222,12 +222,12 @@ pub fn fetch_game_logic_offline( id: String, _state: tauri::State<'_, Mutex>, ) -> Result { - let handle = borrow_db_checked(); - let metadata_option = handle.applications.installed_game_version.get(&id); + let db_handle = borrow_db_checked(); + let metadata_option = db_handle.applications.installed_game_version.get(&id); let version = match metadata_option { None => None, Some(metadata) => Some( - handle + db_handle .applications .game_versions .get(&metadata.id) @@ -237,11 +237,12 @@ pub fn fetch_game_logic_offline( .clone(), ), }; - drop(handle); - let status = GameStatusManager::fetch_state(&id); + let status = GameStatusManager::fetch_state(&id, &db_handle); let game = get_cached_object::(&id)?; + drop(db_handle); + Ok(FetchGameStruct { game, status, @@ -275,7 +276,11 @@ pub fn fetch_game_verion_options_logic( let process_manager_lock = state_lock.process_manager.lock().unwrap(); let data: Vec = data .into_iter() - .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap()) + .filter(|v| { + process_manager_lock + .valid_platform(&v.platform, &state_lock) + .unwrap() + }) .collect(); drop(process_manager_lock); drop(state_lock); @@ -283,6 +288,49 @@ pub fn fetch_game_verion_options_logic( Ok(data) } +/** + * Called by: + * - on_cancel, when cancelled, for obvious reasons + * - when downloading, so if drop unexpectedly quits, we can resume the download. hidden by the "Downloading..." transient state, though + * - when scanning, to import the game + */ +pub fn set_partially_installed( + meta: &DownloadableMetadata, + install_dir: String, + app_handle: Option<&AppHandle>, +) { + set_partially_installed_db(&mut borrow_db_mut_checked(), meta, install_dir, app_handle); +} + +pub fn set_partially_installed_db( + db_lock: &mut Database, + meta: &DownloadableMetadata, + install_dir: String, + app_handle: Option<&AppHandle>, +) { + db_lock.applications.transient_statuses.remove(meta); + db_lock.applications.game_statuses.insert( + meta.id.clone(), + GameDownloadStatus::PartiallyInstalled { + version_name: meta.version.as_ref().unwrap().clone(), + install_dir, + }, + ); + db_lock + .applications + .installed_game_version + .insert(meta.id.clone(), meta.clone()); + + if let Some(app_handle) = app_handle { + push_game_update( + app_handle, + &meta.id, + None, + GameStatusManager::fetch_state(&meta.id, db_lock), + ); + } +} + pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) { debug!("triggered uninstall for agent"); let mut db_handle = borrow_db_mut_checked(); @@ -296,7 +344,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) app_handle, &meta.id, None, - (None, Some(ApplicationTransientStatus::Uninstalling {})), + GameStatusManager::fetch_state(&meta.id, &db_handle), ); let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned(); @@ -330,31 +378,35 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) drop(db_handle); let app_handle = app_handle.clone(); - spawn(move || if let Err(e) = remove_dir_all(install_dir) { - error!("{e}"); - } else { - let mut db_handle = borrow_db_mut_checked(); - db_handle.applications.transient_statuses.remove(&meta); - db_handle - .applications - .installed_game_version - .remove(&meta.id); - db_handle - .applications - .game_statuses - .entry(meta.id.clone()) - .and_modify(|e| *e = GameDownloadStatus::Remote {}); - drop(db_handle); + spawn(move || { + if let Err(e) = remove_dir_all(install_dir) { + error!("{e}"); + } else { + let mut db_handle = borrow_db_mut_checked(); + db_handle.applications.transient_statuses.remove(&meta); + db_handle + .applications + .installed_game_version + .remove(&meta.id); + db_handle + .applications + .game_statuses + .entry(meta.id.clone()) + .and_modify(|e| *e = GameDownloadStatus::Remote {}); + let _ = db_handle.applications.transient_statuses.remove(&meta); - debug!("uninstalled game id {}", &meta.id); - app_handle.emit("update_library", ()).unwrap(); + push_game_update( + &app_handle, + &meta.id, + None, + GameStatusManager::fetch_state(&meta.id, &db_handle), + ); - push_game_update( - &app_handle, - &meta.id, - None, - (Some(GameDownloadStatus::Remote {}), None), - ); + debug!("uninstalled game id {}", &meta.id); + app_handle.emit("update_library", ()).unwrap(); + + drop(db_handle); + } }); } else { warn!("invalid previous state for uninstall, failing silently."); @@ -369,66 +421,6 @@ pub fn get_current_meta(game_id: &String) -> Option { .cloned() } -pub fn on_game_incomplete( - meta: &DownloadableMetadata, - install_dir: String, - app_handle: &AppHandle, -) -> Result<(), RemoteAccessError> { - // Fetch game version information from remote - if meta.version.is_none() { - return Err(RemoteAccessError::GameNotFound(meta.id.clone())); - } - - let client = DROP_CLIENT_SYNC.clone(); - let response = make_request( - &client, - &["/api/v1/client/game/version"], - &[ - ("id", &meta.id), - ("version", meta.version.as_ref().unwrap()), - ], - |f| f.header("Authorization", generate_authorization_header()), - )? - .send()?; - - let game_version: GameVersion = response.json()?; - - let mut handle = borrow_db_mut_checked(); - handle - .applications - .game_versions - .entry(meta.id.clone()) - .or_default() - .insert(meta.version.clone().unwrap(), game_version.clone()); - handle - .applications - .installed_game_version - .insert(meta.id.clone(), meta.clone()); - - let status = GameDownloadStatus::PartiallyInstalled { - version_name: meta.version.clone().unwrap(), - install_dir, - }; - - handle - .applications - .game_statuses - .insert(meta.id.clone(), status.clone()); - drop(handle); - app_handle - .emit( - &format!("update_game/{}", meta.id), - GameUpdateEvent { - game_id: meta.id.clone(), - status: (Some(status), None), - version: Some(game_version), - }, - ) - .unwrap(); - - Ok(()) -} - pub fn on_game_complete( meta: &DownloadableMetadata, install_dir: String, diff --git a/src-tauri/src/games/state.rs b/src-tauri/src/games/state.rs index c95c0d5..668eb1b 100644 --- a/src-tauri/src/games/state.rs +++ b/src-tauri/src/games/state.rs @@ -1,7 +1,4 @@ -use crate::database::{ - db::borrow_db_checked, - models::data::{ApplicationTransientStatus, GameDownloadStatus}, -}; +use crate::database::models::data::{ApplicationTransientStatus, Database, GameDownloadStatus}; pub type GameStatusWithTransient = ( Option, @@ -10,14 +7,12 @@ pub type GameStatusWithTransient = ( pub struct GameStatusManager {} impl GameStatusManager { - pub fn fetch_state(game_id: &String) -> GameStatusWithTransient { - let db_lock = borrow_db_checked(); - let online_state = match db_lock.applications.installed_game_version.get(game_id) { - Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(), + pub fn fetch_state(game_id: &String, database: &Database) -> GameStatusWithTransient { + let online_state = match database.applications.installed_game_version.get(game_id) { + Some(meta) => database.applications.transient_statuses.get(meta).cloned(), None => None, }; - let offline_state = db_lock.applications.game_statuses.get(game_id).cloned(); - drop(db_lock); + let offline_state = database.applications.game_statuses.get(game_id).cloned(); if online_state.is_some() { return (None, online_state); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 13f6525..b7f2483 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,7 +11,9 @@ mod error; mod process; mod remote; +use crate::database::scan::scan_install_dirs; use crate::process::commands::open_process_logs; +use crate::process::process_handlers::UMU_LAUNCHER_EXECUTABLE; use crate::remote::commands::auth_initiate_code; use crate::{database::db::DatabaseImpls, games::downloads::commands::resume_download}; use bitcode::{Decode, Encode}; @@ -60,6 +62,7 @@ use std::fs::File; use std::io::Write; use std::panic::PanicHookInfo; use std::path::Path; +use std::process::{Command, Stdio}; use std::str::FromStr; use std::sync::Arc; use std::time::SystemTime; @@ -95,6 +98,27 @@ pub struct User { profile_picture_object_id: String, } +#[derive(Clone)] +pub struct CompatInfo { + umu_installed: bool, +} + +fn create_new_compat_info() -> Option { + #[cfg(target_os = "windows")] + return None; + + let has_umu_installed = Command::new(UMU_LAUNCHER_EXECUTABLE) + .stdout(Stdio::null()) + .spawn(); + if let Err(umu_error) = &has_umu_installed { + warn!("disabling windows support with error: {umu_error}"); + } + let has_umu_installed = has_umu_installed.is_ok(); + Some(CompatInfo { + umu_installed: has_umu_installed, + }) +} + #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AppState<'a> { @@ -106,6 +130,8 @@ pub struct AppState<'a> { download_manager: Arc, #[serde(skip_serializing)] process_manager: Arc>>, + #[serde(skip_serializing)] + compat_info: Option, } fn setup(handle: AppHandle) -> AppState<'static> { @@ -142,9 +168,13 @@ fn setup(handle: AppHandle) -> AppState<'static> { let games = HashMap::new(); let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone())); let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone()))); + let compat_info = create_new_compat_info(); debug!("checking if database is set up"); let is_set_up = DB.database_is_set_up(); + + scan_install_dirs(); + if !is_set_up { return AppState { status: AppStatus::NotConfigured, @@ -152,6 +182,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { games, download_manager, process_manager, + compat_info, }; } @@ -164,6 +195,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { let mut missing_games = Vec::new(); let statuses = db_handle.applications.game_statuses.clone(); drop(db_handle); + for (game_id, status) in statuses { match status { GameDownloadStatus::Remote {} => {} @@ -215,6 +247,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { games, download_manager, process_manager, + compat_info, } } diff --git a/src-tauri/src/process/commands.rs b/src-tauri/src/process/commands.rs index fda520f..13b89fa 100644 --- a/src-tauri/src/process/commands.rs +++ b/src-tauri/src/process/commands.rs @@ -16,7 +16,7 @@ pub fn launch_game( // download_type: DownloadType::Game, //}; - match process_manager_lock.launch_process(id) { + match process_manager_lock.launch_process(id, &state_lock) { Ok(()) => {} Err(e) => return Err(e), } diff --git a/src-tauri/src/process/format.rs b/src-tauri/src/process/format.rs new file mode 100644 index 0000000..785746d --- /dev/null +++ b/src-tauri/src/process/format.rs @@ -0,0 +1,33 @@ +use std::collections::HashMap; + +use dynfmt::{Argument, FormatArgs}; + +pub struct DropFormatArgs { + positional: Vec, + map: HashMap<&'static str, String>, +} + +impl DropFormatArgs { + pub fn new(launch_string: String, working_dir: &String, executable_name: &String, absolute_executable_name: String) -> Self { + let mut positional = Vec::new(); + let mut map: HashMap<&'static str, String> = HashMap::new(); + + positional.push(launch_string); + + map.insert("dir", working_dir.to_string()); + map.insert("exe", executable_name.to_string()); + map.insert("abs_exe", absolute_executable_name); + + Self { positional, map } + } +} + +impl FormatArgs for DropFormatArgs { + fn get_index(&self, index: usize) -> Result>, ()> { + Ok(self.positional.get(index).map(|arg| arg as Argument<'_>)) + } + + fn get_key(&self, key: &str) -> Result>, ()> { + Ok(self.map.get(key).map(|arg| arg as Argument<'_>)) + } +} diff --git a/src-tauri/src/process/mod.rs b/src-tauri/src/process/mod.rs index 2fc8dfd..02b74ba 100644 --- a/src-tauri/src/process/mod.rs +++ b/src-tauri/src/process/mod.rs @@ -2,3 +2,5 @@ pub mod commands; #[cfg(target_os = "linux")] pub mod compat; pub mod process_manager; +pub mod process_handlers; +pub mod format; \ No newline at end of file diff --git a/src-tauri/src/process/process_handlers.rs b/src-tauri/src/process/process_handlers.rs new file mode 100644 index 0000000..e11f612 --- /dev/null +++ b/src-tauri/src/process/process_handlers.rs @@ -0,0 +1,109 @@ +use log::debug; + +use crate::{ + AppState, + database::models::data::{Database, DownloadableMetadata, GameVersion}, + process::process_manager::{Platform, ProcessHandler}, +}; + +pub struct NativeGameLauncher; +impl ProcessHandler for NativeGameLauncher { + fn create_launch_process( + &self, + _meta: &DownloadableMetadata, + launch_command: String, + args: Vec, + _game_version: &GameVersion, + _current_dir: &str, + ) -> String { + format!("\"{}\" {}", launch_command, args.join(" ")) + } + + fn valid_for_platform(&self, _db: &Database, _state: &AppState, _target: &Platform) -> bool { + true + } +} + +pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run"; +pub struct UMULauncher; +impl ProcessHandler for UMULauncher { + fn create_launch_process( + &self, + _meta: &DownloadableMetadata, + launch_command: String, + args: Vec, + game_version: &GameVersion, + _current_dir: &str, + ) -> String { + debug!("Game override: \"{:?}\"", &game_version.umu_id_override); + let game_id = match &game_version.umu_id_override { + Some(game_override) => { + if game_override.is_empty() { + game_version.game_id.clone() + } else { + game_override.clone() + } + } + None => game_version.game_id.clone(), + }; + format!( + "GAMEID={game_id} {umu} \"{launch}\" {args}", + umu = UMU_LAUNCHER_EXECUTABLE, + launch = launch_command, + args = args.join(" ") + ) + } + + fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool { + let Some(ref compat_info) = state.compat_info else { + return false; + }; + compat_info.umu_installed + } +} + +pub struct AsahiMuvmLauncher; +impl ProcessHandler for AsahiMuvmLauncher { + fn create_launch_process( + &self, + meta: &DownloadableMetadata, + launch_command: String, + args: Vec, + game_version: &GameVersion, + current_dir: &str, + ) -> String { + let umu_launcher = UMULauncher {}; + let umu_string = umu_launcher.create_launch_process( + meta, + launch_command, + args, + game_version, + current_dir, + ); + let mut args_cmd = umu_string.split("umu-run").collect::>().into_iter(); + let args = args_cmd.next().unwrap().trim(); + let cmd = format!("umu-run{}", args_cmd.next().unwrap()); + + format!("{args} muvm -- {cmd}") + } + + #[allow(unreachable_code)] + fn valid_for_platform(&self, _db: &Database, state: &AppState, _target: &Platform) -> bool { + #[cfg(not(target_os = "linux"))] + return false; + + #[cfg(not(target_arch = "aarch64"))] + return false; + + let page_size = page_size::get(); + if page_size != 16384 { + return false; + } + + let Some(ref compat_info) = state.compat_info else { + return false; + }; + + compat_info.umu_installed + } +} diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs index 5113b54..f3d72e9 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -21,14 +21,18 @@ use tauri_plugin_opener::OpenerExt; use crate::{ AppState, DB, database::{ - db::{DATA_ROOT_DIR, borrow_db_mut_checked}, + db::{DATA_ROOT_DIR, borrow_db_checked, borrow_db_mut_checked}, models::data::{ - ApplicationTransientStatus, DownloadType, DownloadableMetadata, GameDownloadStatus, - GameVersion, + ApplicationTransientStatus, Database, DownloadType, DownloadableMetadata, + GameDownloadStatus, GameVersion, }, }, error::process_error::ProcessError, games::{library::push_game_update, state::GameStatusManager}, + process::{ + format::DropFormatArgs, + process_handlers::{AsahiMuvmLauncher, NativeGameLauncher, UMULauncher}, + }, }; pub struct RunningProcess { @@ -42,7 +46,10 @@ pub struct ProcessManager<'a> { log_output_dir: PathBuf, processes: HashMap, app_handle: AppHandle, - game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>, + game_launchers: Vec<( + (Platform, Platform), + &'a (dyn ProcessHandler + Sync + Send + 'static), + )>, } impl ProcessManager<'_> { @@ -62,7 +69,7 @@ impl ProcessManager<'_> { app_handle, processes: HashMap::new(), log_output_dir, - game_launchers: HashMap::from([ + game_launchers: vec![ // Current platform to target platform ( (Platform::Windows, Platform::Windows), @@ -76,11 +83,15 @@ impl ProcessManager<'_> { (Platform::MacOs, Platform::MacOs), &NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static), ), + ( + (Platform::Linux, Platform::Windows), + &AsahiMuvmLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static), + ), ( (Platform::Linux, Platform::Windows), &UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static), ), - ]), + ], } } @@ -99,8 +110,12 @@ impl ProcessManager<'_> { } } + fn get_log_dir(&self, game_id: String) -> PathBuf { + self.log_output_dir.join(game_id) + } + pub fn open_process_logs(&mut self, game_id: String) -> Result<(), ProcessError> { - let dir = self.log_output_dir.join(game_id); + let dir = self.get_log_dir(game_id); self.app_handle .opener() .open_path(dir.to_str().unwrap(), None::<&str>) @@ -145,7 +160,6 @@ impl ProcessManager<'_> { }, ); } - drop(db_handle); let elapsed = process.start.elapsed().unwrap_or(Duration::ZERO); // If we started and ended really quickly, something might've gone wrong @@ -158,16 +172,42 @@ impl ProcessManager<'_> { let _ = self.app_handle.emit("launch_external_error", &game_id); } - let status = GameStatusManager::fetch_state(&game_id); + let status = GameStatusManager::fetch_state(&game_id, &db_handle); + drop(db_handle); + push_game_update(&self.app_handle, &game_id, None, status); } - pub fn valid_platform(&self, platform: &Platform) -> Result { - let current = &self.current_platform; - Ok(self.game_launchers.contains_key(&(*current, *platform))) + fn fetch_process_handler( + &self, + db_lock: &Database, + state: &AppState, + target_platform: &Platform, + ) -> Result<&(dyn ProcessHandler + Send + Sync), ProcessError> { + Ok(self + .game_launchers + .iter() + .find(|e| { + let (e_current, e_target) = e.0; + e_current == self.current_platform + && e_target == *target_platform + && e.1.valid_for_platform(db_lock, state, target_platform) + }) + .ok_or(ProcessError::InvalidPlatform)? + .1) } - pub fn launch_process(&mut self, game_id: String) -> Result<(), ProcessError> { + pub fn valid_platform(&self, platform: &Platform, state: &AppState) -> Result { + let db_lock = borrow_db_checked(); + let process_handler = self.fetch_process_handler(&db_lock, state, platform); + Ok(process_handler.is_ok()) + } + + pub fn launch_process( + &mut self, + game_id: String, + state: &AppState, + ) -> Result<(), ProcessError> { if self.processes.contains_key(&game_id) { return Err(ProcessError::AlreadyRunning); } @@ -191,10 +231,6 @@ impl ProcessManager<'_> { }; let mut db_lock = borrow_db_mut_checked(); - debug!( - "Launching process {:?} with games {:?}", - &game_id, db_lock.applications.game_versions - ); let game_status = db_lock .applications @@ -211,13 +247,15 @@ impl ProcessManager<'_> { version_name, install_dir, } => (version_name, install_dir), - GameDownloadStatus::PartiallyInstalled { - version_name, - install_dir, - } => (version_name, install_dir), - _ => return Err(ProcessError::NotDownloaded), + _ => return Err(ProcessError::NotInstalled), }; + debug!( + "Launching process {:?} with version {:?}", + &game_id, + db_lock.applications.game_versions.get(&game_id).unwrap() + ); + let game_version = db_lock .applications .game_versions @@ -227,7 +265,7 @@ impl ProcessManager<'_> { .ok_or(ProcessError::InvalidVersion)?; // TODO: refactor this path with open_process_logs - let game_log_folder = &self.log_output_dir.join(game_id); + let game_log_folder = &self.get_log_dir(game_id); create_dir_all(game_log_folder).map_err(ProcessError::IOError)?; let current_time = chrono::offset::Local::now(); @@ -251,13 +289,9 @@ impl ProcessManager<'_> { ))) .map_err(ProcessError::IOError)?; - let current_platform = self.current_platform; let target_platform = game_version.platform; - let game_launcher = self - .game_launchers - .get(&(current_platform, target_platform)) - .ok_or(ProcessError::InvalidPlatform)?; + let process_handler = self.fetch_process_handler(&db_lock, state, &target_platform)?; let (launch, args) = match game_status { GameDownloadStatus::Installed { @@ -278,7 +312,7 @@ impl ProcessManager<'_> { let launch = PathBuf::from_str(install_dir).unwrap().join(launch); let launch = launch.to_str().unwrap(); - let launch_string = game_launcher.create_launch_process( + let launch_string = process_handler.create_launch_process( &meta, launch.to_string(), args.clone(), @@ -286,8 +320,15 @@ impl ProcessManager<'_> { install_dir, ); + let format_args = DropFormatArgs::new( + launch_string, + install_dir, + &game_version.launch_command, + launch.to_string(), + ); + let launch_string = SimpleCurlyFormat - .format(&game_version.launch_command_template, &[launch_string]) + .format(&game_version.launch_command_template, format_args) .map_err(|e| ProcessError::FormatError(e.to_string()))? .to_string(); @@ -306,9 +347,12 @@ impl ProcessManager<'_> { #[cfg(unix)] command.args(vec!["-c", &launch_string]); + debug!("final launch string:\n\n{launch_string}\n"); + command .stderr(error_file) .stdout(log_file) + .env_remove("RUST_LOG") .current_dir(install_dir); let child = command.spawn().map_err(ProcessError::IOError)?; @@ -413,49 +457,6 @@ pub trait ProcessHandler: Send + 'static { game_version: &GameVersion, current_dir: &str, ) -> String; -} -struct NativeGameLauncher; -impl ProcessHandler for NativeGameLauncher { - fn create_launch_process( - &self, - _meta: &DownloadableMetadata, - launch_command: String, - args: Vec, - _game_version: &GameVersion, - _current_dir: &str, - ) -> String { - format!("\"{}\" {}", launch_command, args.join(" ")) - } -} - -pub const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run"; -struct UMULauncher; -impl ProcessHandler for UMULauncher { - fn create_launch_process( - &self, - _meta: &DownloadableMetadata, - launch_command: String, - args: Vec, - game_version: &GameVersion, - _current_dir: &str, - ) -> String { - debug!("Game override: \"{:?}\"", &game_version.umu_id_override); - let game_id = match &game_version.umu_id_override { - Some(game_override) => { - if game_override.is_empty() { - game_version.game_id.clone() - } else { - game_override.clone() - } - } - None => game_version.game_id.clone(), - }; - format!( - "GAMEID={game_id} {umu} \"{launch}\" {args}", - umu = UMU_LAUNCHER_EXECUTABLE, - launch = launch_command, - args = args.join(" ") - ) - } + fn valid_for_platform(&self, db: &Database, state: &AppState, target: &Platform) -> bool; } diff --git a/src-tauri/src/remote/cache.rs b/src-tauri/src/remote/cache.rs index 082bbc4..2c07037 100644 --- a/src-tauri/src/remote/cache.rs +++ b/src-tauri/src/remote/cache.rs @@ -51,8 +51,15 @@ fn read_sync(base: &Path, key: &str) -> io::Result> { } pub fn cache_object(key: &str, data: &D) -> Result<(), RemoteAccessError> { + cache_object_db(key, data, &borrow_db_checked()) +} +pub fn cache_object_db( + key: &str, + data: &D, + database: &Database, +) -> Result<(), RemoteAccessError> { let bytes = bitcode::encode(data); - write_sync(&borrow_db_checked().cache_dir, key, bytes).map_err(RemoteAccessError::Cache) + write_sync(&database.cache_dir, key, bytes).map_err(RemoteAccessError::Cache) } pub fn get_cached_object(key: &str) -> Result { get_cached_object_db::(key, &borrow_db_checked()) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2d9b6e7..d03f9ea 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2.0.0", "productName": "Drop Desktop Client", - "version": "0.3.1-mac", + "version": "0.3.1", "identifier": "dev.drop.app", "build": { "beforeDevCommand": "yarn dev --port 1432", diff --git a/tsconfig.json b/tsconfig.json index a746f2a..0422712 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,4 +1,5 @@ { // https://nuxt.com/docs/guide/concepts/typescript - "extends": "./.nuxt/tsconfig.json" + "extends": "./.nuxt/tsconfig.json", + "exclude": ["src-tauri/**/*"] } diff --git a/types.ts b/types.ts index 57cd321..2ccd6ff 100644 --- a/types.ts +++ b/types.ts @@ -54,12 +54,13 @@ export enum GameStatusEnum { Remote = "Remote", Queued = "Queued", Downloading = "Downloading", + Validating = "Validating", Installed = "Installed", Updating = "Updating", Uninstalling = "Uninstalling", SetupRequired = "SetupRequired", Running = "Running", - PartiallyInstalled = "PartiallyInstalled" + PartiallyInstalled = "PartiallyInstalled", } export type GameStatus = {