diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 1735f73..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[BUG]" -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. Arch Linux, Windows] - - App Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0fc3abe..7d0ff2c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ stages: build-linux: stage: build - image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rustlang/rust:nightly + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/rust:1.81.0-bookworm script: - apt-get update -y - apt-get install yarnpkg libsoup-3.0-0 libsoup-3.0-dev libatk-adaptor libgtk-3-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev -y @@ -13,10 +13,9 @@ build-linux: - yarnpkg tauri build - cp src-tauri/target/release/bundle/deb/*.deb . - cp src-tauri/target/release/bundle/rpm/*.rpm . - - cp src-tauri/target/release/bundle/appimage/*.AppImage . artifacts: paths: - - "*.{deb,rpm,AppImage}" + - "*.{deb,rpm}" build-windows: stage: build diff --git a/app.vue b/app.vue index b9011c6..f81c432 100644 --- a/app.vue +++ b/app.vue @@ -1,8 +1,8 @@ diff --git a/pages/library.vue b/pages/library.vue index ab5079a..925addb 100644 --- a/pages/library.vue +++ b/pages/library.vue @@ -31,7 +31,7 @@
- +
@@ -40,19 +40,7 @@ import { invoke } from "@tauri-apps/api/core"; import { GameStatusEnum, type Game, type NavigationItem } from "~/types"; -let libraryDownloadError = false; - -async function calculateGames(): Promise { - try { - return await invoke("fetch_library"); - } - catch(e) { - libraryDownloadError = true; - return new Array(); - } -} - -const rawGames: Array = await calculateGames(); +const rawGames: Array = await invoke("fetch_library"); const games = await Promise.all(rawGames.map((e) => useGame(e.id))); const icons = await Promise.all( games.map(({ game, status }) => useObject(game.mIconId)) diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index 539ec61..3563722 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -242,7 +242,7 @@ " :loading="installLoading" type="submit" - class="ml-2 w-full sm:w-fit" + class="w-full sm:w-fit" > Install @@ -306,7 +306,6 @@ async function installFlow() { versionOptions.value = await invoke("fetch_game_verion_options", { gameId: game.value.id, }); - console.log(versionOptions.value); installDirs.value = await invoke("fetch_download_dir_stats"); } catch (error) { installError.value = (error as string).toString(); @@ -320,19 +319,19 @@ const installDir = ref(0); async function install() { try { if (!versionOptions.value) - throw new Error("Versions have not been loaded"); + throw new Error("Versions have not been loaded."); installLoading.value = true; await invoke("download_game", { gameId: game.value.id, gameVersion: versionOptions.value[installVersionIndex.value].versionName, installDir: installDir.value, }); + installLoading.value = false; + installFlowOpen.value = false; } catch (error) { installError.value = (error as string).toString(); } - - installLoading.value = false; } async function launch() { diff --git a/pages/library/index.vue b/pages/library/index.vue index dd245b6..61c7765 100644 --- a/pages/library/index.vue +++ b/pages/library/index.vue @@ -1,9 +1,3 @@ - \ No newline at end of file diff --git a/pages/queue.vue b/pages/queue.vue index 8d46bea..d78967c 100644 --- a/pages/queue.vue +++ b/pages/queue.vue @@ -1,37 +1,23 @@ diff --git a/plugins/global-error-handler.ts b/plugins/global-error-handler.ts index b7bcd45..f9d7249 100644 --- a/plugins/global-error-handler.ts +++ b/plugins/global-error-handler.ts @@ -1,7 +1,7 @@ export default defineNuxtPlugin((nuxtApp) => { // Also possible nuxtApp.hook("vue:error", (error, instance, info) => { - console.error(error, info); + console.log(error); const router = useRouter(); router.replace(`/error`); }); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 22b5365..0c8123b 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -255,12 +255,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "atomic-instant-full" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6541700e074cda41b1c6f98c2cae6cde819967bf142078f069cad85387cdbe" - [[package]] name = "atomic-waker" version = "1.1.2" @@ -852,6 +846,37 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.91", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.91", +] + [[package]] name = "derive_more" version = "0.99.18" @@ -1006,17 +1031,18 @@ dependencies = [ [[package]] name = "drop-app" -version = "0.2.0-beta-prerelease-1" +version = "0.1.0" dependencies = [ - "atomic-instant-full", "boxcar", "chrono", + "derive_builder", "directories", "hex", "http", "log", "log4rs", "md5", + "merge-struct", "openssl", "parking_lot 0.12.3", "rayon", @@ -1026,6 +1052,7 @@ dependencies = [ "serde", "serde-binary", "serde_json", + "serde_merge", "serde_with", "shared_child", "slice-deque", @@ -2428,6 +2455,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "merge-struct" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d82012d21e24135b839b6b9bebd622b7ff0cb40071498bc2d066d3a6d04dd4a" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "mime" version = "0.3.17" @@ -3876,6 +3913,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_merge" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "606e91878516232ac3b16c12e063d4468d762f16d77e7aef14a1f2326c5f409b" +dependencies = [ + "serde", + "serde_json", + "thiserror 1.0.69", +] + [[package]] name = "serde_repr" version = "0.1.19" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f9f24c3..c48f490 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "drop-app" -version = "0.2.0-beta-prerelease-1" +version = "0.1.0" description = "The client application for the open-source, self-hosted game distribution platform Drop" authors = ["Drop OSS"] edition = "2021" @@ -47,9 +47,11 @@ tauri-plugin-autostart = "2.0.0" shared_child = "1.0.1" serde_with = "3.12.0" slice-deque = "0.3.0" +derive_builder = "0.20.2" throttle_my_fn = "0.2.6" parking_lot = "0.12.3" -atomic-instant-full = "0.1.0" +merge-struct = "0.1.0" +serde_merge = "0.1.3" [dependencies.tauri] version = "2.1.1" diff --git a/src-tauri/src/autostart.rs b/src-tauri/src/autostart.rs index dec9148..da0c1aa 100644 --- a/src-tauri/src/autostart.rs +++ b/src-tauri/src/autostart.rs @@ -1,4 +1,4 @@ -use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}; +use crate::DB; use log::debug; use tauri::AppHandle; use tauri_plugin_autostart::ManagerExt; @@ -7,24 +7,24 @@ pub fn toggle_autostart_logic(app: AppHandle, enabled: bool) -> Result<(), Strin let manager = app.autolaunch(); if enabled { manager.enable().map_err(|e| e.to_string())?; - debug!("enabled autostart"); + debug!("Enabled autostart"); } else { manager.disable().map_err(|e| e.to_string())?; - debug!("eisabled autostart"); + debug!("Disabled autostart"); } // Store the state in DB - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?; db_handle.settings.autostart = enabled; drop(db_handle); - save_db(); + DB.save().map_err(|e| e.to_string())?; Ok(()) } pub fn get_autostart_enabled_logic(app: AppHandle) -> Result { // First check DB state - let db_handle = borrow_db_checked(); + let db_handle = DB.borrow_data().unwrap(); let db_state = db_handle.settings.autostart; drop(db_handle); @@ -46,7 +46,7 @@ pub fn get_autostart_enabled_logic(app: AppHandle) -> Result Result<(), String> { - let db_handle = borrow_db_checked(); + let db_handle = DB.borrow_data().map_err(|e| e.to_string())?; let should_be_enabled = db_handle.settings.autostart; drop(db_handle); @@ -56,10 +56,10 @@ pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> { if current_state != should_be_enabled { if should_be_enabled { manager.enable().map_err(|e| e.to_string())?; - debug!("synced autostart: enabled"); + debug!("Synced autostart: enabled"); } else { manager.disable().map_err(|e| e.to_string())?; - debug!("synced autostart: disabled"); + debug!("Synced autostart: disabled"); } } diff --git a/src-tauri/src/cleanup.rs b/src-tauri/src/cleanup.rs index de0e4e6..d8817ae 100644 --- a/src-tauri/src/cleanup.rs +++ b/src-tauri/src/cleanup.rs @@ -1,23 +1,13 @@ -use log::{debug, error}; +use log::{debug, info}; use tauri::AppHandle; -use crate::AppState; - #[tauri::command] -pub fn quit(app: tauri::AppHandle, state: tauri::State<'_, std::sync::Mutex>>) { - cleanup_and_exit(&app, &state); +pub fn quit(app: tauri::AppHandle) { + cleanup_and_exit(&app); } -pub fn cleanup_and_exit(app: &AppHandle, state: &tauri::State<'_, std::sync::Mutex>>) { - debug!("cleaning up and exiting application"); - let download_manager = state.lock().unwrap().download_manager.clone(); - match download_manager.ensure_terminated() { - Ok(res) => match res { - Ok(_) => debug!("download manager terminated correctly"), - Err(_) => error!("download manager failed to terminate correctly"), - }, - Err(e) => panic!("{:?}", e), - } +pub fn cleanup_and_exit(app: &AppHandle) { + debug!("Cleaning up and exiting application"); app.exit(0); } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index b47a348..a0b646e 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,4 +1,9 @@ -use crate::AppState; +use tauri::AppHandle; + +use crate::{ + autostart::{get_autostart_enabled_logic, toggle_autostart_logic}, + AppState, +}; #[tauri::command] pub fn fetch_state( diff --git a/src-tauri/src/database/commands.rs b/src-tauri/src/database/commands.rs index 18befb8..b254e00 100644 --- a/src-tauri/src/database/commands.rs +++ b/src-tauri/src/database/commands.rs @@ -6,83 +6,74 @@ use std::{ use serde_json::Value; -use crate::{ - database::{db::borrow_db_mut_checked, settings::Settings}, - download_manager::internal_error::InternalError, -}; +use crate::{database::settings::Settings, error::user_error::UserValue, DB}; -use super::{ - db::{borrow_db_checked, save_db, DATA_ROOT_DIR}, - debug::SystemData, -}; +use super::{db::DATA_ROOT_DIR, debug::SystemData}; // Will, in future, return disk/remaining size // Just returns the directories that have been set up #[tauri::command] pub fn fetch_download_dir_stats() -> Vec { - let lock = borrow_db_checked(); + let lock = DB.borrow_data().unwrap(); lock.applications.install_dirs.clone() } #[tauri::command] pub fn delete_download_dir(index: usize) { - let mut lock = borrow_db_mut_checked(); + let mut lock = DB.borrow_data_mut().unwrap(); lock.applications.install_dirs.remove(index); drop(lock); - save_db(); + DB.save().unwrap(); } #[tauri::command] -pub fn add_download_dir(new_dir: PathBuf) -> Result<(), InternalError<()>> { +pub fn add_download_dir(new_dir: PathBuf) -> UserValue<(), Error> { // Check the new directory is all good let new_dir_path = Path::new(&new_dir); if new_dir_path.exists() { let dir_contents = new_dir_path.read_dir()?; if dir_contents.count() != 0 { - return Err(Error::new( + return UserValue::Err(Error::new( ErrorKind::DirectoryNotEmpty, "Selected directory cannot contain any existing files", - ) - .into()); + )); } } else { create_dir_all(new_dir_path)?; } // Add it to the dictionary - let mut lock = borrow_db_mut_checked(); + let mut lock = DB.borrow_data_mut().unwrap(); if lock.applications.install_dirs.contains(&new_dir) { - return Err(Error::new( + return UserValue::Err(Error::new( ErrorKind::AlreadyExists, "Selected directory already exists in database", - ) - .into()); + )); } lock.applications.install_dirs.push(new_dir); drop(lock); - save_db(); + DB.save().unwrap(); - Ok(()) + UserValue::Ok(()) } #[tauri::command] pub fn update_settings(new_settings: Value) { - let mut db_lock = borrow_db_mut_checked(); + println!("{}", new_settings); + let mut db_lock = DB.borrow_data_mut().unwrap(); let mut current_settings = serde_json::to_value(db_lock.settings.clone()).unwrap(); for (key, value) in new_settings.as_object().unwrap() { current_settings[key] = value.clone(); } + println!("New settings unset: {}", ¤t_settings); let new_settings: Settings = serde_json::from_value(current_settings).unwrap(); db_lock.settings = new_settings; - println!("new Settings: {:?}", db_lock.settings); -} -#[tauri::command] -pub fn fetch_settings() -> Settings { - borrow_db_checked().settings.clone() + println!("New Settings: {:?}", db_lock.settings); } + #[tauri::command] pub fn fetch_system_data() -> SystemData { - let db_handle = borrow_db_checked(); + let db_handle = DB.borrow_data().unwrap(); SystemData::new( db_handle.auth.as_ref().unwrap().client_id.clone(), db_handle.base_url.clone(), diff --git a/src-tauri/src/database/db.rs b/src-tauri/src/database/db.rs index b92de3d..376bcb0 100644 --- a/src-tauri/src/database/db.rs +++ b/src-tauri/src/database/db.rs @@ -1,14 +1,13 @@ use std::{ collections::HashMap, fs::{self, create_dir_all}, - hash::Hash, path::{Path, PathBuf}, - sync::{LazyLock, Mutex, RwLockReadGuard, RwLockWriteGuard}, + sync::{LazyLock, Mutex, RwLockWriteGuard}, }; use chrono::Utc; use directories::BaseDirs; -use log::{debug, error, info}; +use log::{debug, info}; use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_with::serde_as; @@ -57,23 +56,11 @@ pub enum ApplicationTransientStatus { #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct GameVersion { - pub game_id: String, - pub version_name: String, - - pub platform: Platform, - - pub launch_command: String, - pub launch_args: Vec, - - pub setup_command: String, - pub setup_args: Vec, - - pub only_setup: bool, - pub version_index: usize, - pub delta: bool, - - pub umu_id_override: Option, + pub version_name: String, + pub launch_command: String, + pub setup_command: String, + pub platform: Platform, } #[serde_as] @@ -104,18 +91,10 @@ impl Database { Self { applications: DatabaseApplications { install_dirs: vec![games_base_dir.into()], - game_statuses: HashMap::new(), - game_versions: HashMap::new(), - installed_game_version: HashMap::new(), - transient_statuses: HashMap::new(), + ..Default::default() }, prev_database, - base_url: "".to_owned(), - auth: None, - settings: Settings { - autostart: false, - max_download_threads: 4, - }, + ..Default::default() } } } @@ -151,7 +130,7 @@ impl DatabaseImpls for DatabaseInterface { let games_base_dir = data_root_dir.join("games"); let logs_root_dir = data_root_dir.join("logs"); - debug!("creating data directory at {:?}", data_root_dir); + debug!("Creating data directory at {:?}", data_root_dir); create_dir_all(data_root_dir.clone()).unwrap(); create_dir_all(games_base_dir.clone()).unwrap(); create_dir_all(logs_root_dir.clone()).unwrap(); @@ -190,10 +169,10 @@ pub fn set_game_status, &Downloada meta: DownloadableMetadata, setter: F, ) { - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); setter(&mut db_handle, &meta); drop(db_handle); - save_db(); + DB.save().unwrap(); let status = GameStatusManager::fetch_state(&meta.id); @@ -208,49 +187,16 @@ fn handle_invalid_database( let new_path = { let time = Utc::now().timestamp(); let mut base = db_path.clone(); - base.set_file_name(format!("drop.db.backup-{}", time)); + base.set_file_name(format!("drop.db.backup-{}", time.to_string())); base }; - info!( - "old database stored at: {}", - new_path.to_string_lossy().to_string() - ); + info!("{:?}", new_path); fs::rename(&db_path, &new_path).unwrap(); let db = Database::new( games_base_dir.into_os_string().into_string().unwrap(), - Some(new_path), + Some(new_path.into()), ); PathDatabase::create_at_path(db_path, db).expect("Database could not be created") } - -pub fn borrow_db_checked<'a>() -> RwLockReadGuard<'a, Database> { - match DB.borrow_data() { - Ok(data) => data, - Err(e) => { - error!("database borrow failed with error {}", e); - panic!("database borrow failed with error {}", e); - } - } -} - -pub fn borrow_db_mut_checked<'a>() -> RwLockWriteGuard<'a, Database> { - match DB.borrow_data_mut() { - Ok(data) => data, - Err(e) => { - error!("database borrow mut failed with error {}", e); - panic!("database borrow mut failed with error {}", e); - } - } -} - -pub fn save_db() { - match DB.save() { - Ok(_) => {} - Err(e) => { - error!("database failed to save with error {}", e); - panic!("database failed to save with error {}", e) - } - } -} diff --git a/src-tauri/src/database/debug.rs b/src-tauri/src/database/debug.rs index 45d2034..8547b73 100644 --- a/src-tauri/src/database/debug.rs +++ b/src-tauri/src/database/debug.rs @@ -1,3 +1,4 @@ +use crate::{DATA_ROOT_DIR, DB}; use serde::Serialize; #[derive(Serialize)] diff --git a/src-tauri/src/download_manager/commands.rs b/src-tauri/src/download_manager/commands.rs index 0a65c0d..aa69aae 100644 --- a/src-tauri/src/download_manager/commands.rs +++ b/src-tauri/src/download_manager/commands.rs @@ -1,4 +1,4 @@ -use std::sync::Mutex; +use std::sync::{mpsc::SendError, Arc, Mutex}; use crate::{download_manager::downloadable_metadata::DownloadableMetadata, AppState}; diff --git a/src-tauri/src/download_manager/download_manager.rs b/src-tauri/src/download_manager/download_manager.rs index b3dee68..8e5474a 100644 --- a/src-tauri/src/download_manager/download_manager.rs +++ b/src-tauri/src/download_manager/download_manager.rs @@ -4,12 +4,12 @@ use std::{ fmt::Debug, sync::{ mpsc::{SendError, Sender}, - Mutex, MutexGuard, + MutexGuard, }, thread::JoinHandle, }; -use log::{debug, info}; +use log::info; use serde::Serialize; use crate::error::application_download_error::ApplicationDownloadError; @@ -84,7 +84,7 @@ pub enum DownloadStatus { /// which provides raw access to the underlying queue. /// THIS EDITING IS BLOCKING!!! pub struct DownloadManager { - terminator: Mutex>>>, + terminator: JoinHandle>, download_queue: Queue, progress: CurrentProgressObject, command_sender: Sender, @@ -99,7 +99,7 @@ impl DownloadManager { command_sender: Sender, ) -> Self { Self { - terminator: Mutex::new(Some(terminator)), + terminator, download_queue, progress, command_sender, @@ -110,7 +110,7 @@ impl DownloadManager { &self, download: DownloadAgent, ) -> Result<(), SendError> { - info!("creating download with meta {:?}", download.metadata()); + info!("Adding download id {:?}", download.metadata()); self.command_sender .send(DownloadManagerSignal::Queue(download))?; self.command_sender.send(DownloadManagerSignal::Go) @@ -151,14 +151,12 @@ impl DownloadManager { .unwrap(); } - debug!( - "moving download at index {} to index {}", - current_index, new_index - ); + info!("moving {} to {}", current_index, new_index); let mut queue = self.edit(); let to_move = queue.remove(current_index).unwrap(); queue.insert(new_index, to_move); + info!("new queue: {:?}", queue); drop(queue); if needs_pause { @@ -167,10 +165,6 @@ impl DownloadManager { self.command_sender .send(DownloadManagerSignal::UpdateUIQueue) .unwrap(); - self.command_sender - .send(DownloadManagerSignal::Go) - .unwrap(); - } pub fn pause_downloads(&self) { self.command_sender @@ -180,12 +174,11 @@ impl DownloadManager { pub fn resume_downloads(&self) { self.command_sender.send(DownloadManagerSignal::Go).unwrap(); } - pub fn ensure_terminated(&self) -> Result, Box> { + pub fn ensure_terminated(self) -> Result, Box> { self.command_sender .send(DownloadManagerSignal::Finish) .unwrap(); - let terminator = self.terminator.lock().unwrap().take(); - terminator.unwrap().join() + self.terminator.join() } pub fn uninstall_application(&self, meta: DownloadableMetadata) { self.command_sender diff --git a/src-tauri/src/download_manager/download_manager_builder.rs b/src-tauri/src/download_manager/download_manager_builder.rs index dd43dc2..4738730 100644 --- a/src-tauri/src/download_manager/download_manager_builder.rs +++ b/src-tauri/src/download_manager/download_manager_builder.rs @@ -7,7 +7,7 @@ use std::{ thread::{spawn, JoinHandle}, }; -use log::{debug, error, info, warn}; +use log::{debug, error, info}; use tauri::{AppHandle, Emitter}; use crate::{ @@ -179,13 +179,13 @@ impl DownloadManagerBuilder { } } fn manage_queue_signal(&mut self, download_agent: DownloadAgent) { - debug!("got signal Queue"); + info!("Got signal Queue"); let meta = download_agent.metadata(); - debug!("queue metadata: {:?}", meta); + info!("Meta: {:?}", meta); if self.download_queue.exists(meta.clone()) { - warn!("download with same ID already exists"); + info!("Download with same ID already exists"); return; } @@ -199,9 +199,9 @@ impl DownloadManagerBuilder { } fn manage_go_signal(&mut self) { - debug!("got signal Go"); + info!("Got signal Go"); if self.download_agent_registry.is_empty() { - debug!( + info!( "Download agent registry: {:?}", self.download_agent_registry.len() ); @@ -209,21 +209,19 @@ impl DownloadManagerBuilder { } if self.current_download_agent.is_some() { - if self.download_queue.read().front().unwrap() == &self.current_download_agent.as_ref().unwrap().metadata() { - debug!( - "Current download agent: {:?}", - self.current_download_agent.as_ref().unwrap().metadata() - ); - return; - } + info!( + "Current download agent: {:?}", + self.current_download_agent.as_ref().unwrap().metadata() + ); + return; } - debug!("current download queue: {:?}", self.download_queue.read()); + info!("Current download queue: {:?}", self.download_queue.read()); // Should always be Some if the above two statements keep going let agent_data = self.download_queue.read().front().unwrap().clone(); - info!("starting download for {:?}", agent_data); + info!("Starting download for {:?}", agent_data); let download_agent = self .download_agent_registry @@ -267,7 +265,7 @@ impl DownloadManagerBuilder { active_control_flag.set(DownloadThreadControlFlag::Go); } fn manage_stop_signal(&mut self) { - debug!("got signal Stop"); + info!("Got signal Stop"); if let Some(active_control_flag) = self.active_control_flag.clone() { self.set_status(DownloadManagerStatus::Paused); @@ -275,9 +273,10 @@ impl DownloadManagerBuilder { } } fn manage_completed_signal(&mut self, meta: DownloadableMetadata) { - debug!("got signal Completed"); + info!("Got signal Completed"); if let Some(interface) = &self.current_download_agent { if interface.metadata() == meta { + info!("Popping consumed data"); self.remove_and_cleanup_front_download(&meta); } } @@ -285,17 +284,18 @@ impl DownloadManagerBuilder { self.sender.send(DownloadManagerSignal::Go).unwrap(); } fn manage_error_signal(&mut self, error: ApplicationDownloadError) { - debug!("got signal Error"); - if let Some(current_agent) = self.current_download_agent.clone() { - current_agent.on_error(&self.app_handle, error.clone()); + info!("Got signal Error"); + let current_agent = self.current_download_agent.clone().unwrap(); + + current_agent.on_error(&self.app_handle, error.clone()); + + self.stop_and_wait_current_download(); + self.remove_and_cleanup_front_download(¤t_agent.metadata()); - self.stop_and_wait_current_download(); - self.remove_and_cleanup_front_download(¤t_agent.metadata()); - } self.set_status(DownloadManagerStatus::Error(error)); } fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) { - debug!("got signal Cancel"); + info!("Got signal Cancel"); if let Some(current_download) = &self.current_download_agent { if ¤t_download.metadata() == meta { @@ -306,30 +306,32 @@ impl DownloadManagerBuilder { self.download_queue.pop_front(); self.cleanup_current_download(); - debug!("current download queue: {:?}", self.download_queue.read()); + info!("Current donwload queue: {:?}", self.download_queue.read()); } // TODO: Collapse these two into a single if statement somehow else if let Some(download_agent) = self.download_agent_registry.get(meta) { + info!("Object exists in registry"); let index = self.download_queue.get_by_meta(meta); if let Some(index) = index { download_agent.on_cancelled(&self.app_handle); let _ = self.download_queue.edit().remove(index).unwrap(); let removed = self.download_agent_registry.remove(meta); - debug!( - "removed {:?} from queue {:?}", + info!( + "Removed {:?} from queue {:?}", removed.map(|x| x.metadata()), self.download_queue.read() ); } } } else if let Some(download_agent) = self.download_agent_registry.get(meta) { + info!("Object exists in registry"); let index = self.download_queue.get_by_meta(meta); if let Some(index) = index { download_agent.on_cancelled(&self.app_handle); let _ = self.download_queue.edit().remove(index).unwrap(); let removed = self.download_agent_registry.remove(meta); - debug!( - "removed {:?} from queue {:?}", + info!( + "Removed {:?} from queue {:?}", removed.map(|x| x.metadata()), self.download_queue.read() ); @@ -352,8 +354,6 @@ impl DownloadManagerBuilder { meta: DownloadableMetadata::clone(key), status: val.status(), progress: val.progress().get_progress(), - current: val.progress().sum(), - max: val.progress().get_max(), } }) .collect(); diff --git a/src-tauri/src/download_manager/internal_error.rs b/src-tauri/src/download_manager/internal_error.rs deleted file mode 100644 index 4864599..0000000 --- a/src-tauri/src/download_manager/internal_error.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::{fmt::Display, io, sync::mpsc::SendError}; - -use serde_with::SerializeDisplay; - -#[derive(SerializeDisplay)] -pub enum InternalError { - IOError(io::Error), - SignalError(SendError), -} -impl Display for InternalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InternalError::IOError(error) => write!(f, "{}", error), - InternalError::SignalError(send_error) => write!(f, "{}", send_error), - } - } -} -impl From> for InternalError { - fn from(value: SendError) -> Self { - InternalError::SignalError(value) - } -} -impl From for InternalError { - fn from(value: io::Error) -> Self { - InternalError::IOError(value) - } -} diff --git a/src-tauri/src/download_manager/mod.rs b/src-tauri/src/download_manager/mod.rs index 0bac198..0d664dd 100644 --- a/src-tauri/src/download_manager/mod.rs +++ b/src-tauri/src/download_manager/mod.rs @@ -4,7 +4,5 @@ pub mod download_manager_builder; pub mod download_thread_control_flag; pub mod downloadable; pub mod downloadable_metadata; -pub mod internal_error; pub mod progress_object; pub mod queue; -pub mod rolling_progress_updates; diff --git a/src-tauri/src/download_manager/progress_object.rs b/src-tauri/src/download_manager/progress_object.rs index fc0907b..5d65207 100644 --- a/src-tauri/src/download_manager/progress_object.rs +++ b/src-tauri/src/download_manager/progress_object.rs @@ -2,17 +2,15 @@ use std::{ sync::{ atomic::{AtomicUsize, Ordering}, mpsc::Sender, - Arc, Mutex, + Arc, Mutex, RwLock, }, time::{Duration, Instant}, }; -use atomic_instant_full::AtomicInstant; +use log::info; use throttle_my_fn::throttle; -use super::{ - download_manager::DownloadManagerSignal, rolling_progress_updates::RollingProgressWindow, -}; +use super::download_manager::DownloadManagerSignal; #[derive(Clone)] pub struct ProgressObject { @@ -20,10 +18,10 @@ pub struct ProgressObject { progress_instances: Arc>>>, start: Arc>, sender: Sender, - //last_update: Arc>, - last_update_time: Arc, - bytes_last_update: Arc, - rolling: RollingProgressWindow<250>, + points_towards_update: Arc, + points_to_push_update: Arc, + last_update: Arc>, + amount_last_update: Arc, } pub struct ProgressHandle { @@ -44,32 +42,67 @@ impl ProgressHandle { pub fn add(&self, amount: usize) { self.progress .fetch_add(amount, std::sync::atomic::Ordering::Relaxed); - calculate_update(&self.progress_object); - } - pub fn skip(&self, amount: usize) { - self.progress - .fetch_add(amount, std::sync::atomic::Ordering::Relaxed); - // Offset the bytes at last offset by this amount - self.progress_object - .bytes_last_update - .fetch_add(amount, Ordering::Relaxed); - // Dont' fire update + self.progress_object.check_push_update(amount); } } +static PROGRESS_UPDATES: usize = 100; + 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 + let points_to_push_update = max / PROGRESS_UPDATES; Self { max: Arc::new(Mutex::new(max)), progress_instances: Arc::new(arr), start: Arc::new(Mutex::new(Instant::now())), sender, - last_update_time: Arc::new(AtomicInstant::now()), - bytes_last_update: Arc::new(AtomicUsize::new(0)), - rolling: RollingProgressWindow::new(), + points_towards_update: Arc::new(AtomicUsize::new(0)), + points_to_push_update: Arc::new(AtomicUsize::new(points_to_push_update)), + last_update: Arc::new(RwLock::new(Instant::now())), + amount_last_update: Arc::new(AtomicUsize::new(0)), + } + } + + pub fn check_push_update(&self, amount_added: usize) { + let current_amount = self + .points_towards_update + .fetch_add(amount_added, Ordering::Relaxed); + + let to_update = self.points_to_push_update.fetch_add(0, Ordering::Relaxed); + + if current_amount >= to_update { + self.points_towards_update + .fetch_sub(to_update, Ordering::Relaxed); + update_queue(&self); + } + + let last_update = self.last_update.read().unwrap(); + let last_update_difference = Instant::now().duration_since(*last_update).as_millis(); + if last_update_difference > 1000 { + // push update + drop(last_update); + let mut last_update = self.last_update.write().unwrap(); + *last_update = Instant::now(); + drop(last_update); + + let current_amount = self.sum(); + let max = self.get_max(); + let amount_at_last_update = self.amount_last_update.fetch_add(0, Ordering::Relaxed); + self.amount_last_update + .store(current_amount, Ordering::Relaxed); + + let amount_since_last_update = current_amount - amount_at_last_update; + + let kilobytes_per_second = + amount_since_last_update / (last_update_difference as usize).max(1); + + let remaining = max - current_amount; // bytes + let time_remaining = (remaining / 1000) / kilobytes_per_second.max(1); + + update_ui(&self, kilobytes_per_second, time_remaining); } } @@ -89,6 +122,9 @@ impl ProgressObject { } pub fn set_max(&self, new_max: usize) { *self.max.lock().unwrap() = new_max; + self.points_to_push_update + .store(new_max / PROGRESS_UPDATES, Ordering::Relaxed); + info!("points to push update: {}", new_max / PROGRESS_UPDATES); } pub fn set_size(&self, length: usize) { *self.progress_instances.lock().unwrap() = @@ -100,43 +136,9 @@ impl ProgressObject { pub fn get(&self, index: usize) -> Arc { self.progress_instances.lock().unwrap()[index].clone() } - fn update_window(&self, kilobytes_per_second: usize) { - self.rolling.update(kilobytes_per_second); - } -} - -#[throttle(1, Duration::from_millis(20))] -pub fn calculate_update(progress: &ProgressObject) { - let last_update_time = progress - .last_update_time - .swap(Instant::now(), Ordering::SeqCst); - let time_since_last_update = Instant::now().duration_since(last_update_time).as_millis(); - - let current_bytes_downloaded = progress.sum(); - let max = progress.get_max(); - let bytes_at_last_update = progress - .bytes_last_update - .swap(current_bytes_downloaded, Ordering::Relaxed); - - let bytes_since_last_update = current_bytes_downloaded - bytes_at_last_update; - - let kilobytes_per_second = bytes_since_last_update / (time_since_last_update as usize).max(1); - - let bytes_remaining = max - current_bytes_downloaded; // bytes - - progress.update_window(kilobytes_per_second); - push_update(progress, bytes_remaining); -} - -#[throttle(1, Duration::from_millis(500))] -pub fn push_update(progress: &ProgressObject, bytes_remaining: usize) { - let average_speed = progress.rolling.get_average(); - let time_remaining = (bytes_remaining / 1000) / average_speed.max(1); - - update_ui(progress, average_speed, time_remaining); - update_queue(progress); } +#[throttle(50, Duration::from_secs(1))] fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time_remaining: usize) { progress_object .sender @@ -147,9 +149,11 @@ fn update_ui(progress_object: &ProgressObject, kilobytes_per_second: usize, time .unwrap(); } +#[throttle(50, Duration::from_secs(1))] fn update_queue(progress: &ProgressObject) { progress .sender .send(DownloadManagerSignal::UpdateUIQueue) .unwrap(); } + diff --git a/src-tauri/src/download_manager/queue.rs b/src-tauri/src/download_manager/queue.rs index f3e9493..8025c91 100644 --- a/src-tauri/src/download_manager/queue.rs +++ b/src-tauri/src/download_manager/queue.rs @@ -32,7 +32,7 @@ impl Queue { pub fn pop_front(&self) -> Option { self.edit().pop_front() } - pub fn is_empty(&self) -> bool { + pub fn empty(&self) -> bool { self.inner.lock().unwrap().len() == 0 } pub fn exists(&self, meta: DownloadableMetadata) -> bool { @@ -52,7 +52,10 @@ impl Queue { } pub fn pop_front_if_equal(&self, meta: &DownloadableMetadata) -> Option { let mut queue = self.edit(); - let front = queue.front()?; + let front = match queue.front() { + Some(front) => front, + None => return None, + }; if front == meta { return queue.pop_front(); } diff --git a/src-tauri/src/download_manager/rolling_progress_updates.rs b/src-tauri/src/download_manager/rolling_progress_updates.rs deleted file mode 100644 index 2239b9a..0000000 --- a/src-tauri/src/download_manager/rolling_progress_updates.rs +++ /dev/null @@ -1,33 +0,0 @@ -use std::sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, -}; - -#[derive(Clone)] -pub struct RollingProgressWindow { - window: Arc<[AtomicUsize; S]>, - current: Arc, -} -impl RollingProgressWindow { - pub fn new() -> Self { - Self { - window: Arc::new([(); S].map(|_| AtomicUsize::new(0))), - current: Arc::new(AtomicUsize::new(0)), - } - } - pub fn update(&self, kilobytes_per_second: usize) { - let index = self.current.fetch_add(1, Ordering::SeqCst); - let current = &self.window[index % S]; - current.store(kilobytes_per_second, Ordering::SeqCst); - } - pub fn get_average(&self) -> usize { - let current = self.current.load(Ordering::SeqCst); - self.window - .iter() - .enumerate() - .filter(|(i, _)| i < ¤t) - .map(|(_, x)| x.load(Ordering::Relaxed)) - .sum::() - / S - } -} diff --git a/src-tauri/src/error/application_download_error.rs b/src-tauri/src/error/application_download_error.rs index d68bd71..fb6035a 100644 --- a/src-tauri/src/error/application_download_error.rs +++ b/src-tauri/src/error/application_download_error.rs @@ -3,12 +3,10 @@ use std::{ io, }; -use serde_with::SerializeDisplay; - use super::{remote_access_error::RemoteAccessError, setup_error::SetupError}; // TODO: Rename / separate from downloads -#[derive(Debug, Clone, SerializeDisplay)] +#[derive(Debug, Clone)] pub enum ApplicationDownloadError { Communication(RemoteAccessError), Checksum, @@ -22,11 +20,11 @@ impl Display for ApplicationDownloadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { ApplicationDownloadError::Communication(error) => write!(f, "{}", error), - ApplicationDownloadError::Setup(error) => write!(f, "an error occurred while setting up the download: {}", 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::Setup(error) => write!(f, "An error occurred while setting up the download: {}", 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::IoError(error) => write!(f, "{}", 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"), } } } diff --git a/src-tauri/src/error/library_error.rs b/src-tauri/src/error/library_error.rs index c13dd23..4957ae9 100644 --- a/src-tauri/src/error/library_error.rs +++ b/src-tauri/src/error/library_error.rs @@ -1,8 +1,5 @@ use std::fmt::Display; -use serde_with::SerializeDisplay; - -#[derive(SerializeDisplay)] pub enum LibraryError { MetaNotFound(String), } diff --git a/src-tauri/src/error/mod.rs b/src-tauri/src/error/mod.rs index 89b74ae..a38a696 100644 --- a/src-tauri/src/error/mod.rs +++ b/src-tauri/src/error/mod.rs @@ -4,3 +4,4 @@ pub mod library_error; pub mod process_error; pub mod remote_access_error; pub mod setup_error; +pub mod user_error; diff --git a/src-tauri/src/error/process_error.rs b/src-tauri/src/error/process_error.rs index 8afc9dc..8a105a7 100644 --- a/src-tauri/src/error/process_error.rs +++ b/src-tauri/src/error/process_error.rs @@ -1,8 +1,5 @@ use std::{fmt::Display, io::Error}; -use serde_with::SerializeDisplay; - -#[derive(SerializeDisplay)] pub enum ProcessError { SetupRequired, NotInstalled, diff --git a/src-tauri/src/error/remote_access_error.rs b/src-tauri/src/error/remote_access_error.rs index 32572b2..40740b2 100644 --- a/src-tauri/src/error/remote_access_error.rs +++ b/src-tauri/src/error/remote_access_error.rs @@ -5,12 +5,11 @@ use std::{ }; use http::StatusCode; -use serde_with::SerializeDisplay; use url::ParseError; use super::drop_server_error::DropServerError; -#[derive(Debug, Clone, SerializeDisplay)] +#[derive(Debug, Clone)] pub enum RemoteAccessError { FetchError(Arc), ParsingError(ParseError), @@ -40,17 +39,17 @@ impl Display for RemoteAccessError { RemoteAccessError::ParsingError(parse_error) => { write!(f, "{}", parse_error) } - RemoteAccessError::InvalidEndpoint => write!(f, "invalid drop endpoint"), - RemoteAccessError::HandshakeFailed(message) => write!(f, "failed to complete handshake: {}", message), - RemoteAccessError::GameNotFound => write!(f, "could not find game on server"), - RemoteAccessError::InvalidResponse(error) => write!(f, "server returned an invalid response: {} {}", error.status_code, error.status_message), - RemoteAccessError::InvalidRedirect => write!(f, "server redirect was invalid"), + RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"), + RemoteAccessError::HandshakeFailed(message) => write!(f, "Failed to complete handshake: {}", message), + RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"), + RemoteAccessError::InvalidResponse(error) => write!(f, "Server returned an invalid response: {} {}", error.status_code, error.status_message), + RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"), RemoteAccessError::ManifestDownloadFailed(status, response) => write!( f, - "failed to download game manifest: {} {}", + "Failed to download game manifest: {} {}", status, response ), - RemoteAccessError::OutOfSync => write!(f, "server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other"), + RemoteAccessError::OutOfSync => write!(f, "Server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other."), RemoteAccessError::Generic(message) => write!(f, "{}", message), } } diff --git a/src-tauri/src/error/setup_error.rs b/src-tauri/src/error/setup_error.rs index bd76ce5..bae7fea 100644 --- a/src-tauri/src/error/setup_error.rs +++ b/src-tauri/src/error/setup_error.rs @@ -8,7 +8,7 @@ pub enum SetupError { impl Display for SetupError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - SetupError::Context => write!(f, "failed to generate contexts for download"), + SetupError::Context => write!(f, "Failed to generate contexts for download"), } } } diff --git a/src-tauri/src/error/user_error.rs b/src-tauri/src/error/user_error.rs new file mode 100644 index 0000000..bb019fc --- /dev/null +++ b/src-tauri/src/error/user_error.rs @@ -0,0 +1,66 @@ +use std::{ + fmt::Display, + ops::{FromResidual, Try}, +}; + +use serde::Serialize; + +pub enum UserValue +where + T: Serialize, + D: Display, +{ + Ok(T), + Err(D), +} +impl Serialize for UserValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + UserValue::Ok(data) => data.serialize(serializer), + UserValue::Err(err) => serializer.serialize_str(err.to_string().as_ref()), + } + } +} + +impl From> for UserValue { + fn from(value: Result) -> Self { + match value { + Ok(data) => UserValue::Ok(data), + Err(data) => UserValue::Err(data), + } + } +} + +impl Try for UserValue { + type Output = T; + + type Residual = D; + + fn from_output(output: Self::Output) -> Self { + Self::Ok(output) + } + + fn branch(self) -> std::ops::ControlFlow { + match self { + UserValue::Ok(data) => std::ops::ControlFlow::Continue(data), + UserValue::Err(e) => std::ops::ControlFlow::Break(e), + } + } +} +impl FromResidual for UserValue { + fn from_residual(residual: ::Residual) -> Self { + UserValue::Err(residual) + } +} +impl FromResidual> for UserValue { + fn from_residual(residual: Result) -> Self { + match residual { + Ok(_) => unreachable!(), + Err(e) => UserValue::Err(e), + } + } +} + diff --git a/src-tauri/src/games/commands.rs b/src-tauri/src/games/commands.rs index 8f1d652..beec0bc 100644 --- a/src-tauri/src/games/commands.rs +++ b/src-tauri/src/games/commands.rs @@ -3,28 +3,32 @@ use std::sync::Mutex; use tauri::AppHandle; use crate::{ - database::db::GameVersion, error::{library_error::LibraryError, remote_access_error::RemoteAccessError}, games::library::{get_current_meta, uninstall_game_logic}, AppState + error::{ + library_error::LibraryError, remote_access_error::RemoteAccessError, user_error::UserValue, + }, + games::library::{get_current_meta, uninstall_game_logic}, + AppState, }; use super::{ library::{ fetch_game_logic, fetch_game_verion_options_logic, fetch_library_logic, FetchGameStruct, - Game, + Game, GameVersionOption, }, state::{GameStatusManager, GameStatusWithTransient}, }; #[tauri::command] -pub fn fetch_library(app: AppHandle) -> Result, RemoteAccessError> { - fetch_library_logic(app) +pub fn fetch_library(app: AppHandle) -> UserValue, RemoteAccessError> { + fetch_library_logic(app).into() } #[tauri::command] pub fn fetch_game( game_id: String, app: tauri::AppHandle, -) -> Result { - fetch_game_logic(game_id, app) +) -> UserValue { + fetch_game_logic(game_id, app).into() } #[tauri::command] @@ -33,21 +37,21 @@ pub fn fetch_game_status(id: String) -> GameStatusWithTransient { } #[tauri::command] -pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> Result<(), LibraryError> { +pub fn uninstall_game(game_id: String, app_handle: AppHandle) -> UserValue<(), LibraryError> { let meta = match get_current_meta(&game_id) { Some(data) => data, - None => return Err(LibraryError::MetaNotFound(game_id)), + None => return UserValue::Err(LibraryError::MetaNotFound(game_id)), }; println!("{:?}", meta); uninstall_game_logic(meta, &app_handle); - Ok(()) + UserValue::Ok(()) } #[tauri::command] pub fn fetch_game_verion_options( game_id: String, state: tauri::State<'_, Mutex>, -) -> Result, RemoteAccessError> { - fetch_game_verion_options_logic(game_id, state) +) -> UserValue, RemoteAccessError> { + fetch_game_verion_options_logic(game_id, state).into() } diff --git a/src-tauri/src/games/downloads/commands.rs b/src-tauri/src/games/downloads/commands.rs index 67b1359..d3c604a 100644 --- a/src-tauri/src/games/downloads/commands.rs +++ b/src-tauri/src/games/downloads/commands.rs @@ -1,10 +1,8 @@ -use std::sync::{Arc, Mutex}; +use std::sync::{mpsc::SendError, Arc, Mutex}; use crate::{ - download_manager::{ - download_manager::DownloadManagerSignal, downloadable::Downloadable, - internal_error::InternalError, - }, + download_manager::{download_manager::DownloadManagerSignal, downloadable::Downloadable}, + error::user_error::UserValue, AppState, }; @@ -16,7 +14,7 @@ pub fn download_game( game_version: String, install_dir: usize, state: tauri::State<'_, Mutex>, -) -> Result<(), InternalError> { +) -> UserValue<(), SendError> { let sender = state.lock().unwrap().download_manager.get_sender(); let game_download_agent = Arc::new(Box::new(GameDownloadAgent::new( game_id, @@ -24,9 +22,10 @@ pub fn download_game( install_dir, sender, )) as Box); - Ok(state + state .lock() .unwrap() .download_manager - .queue_download(game_download_agent)?) + .queue_download(game_download_agent) + .into() } diff --git a/src-tauri/src/games/downloads/download_agent.rs b/src-tauri/src/games/downloads/download_agent.rs index d4fb1cd..a9da4c3 100644 --- a/src-tauri/src/games/downloads/download_agent.rs +++ b/src-tauri/src/games/downloads/download_agent.rs @@ -1,7 +1,6 @@ use crate::auth::generate_authorization_header; use crate::database::db::{ - borrow_db_checked, set_game_status, ApplicationTransientStatus, DatabaseImpls, - GameDownloadStatus, + set_game_status, ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus, }; use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus}; use crate::download_manager::download_thread_control_flag::{ @@ -14,11 +13,11 @@ use crate::error::application_download_error::ApplicationDownloadError; use crate::error::remote_access_error::RemoteAccessError; use crate::games::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::games::library::{on_game_complete, push_game_update, GameUpdateEvent}; -use crate::remote::requests::make_request; use crate::DB; use log::{debug, error, info}; use rayon::ThreadPoolBuilder; use slice_deque::SliceDeque; +use std::collections::HashMap; use std::fs::{create_dir_all, File}; use std::path::Path; use std::sync::mpsc::Sender; @@ -56,7 +55,7 @@ impl GameDownloadAgent { // Don't run by default let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); - let db_lock = borrow_db_checked(); + let db_lock = DB.borrow_data().unwrap(); let base_dir = db_lock.applications.install_dirs[target_download_dir].clone(); drop(db_lock); @@ -83,8 +82,10 @@ impl GameDownloadAgent { // Blocking pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> { self.ensure_manifest_exists()?; + info!("Ensured manifest exists"); self.ensure_contexts()?; + info!("Ensured contexts exists"); self.control_flag.set(DownloadThreadControlFlag::Go); @@ -93,8 +94,11 @@ impl GameDownloadAgent { // Blocking pub fn download(&self, app_handle: &AppHandle) -> Result { + info!("Setting up download"); self.setup_download()?; + info!("Setting progress object params"); self.set_progress_object_params(); + info!("Running"); let timer = Instant::now(); push_game_update( app_handle, @@ -110,7 +114,7 @@ impl GameDownloadAgent { .run() .map_err(|_| ApplicationDownloadError::DownloadError); - debug!( + info!( "{} took {}ms to download", self.id, timer.elapsed().as_millis() @@ -127,17 +131,25 @@ impl GameDownloadAgent { } fn download_manifest(&self) -> Result<(), ApplicationDownloadError> { + let base_url = DB.fetch_base_url(); + let manifest_url = base_url + .join( + format!( + "/api/v1/client/game/manifest?id={}&version={}", + self.id, + encode(&self.version) + ) + .as_str(), + ) + .unwrap(); + let header = generate_authorization_header(); let client = reqwest::blocking::Client::new(); - let response = make_request( - &client, - &["/api/v1/client/game/manifest"], - &[("id", &self.id), ("version", &self.version)], - |f| f.header("Authorization", header), - ) - .map_err(|e| ApplicationDownloadError::Communication(e))? - .send() - .map_err(|e| ApplicationDownloadError::Communication(e.into()))?; + let response = client + .get(manifest_url.to_string()) + .header("Authorization", header) + .send() + .unwrap(); if response.status() != 200 { return Err(ApplicationDownloadError::Communication( @@ -170,8 +182,11 @@ impl GameDownloadAgent { let chunk_count = contexts.iter().map(|chunk| chunk.length).sum(); + debug!("Setting ProgressObject max to {}", chunk_count); self.progress.set_max(chunk_count); + debug!("Setting ProgressObject size to {}", length); self.progress.set_size(length); + debug!("Setting ProgressObject time to now"); self.progress.set_time_now(); } @@ -235,9 +250,9 @@ impl GameDownloadAgent { // TODO: Change return value on Err pub fn run(&self) -> Result { - let max_download_threads = borrow_db_checked().settings.max_download_threads; + let max_download_threads = DB.borrow_data().unwrap().settings.max_download_threads; - debug!( + info!( "downloading game: {} with {} threads", self.id, max_download_threads ); @@ -249,6 +264,8 @@ impl GameDownloadAgent { let completed_indexes = Arc::new(boxcar::Vec::new()); let completed_indexes_loop_arc = completed_indexes.clone(); + let base_url = DB.fetch_base_url(); + let contexts = self.contexts.lock().unwrap(); pool.scope(|scope| { let client = &reqwest::blocking::Client::new(); @@ -258,32 +275,15 @@ impl GameDownloadAgent { let progress = self.progress.get(index); let progress_handle = ProgressHandle::new(progress, self.progress.clone()); - // If we've done this one already, skip it if self.completed_contexts.lock().unwrap().contains(&index) { - progress_handle.skip(context.length); + progress_handle.add(context.length); continue; } let sender = self.sender.clone(); - let request = match make_request( - &client, - &["/api/v1/client/chunk"], - &[ - ("id", &context.game_id), - ("version", &context.version), - ("name", &context.file_name), - ("chunk", &context.index.to_string()), - ], - |r| r.header("Authorization", generate_authorization_header()), - ) { - Ok(request) => request, - Err(e) => { - sender.send(DownloadManagerSignal::Error(ApplicationDownloadError::Communication(e))).unwrap(); - continue; - }, - }; + let request = generate_request(&base_url, client, context); scope.spawn(move |_| { match download_game_chunk(context, &self.control_flag, progress_handle, request) @@ -298,6 +298,7 @@ impl GameDownloadAgent { sender.send(DownloadManagerSignal::Error(e)).unwrap(); } } + info!("Completed context id {}", index); }); } }); @@ -306,27 +307,28 @@ impl GameDownloadAgent { let completed_lock_len = { let mut completed_contexts_lock = self.completed_contexts.lock().unwrap(); - for (_, item) in newly_completed.iter() { - completed_contexts_lock.push_front(*item); + for (item, _) in newly_completed.iter() { + completed_contexts_lock.push_front(item); } completed_contexts_lock.len() }; + info!("Got newly completed"); + // If we're not out of contexts, we're not done, so we don't fire completed if completed_lock_len != contexts.len() { - info!( - "download agent for {} exited without completing ({}/{})", - self.id.clone(), - completed_lock_len, - contexts.len(), - ); + info!("da for {} exited without completing", self.id.clone()); self.stored_manifest .set_completed_contexts(self.completed_contexts.lock().unwrap().as_slice()); + info!("Setting completed contexts"); self.stored_manifest.write(); + info!("Wrote completed contexts"); return Ok(false); } + info!("Sending completed signal"); + // We've completed self.sender .send(DownloadManagerSignal::Completed(self.metadata())) @@ -336,6 +338,27 @@ impl GameDownloadAgent { } } +fn generate_request( + base_url: &url::Url, + client: reqwest::blocking::Client, + context: &DropDownloadContext, +) -> reqwest::blocking::RequestBuilder { + let chunk_url = base_url + .join(&format!( + "/api/v1/client/chunk?id={}&version={}&name={}&chunk={}", + // Encode the parts we don't trust + context.game_id, + encode(&context.version), + encode(&context.file_name), + context.index + )) + .unwrap(); + + let header = generate_authorization_header(); + + client.get(chunk_url).header("Authorization", header) +} + impl Downloadable for GameDownloadAgent { fn download(&self, app_handle: &AppHandle) -> Result { *self.status.lock().unwrap() = DownloadStatus::Downloading; @@ -384,7 +407,6 @@ impl Downloadable for GameDownloadAgent { .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) { let meta = self.metadata(); *self.status.lock().unwrap() = DownloadStatus::Queued; diff --git a/src-tauri/src/games/downloads/download_logic.rs b/src-tauri/src/games/downloads/download_logic.rs index 70e5b7e..87f057f 100644 --- a/src-tauri/src/games/downloads/download_logic.rs +++ b/src-tauri/src/games/downloads/download_logic.rs @@ -5,12 +5,12 @@ use crate::download_manager::progress_object::ProgressHandle; use crate::error::application_download_error::ApplicationDownloadError; use crate::error::remote_access_error::RemoteAccessError; use crate::games::downloads::manifest::DropDownloadContext; -use log::warn; +use log::{error, warn}; use md5::{Context, Digest}; use reqwest::blocking::{RequestBuilder, Response}; use std::fs::{set_permissions, Permissions}; -use std::io::{ErrorKind, Read}; +use std::io::Read; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::{ @@ -39,17 +39,19 @@ impl DropWriter { // Write automatically pushes to file and hasher impl Write for DropWriter { fn write(&mut self, buf: &[u8]) -> io::Result { + /* self.hasher.write_all(buf).map_err(|e| { io::Error::new( ErrorKind::Other, format!("Unable to write to hasher: {}", e), ) })?; + */ self.destination.write(buf) } fn flush(&mut self) -> io::Result<()> { - self.hasher.flush()?; + // self.hasher.flush()?; self.destination.flush() } } @@ -133,6 +135,7 @@ pub fn download_game_chunk( if response.status() != 200 { let err = response.json().unwrap(); + warn!("{:?}", err); return Err(ApplicationDownloadError::Communication( RemoteAccessError::InvalidResponse(err), )); @@ -148,7 +151,7 @@ pub fn download_game_chunk( let content_length = response.content_length(); if content_length.is_none() { - warn!("recieved 0 length content from server"); + warn!("Recieved 0 length content from server"); return Err(ApplicationDownloadError::Communication( RemoteAccessError::InvalidResponse(response.json().unwrap()), )); @@ -176,14 +179,16 @@ pub fn download_game_chunk( set_permissions(ctx.path.clone(), permissions).unwrap(); } + /* let checksum = pipeline .finish() - .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?; + .map_err(|e| GameDownloadError::IoError(e))?; let res = hex::encode(checksum.0); if res != ctx.checksum { - return Err(ApplicationDownloadError::Checksum); + return Err(GameDownloadError::Checksum); } + */ Ok(true) } diff --git a/src-tauri/src/games/library.rs b/src-tauri/src/games/library.rs index b2f2fe2..ab4cc9e 100644 --- a/src-tauri/src/games/library.rs +++ b/src-tauri/src/games/library.rs @@ -2,20 +2,21 @@ use std::fs::remove_dir_all; use std::sync::Mutex; use std::thread::spawn; -use log::{debug, error, warn}; +use log::{error, info, warn}; use serde::{Deserialize, Serialize}; use tauri::Emitter; use tauri::{AppHandle, Manager}; +use urlencoding::encode; -use crate::database::db::{borrow_db_checked, borrow_db_mut_checked, save_db, GameVersion}; -use crate::database::db::{ApplicationTransientStatus, GameDownloadStatus}; +use crate::database::db::GameVersion; +use crate::database::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus}; use crate::download_manager::download_manager::DownloadStatus; use crate::download_manager::downloadable_metadata::DownloadableMetadata; use crate::error::remote_access_error::RemoteAccessError; use crate::games::state::{GameStatusManager, GameStatusWithTransient}; +use crate::process::process_manager::Platform; use crate::remote::auth::generate_authorization_header; -use crate::remote::requests::make_request; -use crate::AppState; +use crate::{AppState, DB}; #[derive(serde::Serialize)] pub struct FetchGameStruct { @@ -51,8 +52,6 @@ pub struct QueueUpdateEventQueueData { pub meta: DownloadableMetadata, pub status: DownloadStatus, pub progress: f64, - pub current: usize, - pub max: usize, } #[derive(serde::Serialize, Clone)] @@ -66,14 +65,31 @@ pub struct StatsUpdateEvent { pub time: usize, } +// Game version with some fields missing and size information +#[derive(serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GameVersionOption { + version_index: usize, + version_name: String, + platform: Platform, + setup_command: String, + launch_command: String, + delta: bool, + umu_id_override: Option, + // total_size: usize, +} + pub fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> { + let base_url = DB.fetch_base_url(); + let library_url = base_url.join("/api/v1/client/user/library")?; + let header = generate_authorization_header(); let client = reqwest::blocking::Client::new(); - let response = make_request(&client, &["/api/v1/client/user/library"], &[], |f| { - f.header("Authorization", header) - })? - .send()?; + let response = client + .get(library_url.to_string()) + .header("Authorization", header) + .send()?; if response.status() != 200 { let err = response.json().unwrap(); @@ -86,7 +102,7 @@ pub fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessErro let state = app.state::>(); let mut handle = state.lock().unwrap(); - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); for game in games.iter() { handle.games.insert(game.id.clone(), game.clone()); @@ -121,11 +137,17 @@ pub fn fetch_game_logic( return Ok(data); } + + let base_url = DB.fetch_base_url(); + + let endpoint = base_url.join(&format!("/api/v1/game/{}", id))?; + let header = generate_authorization_header(); + let client = reqwest::blocking::Client::new(); - let response = make_request(&client, &["/api/v1/game/", &id], &[], |r| { - r.header("Authorization", generate_authorization_header()) - })? - .send()?; + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; if response.status() == 404 { return Err(RemoteAccessError::GameNotFound); @@ -139,7 +161,7 @@ pub fn fetch_game_logic( let game: Game = response.json()?; state_handle.games.insert(id.clone(), game.clone()); - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle .applications @@ -161,16 +183,18 @@ pub fn fetch_game_logic( pub fn fetch_game_verion_options_logic( game_id: String, state: tauri::State<'_, Mutex>, -) -> Result, RemoteAccessError> { - let client = reqwest::blocking::Client::new(); +) -> Result, RemoteAccessError> { + let base_url = DB.fetch_base_url(); - let response = make_request( - &client, - &["/api/v1/client/game/versions"], - &[("id", &game_id)], - |r| r.header("Authorization", generate_authorization_header()), - )? - .send()?; + let endpoint = + base_url.join(format!("/api/v1/client/game/versions?id={}", game_id).as_str())?; + let header = generate_authorization_header(); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; if response.status() != 200 { let err = response.json().unwrap(); @@ -178,11 +202,11 @@ pub fn fetch_game_verion_options_logic( return Err(RemoteAccessError::InvalidResponse(err)); } - let data: Vec = response.json()?; + let data: Vec = response.json()?; let state_lock = state.lock().unwrap(); let process_manager_lock = state_lock.process_manager.lock().unwrap(); - let data: Vec = data + let data: Vec = data .into_iter() .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap()) .collect(); @@ -193,8 +217,8 @@ pub fn fetch_game_verion_options_logic( } pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) { - println!("triggered uninstall for agent"); - let mut db_handle = borrow_db_mut_checked(); + println!("Triggered uninstall for agent"); + let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle .applications .transient_statuses @@ -209,7 +233,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned(); if previous_state.is_none() { - warn!("uninstall job doesn't have previous state, failing silently"); + info!("uninstall job doesn't have previous state, failing silently"); return; } let previous_state = previous_state.unwrap(); @@ -237,7 +261,7 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) error!("{}", e); } Ok(_) => { - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle.applications.transient_statuses.remove(&meta); db_handle .applications @@ -245,9 +269,9 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) .entry(meta.id.clone()) .and_modify(|e| *e = GameDownloadStatus::Remote {}); drop(db_handle); - save_db(); + DB.save().unwrap(); - debug!("uninstalled game id {}", &meta.id); + info!("uninstalled game id {}", &meta.id); push_game_update( &app_handle, @@ -260,7 +284,8 @@ pub fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) } pub fn get_current_meta(game_id: &String) -> Option { - borrow_db_checked() + DB.borrow_data() + .unwrap() .applications .installed_game_version .get(game_id) @@ -273,27 +298,30 @@ pub fn on_game_complete( app_handle: &AppHandle, ) -> Result<(), RemoteAccessError> { // Fetch game version information from remote + let base_url = DB.fetch_base_url(); if meta.version.is_none() { return Err(RemoteAccessError::GameNotFound); } + let endpoint = base_url.join( + format!( + "/api/v1/client/game/version?id={}&version={}", + meta.id, + encode(meta.version.as_ref().unwrap()) + ) + .as_str(), + )?; let header = generate_authorization_header(); let client = reqwest::blocking::Client::new(); - let response = make_request( - &client, - &["/api/v1/client/metadata/version"], - &[ - ("id", &meta.id), - ("version", meta.version.as_ref().unwrap()), - ], - |f| f.header("Authorization", header), - )? - .send()?; + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; let data: GameVersion = response.json()?; - let mut handle = borrow_db_mut_checked(); + let mut handle = DB.borrow_data_mut().unwrap(); handle .applications .game_versions @@ -306,7 +334,7 @@ pub fn on_game_complete( .insert(meta.id.clone(), meta.clone()); drop(handle); - save_db(); + DB.save().unwrap(); let status = if data.setup_command.is_empty() { GameDownloadStatus::Installed { @@ -320,13 +348,13 @@ pub fn on_game_complete( } }; - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle .applications .game_statuses .insert(meta.id.clone(), status.clone()); drop(db_handle); - save_db(); + DB.save().unwrap(); app_handle .emit( &format!("update_game/{}", meta.id), diff --git a/src-tauri/src/games/state.rs b/src-tauri/src/games/state.rs index 19b1769..29287db 100644 --- a/src-tauri/src/games/state.rs +++ b/src-tauri/src/games/state.rs @@ -1,4 +1,7 @@ -use crate::database::db::{borrow_db_checked, ApplicationTransientStatus, GameDownloadStatus}; +use crate::{ + database::db::{ApplicationTransientStatus, GameDownloadStatus}, + DB, +}; pub type GameStatusWithTransient = ( Option, @@ -8,7 +11,7 @@ pub struct GameStatusManager {} impl GameStatusManager { pub fn fetch_state(game_id: &String) -> GameStatusWithTransient { - let db_lock = borrow_db_checked(); + let db_lock = DB.borrow_data().unwrap(); let online_state = match db_lock.applications.installed_game_version.get(game_id) { Some(meta) => db_lock.applications.transient_statuses.get(meta).cloned(), None => None, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1e374eb..1a3903e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(try_trait_v2)] + mod database; mod games; @@ -14,12 +16,10 @@ use autostart::{get_autostart_enabled, toggle_autostart}; use cleanup::{cleanup_and_exit, quit}; use commands::fetch_state; use database::commands::{ - add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_settings, - fetch_system_data, update_settings, -}; -use database::db::{ - borrow_db_checked, borrow_db_mut_checked, DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR, + add_download_dir, delete_download_dir, fetch_download_dir_stats, fetch_system_data, + update_settings, }; +use database::db::{DatabaseInterface, GameDownloadStatus, DATA_ROOT_DIR}; use download_manager::commands::{ cancel_game, move_download_in_queue, pause_downloads, resume_downloads, }; @@ -44,11 +44,8 @@ use remote::auth::{self, generate_authorization_header, recieve_handshake}; use remote::commands::{ auth_initiate, gen_drop_url, manual_recieve_handshake, retry_connect, sign_out, use_remote, }; -use remote::requests::make_request; use serde::{Deserialize, Serialize}; -use std::env; use std::path::Path; -use std::str::FromStr; use std::sync::Arc; use std::{ collections::HashMap, @@ -95,21 +92,15 @@ pub struct AppState<'a> { fn setup(handle: AppHandle) -> AppState<'static> { let logfile = FileAppender::builder() - .encoder(Box::new(PatternEncoder::new( - "{d} | {l} | {f}:{L} - {m}{n}", - ))) + .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}"))) .append(false) .build(DATA_ROOT_DIR.lock().unwrap().join("./drop.log")) .unwrap(); let console = ConsoleAppender::builder() - .encoder(Box::new(PatternEncoder::new( - "{d} | {l} | {f}:{L} - {m}{n}", - ))) + .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}"))) .build(); - let log_level = env::var("RUST_LOG").unwrap_or(String::from("Info")); - let config = Config::builder() .appenders(vec![ Appender::builder().build("logfile", Box::new(logfile)), @@ -118,7 +109,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { .build( Root::builder() .appenders(vec!["logfile", "console"]) - .build(LevelFilter::from_str(&log_level).expect("Invalid log level")), + .build(LevelFilter::Info), ) .unwrap(); @@ -128,7 +119,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone())); let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone()))); - debug!("checking if database is set up"); + debug!("Checking if database is set up"); let is_set_up = DB.database_is_set_up(); if !is_set_up { return AppState { @@ -140,12 +131,12 @@ fn setup(handle: AppHandle) -> AppState<'static> { }; } - debug!("database is set up"); + debug!("Database is set up"); // TODO: Account for possible failure - let (app_status, user) = auth::setup(); + let (app_status, user) = auth::setup().unwrap(); - let db_handle = borrow_db_checked(); + let db_handle = DB.borrow_data().unwrap(); let mut missing_games = Vec::new(); let statuses = db_handle.applications.game_statuses.clone(); drop(db_handle); @@ -175,7 +166,7 @@ fn setup(handle: AppHandle) -> AppState<'static> { info!("detected games missing: {:?}", missing_games); - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); for game_id in missing_games { db_handle .applications @@ -186,11 +177,11 @@ fn setup(handle: AppHandle) -> AppState<'static> { drop(db_handle); - debug!("finished setup!"); + info!("finished setup!"); // Sync autostart state if let Err(e) = autostart::sync_autostart_on_startup(&handle) { - warn!("failed to sync autostart state: {}", e); + warn!("Failed to sync autostart state: {}", e); } AppState { @@ -227,7 +218,6 @@ pub fn run() { fetch_system_data, // User utils update_settings, - fetch_settings, // Auth auth_initiate, retry_connect, @@ -266,14 +256,14 @@ pub fn run() { .setup(|app| { let handle = app.handle().clone(); let state = setup(handle); - debug!("initialized drop client"); + info!("initialized drop client"); app.manage(Mutex::new(state)); #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { use tauri_plugin_deep_link::DeepLinkExt; app.deep_link().register_all()?; - debug!("registered all pre-defined deep links"); + info!("registered all pre-defined deep links"); } let handle = app.handle().clone(); @@ -293,7 +283,7 @@ pub fn run() { .unwrap(); app.deep_link().on_open_url(move |event| { - debug!("handling drop:// url"); + info!("handling drop:// url"); let binding = event.urls(); let url = binding.first().unwrap(); if url.host_str().unwrap() == "handshake" { @@ -323,18 +313,18 @@ pub fn run() { app.webview_windows().get("main").unwrap().show().unwrap(); } "quit" => { - cleanup_and_exit(app, &app.state()); + cleanup_and_exit(app); } _ => { - println!("menu event not handled: {:?}", event.id); + println!("Menu event not handled: {:?}", event.id); } }) .build(app) .expect("error while setting up tray menu"); { - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); if let Some(original) = db_handle.prev_database.take() { warn!( "Database corrupted. Original file at {}", @@ -357,16 +347,23 @@ pub fn run() { Ok(()) }) .register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| { + let base_url = DB.fetch_base_url(); + // Drop leading / let object_id = &request.uri().path()[1..]; + let object_url = base_url + .join("/api/v1/client/object/") + .unwrap() + .join(object_id) + .unwrap(); + let header = generate_authorization_header(); let client: reqwest::blocking::Client = reqwest::blocking::Client::new(); - let response = make_request(&client, &["/api/v1/client/object/", object_id], &[], |f| { - f.header("Authorization", header) - }) - .unwrap() - .send(); + let response = client + .get(object_url.to_string()) + .header("Authorization", header) + .send(); if response.is_err() { warn!( "failed to fetch object with error: {}", diff --git a/src-tauri/src/process/commands.rs b/src-tauri/src/process/commands.rs index 8a213a4..0d7492e 100644 --- a/src-tauri/src/process/commands.rs +++ b/src-tauri/src/process/commands.rs @@ -1,12 +1,15 @@ use std::sync::Mutex; -use crate::{error::process_error::ProcessError, AppState}; +use crate::{ + error::{process_error::ProcessError, user_error::UserValue}, + AppState, DB, +}; #[tauri::command] pub fn launch_game( id: String, state: tauri::State<'_, Mutex>, -) -> Result<(), ProcessError> { +) -> UserValue<(), ProcessError> { let state_lock = state.lock().unwrap(); let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); @@ -18,23 +21,24 @@ pub fn launch_game( match process_manager_lock.launch_process(id) { Ok(_) => {} - Err(e) => return Err(e), + Err(e) => return UserValue::Err(e), }; drop(process_manager_lock); drop(state_lock); - Ok(()) + UserValue::Ok(()) } #[tauri::command] pub fn kill_game( game_id: String, state: tauri::State<'_, Mutex>, -) -> Result<(), ProcessError> { +) -> UserValue<(), ProcessError> { let state_lock = state.lock().unwrap(); let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); process_manager_lock .kill_game(game_id) .map_err(ProcessError::IOError) + .into() } diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs index 4d8ae12..f53ea20 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -15,9 +15,7 @@ use tauri::{AppHandle, Manager}; use umu_wrapper_lib::command_builder::UmuCommandBuilder; use crate::{ - database::db::{ - borrow_db_mut_checked, ApplicationTransientStatus, GameDownloadStatus, GameVersion, DATA_ROOT_DIR - }, + database::db::{ApplicationTransientStatus, GameDownloadStatus, DATA_ROOT_DIR}, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, error::process_error::ProcessError, games::{library::push_game_update, state::GameStatusManager}, @@ -66,8 +64,12 @@ impl ProcessManager<'_> { } } - fn process_command(&self, install_dir: &String, command: Vec) -> (PathBuf, Vec) { - let root = &command[0]; + // There's no easy way to distinguish between an executable name with + // spaces and it's arguments. + // I think if we just join the install_dir to whatever the user provides us, we'll be alright + // In future, we should have a separate field for executable name and it's arguments + fn process_command(&self, install_dir: &String, raw_command: String) -> (PathBuf, Vec) { + let root = raw_command; let install_dir = Path::new(install_dir); let absolute_exe = install_dir.join(root); @@ -81,7 +83,7 @@ impl ProcessManager<'_> { (absolute_exe, Vec::new()) } pub fn kill_game(&mut self, game_id: String) -> Result<(), io::Error> { - match self.processes.get(&game_id) { + return match self.processes.get(&game_id) { Some(child) => { child.kill()?; child.wait()?; @@ -91,7 +93,7 @@ impl ProcessManager<'_> { io::ErrorKind::NotFound, "Game ID not running", )), - } + }; } fn on_process_finish(&mut self, game_id: String, result: Result) { @@ -104,7 +106,7 @@ impl ProcessManager<'_> { self.processes.remove(&game_id); - let mut db_handle = borrow_db_mut_checked(); + let mut db_handle = DB.borrow_data_mut().unwrap(); let meta = db_handle .applications .installed_game_version @@ -164,9 +166,9 @@ impl ProcessManager<'_> { { Some(GameDownloadStatus::Installed { version_name, .. }) => version_name, Some(GameDownloadStatus::SetupRequired { .. }) => { - return Err(ProcessError::SetupRequired) + return Err(ProcessError::SetupRequired).into() } - _ => return Err(ProcessError::NotInstalled), + _ => return Err(ProcessError::NotInstalled).into(), }; let meta = DownloadableMetadata { id: game_id.clone(), @@ -174,7 +176,7 @@ impl ProcessManager<'_> { download_type: DownloadType::Game, }; - let mut db_lock = borrow_db_mut_checked(); + let mut db_lock = DB.borrow_data_mut().unwrap(); debug!( "Launching process {:?} with games {:?}", &game_id, db_lock.applications.game_versions @@ -186,18 +188,23 @@ impl ProcessManager<'_> { .get(&game_id) .ok_or(ProcessError::NotInstalled)?; - let (version_name, install_dir) = match game_status { + let status_metadata: Option<(&String, &String)> = match game_status { GameDownloadStatus::Installed { version_name, install_dir, - } => (version_name, install_dir), + } => Some((&version_name, &install_dir)), GameDownloadStatus::SetupRequired { version_name, install_dir, - } => (version_name, install_dir), - _ => return Err(ProcessError::NotDownloaded), + } => Some((version_name, install_dir)), + _ => None, }; + if status_metadata.is_none() { + return Err(ProcessError::NotDownloaded); + } + + let (version_name, install_dir) = status_metadata.unwrap(); let game_version = db_lock .applications @@ -207,28 +214,19 @@ impl ProcessManager<'_> { .get(version_name) .ok_or(ProcessError::InvalidVersion)?; - let mut command: Vec = Vec::new(); - - match game_status { + let raw_command: String = match game_status { GameDownloadStatus::Installed { version_name: _, install_dir: _, - } => { - command.extend([game_version.launch_command.clone()]); - command.extend(game_version.launch_args.clone()); - }, + } => game_version.launch_command.clone(), GameDownloadStatus::SetupRequired { version_name: _, install_dir: _, - } => { - command.extend([game_version.setup_command.clone()]); - command.extend(game_version.setup_args.clone()); - }, + } => game_version.setup_command.clone(), _ => panic!("unreachable code"), }; - info!("Command: {:?}", &command); - let (command, args) = self.process_command(install_dir, command); + let (command, args) = self.process_command(install_dir, raw_command); let target_current_dir = command.parent().unwrap().to_str().unwrap(); @@ -276,8 +274,8 @@ impl ProcessManager<'_> { let launch_process = game_launcher .launch_process( &meta, - command.to_string_lossy().to_string(), - game_version, + command.to_str().unwrap().to_owned(), + args, target_current_dir, log_file, error_file, @@ -318,6 +316,9 @@ impl ProcessManager<'_> { }); self.processes.insert(meta.id, wait_thread_handle); + + info!("finished spawning process"); + Ok(()) } } @@ -332,8 +333,8 @@ pub trait ProcessHandler: Send + 'static { fn launch_process( &self, meta: &DownloadableMetadata, - launch_command: String, - game_version: &GameVersion, + command: String, + args: Vec, current_dir: &str, log_file: File, error_file: File, @@ -345,17 +346,17 @@ impl ProcessHandler for NativeGameLauncher { fn launch_process( &self, _meta: &DownloadableMetadata, - launch_command: String, - game_version: &GameVersion, + command: String, + args: Vec, current_dir: &str, log_file: File, error_file: File, ) -> Result { - Command::new(PathBuf::from(launch_command)) + Command::new(command) .current_dir(current_dir) .stdout(log_file) .stderr(error_file) - .args(game_version.launch_args.clone()) + .args(args) .spawn() } } @@ -366,21 +367,15 @@ impl ProcessHandler for UMULauncher { fn launch_process( &self, _meta: &DownloadableMetadata, - launch_command: String, - game_version: &GameVersion, + command: String, + args: Vec, _current_dir: &str, _log_file: File, _error_file: File, ) -> Result { - println!("Game override: .{:?}.", &game_version.umu_id_override); - let game_id = match &game_version.umu_id_override { - Some(game_override) => game_override.is_empty().then_some(game_version.game_id.clone()).unwrap_or(game_override.clone()) , - None => game_version.game_id.clone() - }; - info!("Game ID: {}", game_id); - UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, launch_command) - .game_id(game_id) - .launch_args(game_version.launch_args.clone()) + UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command) + .game_id(String::from("0")) + .launch_args(args) .build() .spawn() } diff --git a/src-tauri/src/remote/auth.rs b/src-tauri/src/remote/auth.rs index d2594de..afd0c9a 100644 --- a/src-tauri/src/remote/auth.rs +++ b/src-tauri/src/remote/auth.rs @@ -1,22 +1,18 @@ use std::{env, sync::Mutex}; use chrono::Utc; -use log::{debug, error, warn}; +use log::{debug, error, info, warn}; use openssl::{ec::EcKey, hash::MessageDigest, pkey::PKey, sign::Signer}; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter, Manager}; use url::Url; use crate::{ - database::db::{ - borrow_db_checked, borrow_db_mut_checked, save_db, DatabaseAuth, DatabaseImpls, - }, + database::db::{DatabaseAuth, DatabaseImpls}, error::{drop_server_error::DropServerError, remote_access_error::RemoteAccessError}, AppState, AppStatus, User, DB, }; -use super::requests::make_request; - #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct InitiateRequestBody { @@ -55,7 +51,7 @@ pub fn sign_nonce(private_key: String, nonce: String) -> Result { pub fn generate_authorization_header() -> String { let certs = { - let db = borrow_db_checked(); + let db = DB.borrow_data().unwrap(); db.auth.clone().unwrap() }; @@ -69,15 +65,16 @@ pub fn generate_authorization_header() -> String { pub fn fetch_user() -> Result { let base_url = DB.fetch_base_url(); + let endpoint = base_url.join("/api/v1/client/user")?; let header = generate_authorization_header(); let client = reqwest::blocking::Client::new(); - let response = make_request(&client, &["/api/v1/client/user"], &[], |f| { - f.header("Authorization", header) - })? - .send()?; + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; if response.status() != 200 { - let err: DropServerError = response.json()?; + let err: DropServerError = response.json().unwrap(); warn!("{:?}", err); if err.status_message == "Nonce expired" { @@ -100,7 +97,7 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc } let base_url = { - let handle = borrow_db_checked(); + let handle = DB.borrow_data().unwrap(); Url::parse(handle.base_url.as_str())? }; @@ -114,18 +111,18 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc let endpoint = base_url.join("/api/v1/client/auth/handshake")?; let client = reqwest::blocking::Client::new(); let response = client.post(endpoint).json(&body).send()?; - debug!("handshake responsded with {}", response.status().as_u16()); + debug!("Handshake responsded with {}", response.status().as_u16()); let response_struct: HandshakeResponse = response.json()?; { - let mut handle = borrow_db_mut_checked(); + let mut handle = DB.borrow_data_mut().unwrap(); handle.auth = Some(DatabaseAuth { private: response_struct.private, cert: response_struct.certificate, client_id: response_struct.id, }); drop(handle); - save_db(); + DB.save().unwrap(); } { @@ -154,7 +151,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) { pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { let base_url = { - let db_lock = borrow_db_checked(); + let db_lock = DB.borrow_data().unwrap(); Url::parse(&db_lock.base_url.clone())? }; @@ -169,7 +166,7 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { if response.status() != 200 { let data: DropServerError = response.json()?; - error!("could not start handshake: {}", data.status_message); + error!("Could not start handshake: {}", data.status_message); return Err(RemoteAccessError::HandshakeFailed(data.status_message)); } @@ -183,19 +180,15 @@ pub fn auth_initiate_logic() -> Result<(), RemoteAccessError> { Ok(()) } -pub fn setup() -> (AppStatus, Option) { - let data = borrow_db_checked(); +pub fn setup() -> Result<(AppStatus, Option), RemoteAccessError> { + let data = DB.borrow_data().unwrap(); let auth = data.auth.clone(); drop(data); if auth.is_some() { - let user_result = match fetch_user() { - Ok(data) => data, - Err(RemoteAccessError::FetchError(_)) => return (AppStatus::ServerUnavailable, None), - Err(_) => return (AppStatus::SignedInNeedsReauth, None), - }; - return (AppStatus::SignedIn, Some(user_result)); + let user_result = fetch_user()?; + return Ok((AppStatus::SignedIn, Some(user_result))); } - (AppStatus::SignedOut, None) + Ok((AppStatus::SignedOut, None)) } diff --git a/src-tauri/src/remote/commands.rs b/src-tauri/src/remote/commands.rs index 82781c8..9dbfcb4 100644 --- a/src-tauri/src/remote/commands.rs +++ b/src-tauri/src/remote/commands.rs @@ -4,9 +4,8 @@ use tauri::{AppHandle, Emitter, Manager}; use url::Url; use crate::{ - database::db::{borrow_db_checked, borrow_db_mut_checked, save_db}, - error::remote_access_error::RemoteAccessError, - AppState, AppStatus, + error::{remote_access_error::RemoteAccessError, user_error::UserValue}, + AppState, AppStatus, DB, }; use super::{ @@ -18,31 +17,31 @@ use super::{ pub fn use_remote( url: String, state: tauri::State<'_, Mutex>>, -) -> Result<(), RemoteAccessError> { - use_remote_logic(url, state) +) -> UserValue<(), RemoteAccessError> { + UserValue::Ok(use_remote_logic(url, state)?) } #[tauri::command] -pub fn gen_drop_url(path: String) -> Result { +pub fn gen_drop_url(path: String) -> UserValue { let base_url = { - let handle = borrow_db_checked(); + let handle = DB.borrow_data().unwrap(); Url::parse(&handle.base_url).map_err(RemoteAccessError::ParsingError)? }; let url = base_url.join(&path).unwrap(); - Ok(url.to_string()) + UserValue::Ok(url.to_string()) } #[tauri::command] pub fn sign_out(app: AppHandle) { // Clear auth from database { - let mut handle = borrow_db_mut_checked(); + let mut handle = DB.borrow_data_mut().unwrap(); handle.auth = None; drop(handle); - save_db(); + DB.save().unwrap(); } // Update app state @@ -58,18 +57,19 @@ pub fn sign_out(app: AppHandle) { } #[tauri::command] -pub fn retry_connect(state: tauri::State<'_, Mutex>) { - let (app_status, user) = setup(); +pub fn retry_connect(state: tauri::State<'_, Mutex>) -> UserValue<(), RemoteAccessError> { + let (app_status, user) = setup()?; let mut guard = state.lock().unwrap(); guard.status = app_status; guard.user = user; drop(guard); + UserValue::Ok(()) } #[tauri::command] -pub fn auth_initiate() -> Result<(), RemoteAccessError> { - auth_initiate_logic() +pub fn auth_initiate() -> UserValue<(), RemoteAccessError> { + auth_initiate_logic().into() } #[tauri::command] diff --git a/src-tauri/src/remote/mod.rs b/src-tauri/src/remote/mod.rs index a2eb666..29d8c02 100644 --- a/src-tauri/src/remote/mod.rs +++ b/src-tauri/src/remote/mod.rs @@ -1,4 +1,3 @@ pub mod auth; pub mod commands; pub mod remote; -pub mod requests; diff --git a/src-tauri/src/remote/remote.rs b/src-tauri/src/remote/remote.rs index 01f2c2b..7790982 100644 --- a/src-tauri/src/remote/remote.rs +++ b/src-tauri/src/remote/remote.rs @@ -1,15 +1,16 @@ -use std::sync::Mutex; - -use log::{debug, warn}; -use serde::Deserialize; -use url::Url; - -use crate::{ - database::db::{borrow_db_mut_checked, save_db}, - error::remote_access_error::RemoteAccessError, - AppState, AppStatus, +use std::{ + error::Error, + fmt::{Display, Formatter}, + sync::{Arc, Mutex}, }; +use http::StatusCode; +use log::{info, warn}; +use serde::Deserialize; +use url::{ParseError, Url}; + +use crate::{error::remote_access_error::RemoteAccessError, AppState, AppStatus, DB}; + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct DropHealthcheck { @@ -20,7 +21,7 @@ pub fn use_remote_logic( url: String, state: tauri::State<'_, Mutex>>, ) -> Result<(), RemoteAccessError> { - debug!("connecting to url {}", url); + info!("connecting to url {}", url); let base_url = Url::parse(&url)?; // Test Drop url @@ -38,11 +39,11 @@ pub fn use_remote_logic( app_state.status = AppStatus::SignedOut; drop(app_state); - let mut db_state = borrow_db_mut_checked(); + let mut db_state = DB.borrow_data_mut().unwrap(); db_state.base_url = base_url.to_string(); drop(db_state); - save_db(); + DB.save().unwrap(); Ok(()) } diff --git a/src-tauri/src/remote/requests.rs b/src-tauri/src/remote/requests.rs deleted file mode 100644 index 44cdc83..0000000 --- a/src-tauri/src/remote/requests.rs +++ /dev/null @@ -1,23 +0,0 @@ -use reqwest::blocking::{Client, RequestBuilder}; - -use crate::{database::db::DatabaseImpls, error::remote_access_error::RemoteAccessError, DB}; - -pub fn make_request, F: FnOnce(RequestBuilder) -> RequestBuilder>( - client: &Client, - path_components: &[T], - query: &[(T, T)], - f: F, -) -> Result { - let mut base_url = DB.fetch_base_url(); - for endpoint in path_components { - base_url = base_url.join(endpoint.as_ref())?; - } - { - let mut queries = base_url.query_pairs_mut(); - for (param, val) in query { - queries.append_pair(param.as_ref(), val.as_ref()); - } - } - let response = client.get(base_url); - Ok(f(response)) -} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 1dead92..0efb8e0 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.2.0-beta", + "version": "0.1.0", "identifier": "dev.drop.app", "build": { "beforeDevCommand": "yarn dev --port 1432", @@ -23,7 +23,7 @@ }, "bundle": { "active": true, - "targets": ["nsis", "deb", "rpm", "dmg", "appimage"], + "targets": ["nsis", "deb", "rpm", "dmg"], "windows": { "nsis": { "installMode": "both" diff --git a/types.ts b/types.ts index 60a5b23..720ec28 100644 --- a/types.ts +++ b/types.ts @@ -71,9 +71,4 @@ export type DownloadableMetadata = { id: string, version: string, downloadType: DownloadableType -} - -export type Settings = { - autostart: boolean, - maxDownloadThreads: number, } \ No newline at end of file