From 8f6f1847394f1056611166c5f828ef827d0df9ef Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 9 Oct 2024 00:37:27 +1100 Subject: [PATCH 001/164] compliant with new APIs --- .env | 1 - app.vue | 3 + components/Header.vue | 6 +- components/InitiateAuthModule.vue | 93 ++++++++++++++++++++++++++ components/WindowControl.vue | 27 +------- nvidia-prop-dev.sh | 1 + pages/auth/index.vue | 104 +++--------------------------- pages/auth/signedout.vue | 18 ++++++ src-tauri/Cargo.lock | 20 +++++- src-tauri/Cargo.toml | 13 +++- src-tauri/src/auth.rs | 76 +++++++++++++++++++--- src-tauri/src/data.rs | 7 +- src-tauri/src/lib.rs | 4 +- 13 files changed, 234 insertions(+), 139 deletions(-) delete mode 100644 .env create mode 100644 components/InitiateAuthModule.vue create mode 100755 nvidia-prop-dev.sh create mode 100644 pages/auth/signedout.vue diff --git a/.env b/.env deleted file mode 100644 index d21acab..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -WEBKIT_DISABLE_DMABUF_RENDERER=1 \ No newline at end of file diff --git a/app.vue b/app.vue index 4b92728..c5500da 100644 --- a/app.vue +++ b/app.vue @@ -23,6 +23,9 @@ switch (state.status) { case AppStatus.SignedOut: router.push("/auth"); break; + case AppStatus.SignedInNeedsReauth: + router.push("/auth/signedout"); + break; } listen("auth/processing", () => { diff --git a/components/Header.vue b/components/Header.vue index 2b16acf..dfe8d0b 100644 --- a/components/Header.vue +++ b/components/Header.vue @@ -1,9 +1,9 @@ diff --git a/components/InitiateAuthModule.vue b/components/InitiateAuthModule.vue new file mode 100644 index 0000000..00627e4 --- /dev/null +++ b/components/InitiateAuthModule.vue @@ -0,0 +1,93 @@ + + + diff --git a/components/WindowControl.vue b/components/WindowControl.vue index 3610162..f777504 100644 --- a/components/WindowControl.vue +++ b/components/WindowControl.vue @@ -1,28 +1,7 @@ diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 2083da5..474c913 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -931,7 +931,9 @@ version = "0.1.0" dependencies = [ "ciborium", "directories", + "hex", "log", + "openssl", "os_info", "rayon", "reqwest", @@ -945,6 +947,7 @@ dependencies = [ "tauri-plugin-shell", "tauri-plugin-single-instance", "url", + "uuid", "webbrowser", "zstd", ] @@ -3203,9 +3206,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "once_cell", "rustls-pki-types", @@ -4613,7 +4616,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.15", + "rand 0.8.5", "serde", + "uuid-macro-internal", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1cd046f83ea2c4e920d6ee9f7c3537ef928d75dce5d84a87c2c5d6b3999a3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a26e11a..1e2d353 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,6 +35,18 @@ os_info = "3.8.2" tauri-plugin-deep-link = "2" log = "0.4.22" structured-logger = "1.0.3" +hex = "0.4.3" + +[dependencies.uuid] +version = "1.10.0" +features = [ + "v4", # Lets you generate random UUIDs + "fast-rng", # Use a faster (but still sufficiently random) RNG + "macro-diagnostics", # Enable better diagnostics for compile-time UUIDs +] + +[dependencies.openssl] +version = "0.10.66" [dependencies.rustbreak] version = "2" @@ -43,4 +55,3 @@ features = ["bin_enc"] # You can also use "yaml_enc" or "bin_enc" [dependencies.reqwest] version = "0.12" features = ["json", "blocking"] - diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs index 1c7403b..d0ff6f8 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/auth.rs @@ -1,14 +1,22 @@ use std::{ borrow::{Borrow, BorrowMut}, + fmt::format, sync::Mutex, }; use log::info; +use openssl::{ + ec::EcKey, + hash::MessageDigest, + pkey::PKey, + sign::{self, Signer}, +}; use serde::{Deserialize, Serialize}; use tauri::{App, AppHandle, Emitter, Error, EventLoopMessage, Manager, Wry}; use url::Url; +use uuid::Uuid; -use crate::{data::DatabaseCerts, AppState, AppStatus, User, DB}; +use crate::{data::DatabaseAuth, AppState, AppStatus, User, DB}; #[derive(Serialize)] struct InitiateRequestBody { @@ -41,6 +49,52 @@ macro_rules! unwrap_or_return { }; } +pub fn sign_nonce(private_key: String, nonce: String) -> Result { + let client_private_key = EcKey::private_key_from_pem(private_key.as_bytes()).unwrap(); + let pkey_private_key = PKey::from_ec_key(client_private_key).unwrap(); + + let mut signer = Signer::new(MessageDigest::sha256(), &pkey_private_key).unwrap(); + signer.update(nonce.as_bytes()).unwrap(); + let signature = signer.sign_to_vec().unwrap(); + + let hex_signature = hex::encode(signature); + + return Ok(hex_signature); +} + +pub fn generate_authorization_header() -> String { + let certs = { + let db = DB.borrow_data().unwrap(); + db.auth.clone().unwrap() + }; + + let nonce = Uuid::new_v4().to_string(); + let signature = sign_nonce(certs.private, nonce.clone()).unwrap(); + + return format!("Nonce {} {} {}", certs.clientId, nonce, signature); +} + +pub fn fetch_user() -> Result { + let base_url = { + let handle = DB.borrow_data().unwrap(); + Url::parse(&handle.base_url).unwrap() + }; + + let endpoint = base_url.join("/api/v1/client/user").unwrap(); + let header = generate_authorization_header(); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send() + .unwrap(); + + let user = response.json::().unwrap(); + + return Ok(user); +} + pub fn recieve_handshake(app: AppHandle, path: String) { // Tell the app we're processing app.emit("auth/processing", ()).unwrap(); @@ -63,7 +117,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) { token: token.to_string(), }; - let endpoint = unwrap_or_return!(base_url.join("/api/v1/client/handshake"), app); + let endpoint = unwrap_or_return!(base_url.join("/api/v1/client/auth/handshake"), app); let client = reqwest::blocking::Client::new(); let response = unwrap_or_return!(client.post(endpoint).json(&body).send(), app); info!("server responded with {}", response.status()); @@ -71,9 +125,10 @@ pub fn recieve_handshake(app: AppHandle, path: String) { { let mut handle = DB.borrow_data_mut().unwrap(); - handle.certs = Some(DatabaseCerts { + handle.auth = Some(DatabaseAuth { private: response_struct.private, cert: response_struct.certificate, + clientId: response_struct.id, }); drop(handle); DB.save().unwrap(); @@ -86,6 +141,8 @@ pub fn recieve_handshake(app: AppHandle, path: String) { } app.emit("auth/finished", ()).unwrap(); + + fetch_user().unwrap(); } #[tauri::command] @@ -97,7 +154,7 @@ pub async fn auth_initiate<'a>() -> Result<(), String> { let current_os_info = os_info::get(); - let endpoint = base_url.join("/api/v1/client/initiate").unwrap(); + let endpoint = base_url.join("/api/v1/client/auth/initiate").unwrap(); let body = InitiateRequestBody { name: format!("Drop Desktop Client"), platform: current_os_info.os_type().to_string(), @@ -124,10 +181,13 @@ pub fn setup() -> Result<(AppStatus, Option), Error> { let data = DB.borrow_data().unwrap(); // If we have certs, exit for now - if data.certs.is_some() { - // TODO: check if it's still valid, and fetch user information - info!("have existing certs, assuming logged in..."); - return Ok((AppStatus::SignedInNeedsReauth, None)); + if data.auth.is_some() { + let user_result = fetch_user(); + if user_result.is_err() { + return Ok((AppStatus::SignedInNeedsReauth, None)); + + } + return Ok((AppStatus::SignedIn, Some(user_result.unwrap()))) } drop(data); diff --git a/src-tauri/src/data.rs b/src-tauri/src/data.rs index 4e52be4..2dd9ebd 100644 --- a/src-tauri/src/data.rs +++ b/src-tauri/src/data.rs @@ -7,14 +7,15 @@ use serde::Deserialize; use crate::DB; #[derive(serde::Serialize, Clone, Deserialize)] -pub struct DatabaseCerts { +pub struct DatabaseAuth { pub private: String, pub cert: String, + pub clientId: String, } #[derive(serde::Serialize, Clone, Deserialize)] pub struct Database { - pub certs: Option, + pub auth: Option, pub base_url: String, } @@ -24,7 +25,7 @@ pub type DatabaseInterface = pub fn setup() -> DatabaseInterface { let db_path = BaseDirs::new().unwrap().data_dir().join("drop"); let default = Database { - certs: None, + auth: None, base_url: "".to_string(), }; let db = match fs::exists(db_path.clone()).unwrap() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4f7d644..fadfc0c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,7 +13,7 @@ use auth::{auth_initiate, recieve_handshake}; use data::DatabaseInterface; use log::info; use remote::use_remote; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use structured_logger::{json::new_writer, Builder}; use tauri_plugin_deep_link::DeepLinkExt; @@ -24,7 +24,7 @@ pub enum AppStatus { SignedIn, SignedInNeedsReauth, } -#[derive(Clone, Copy, Serialize)] +#[derive(Clone, Copy, Serialize, Deserialize)] pub struct User {} #[derive(Clone, Copy, Serialize)] From ac1c3b609a27ff7be14fd657f2331e8960eb306c Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 9 Oct 2024 03:39:05 +1100 Subject: [PATCH 002/164] ci/cd and patches for windows builds --- .gitlab-ci-local/.gitignore | 2 ++ .gitlab-ci.yml | 28 ++++++++++++++++++++++++++ src-tauri/Cargo.lock | 40 ------------------------------------- src-tauri/Cargo.toml | 1 - src-tauri/src/unpacker.rs | 5 +++-- src-tauri/tauri.conf.json | 15 ++++++++++++-- 6 files changed, 46 insertions(+), 45 deletions(-) create mode 100644 .gitlab-ci-local/.gitignore create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci-local/.gitignore b/.gitlab-ci-local/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/.gitlab-ci-local/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..e53ed5f --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,28 @@ +stages: + - build + +build-linux: + stage: build + 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 + - yarnpkg + - yarnpkg tauri build + - cp src-tauri/target/release/bundle/deb/*.deb . + - cp src-tauri/target/release/bundle/rpm/*.rpm . + artifacts: + paths: + - "*.{deb,rpm}" + +build-windows: + stage: build + tags: + - windows + script: + - yarn + - yarn tauri build + - cp src-tauri/target/release/bundle/nsis/*.exe . + artifacts: + paths: + - "*.exe" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 474c913..1192b43 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -455,8 +455,6 @@ version = "1.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8d9e0b4957f635b8d3da819d0db5603620467ecf1f692d22a8c2717ce27e6d8" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -949,7 +947,6 @@ dependencies = [ "url", "uuid", "webbrowser", - "zstd", ] [[package]] @@ -1962,15 +1959,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" version = "0.3.70" @@ -5478,34 +5466,6 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -[[package]] -name = "zstd" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.13+zstd.1.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "zvariant" version = "4.0.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1e2d353..c7591c3 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,7 +26,6 @@ tauri-plugin-shell = "2.0.0" serde = { version = "1", features = ["derive"] } serde_json = "1" ciborium = "0.2.2" -zstd = "0.13.2" rayon = "1.10.0" directories = "5.0.1" webbrowser = "1.0.2" diff --git a/src-tauri/src/unpacker.rs b/src-tauri/src/unpacker.rs index a85bb89..58676e1 100644 --- a/src-tauri/src/unpacker.rs +++ b/src-tauri/src/unpacker.rs @@ -5,10 +5,11 @@ use std::{ collections::HashMap, fs::{create_dir_all, File}, io::{self, BufReader, Error, Seek, Write}, - os::unix::fs::PermissionsExt, path::Path, }; -use tauri::Runtime; + +#[cfg(unix)] +use std::os::unix::fs::PermissionsExt; #[derive(Deserialize)] struct ManifestChunk { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f6cd689..c7c4954 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2.0.0", "productName": "drop", "version": "0.1.0", - "identifier": "dev.drop.drop", + "identifier": "dev.drop.app", "build": { "beforeDevCommand": "yarn dev --port 1432", "devUrl": "http://localhost:1432", @@ -12,7 +12,7 @@ "app": { "windows": [ { - "title": "drop-app", + "title": "Drop", "width": 1536, "height": 864, "minWidth": 820, @@ -33,6 +33,17 @@ }, "bundle": { "active": true, + "targets": ["nsis", "deb", "rpm", "dmg"], + "windows": { + "nsis": { + "installMode": "both" + }, + "webviewInstallMode": { + "silent": true, + "type": "embedBootstrapper" + }, + "wix": null + }, "icon": [ "icons/32x32.png", "icons/128x128.png", From 0c0cfebc1e9f97355583cf1ef21716fc9fd81aeb Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 9 Oct 2024 16:52:24 +1100 Subject: [PATCH 003/164] client now fetches user information from Drop server --- app.vue | 20 ++++++++------ components/HeaderUserWidget.vue | 49 +++++++++++++++++++++++---------- composables/app-state.ts | 3 ++ layouts/default.vue | 2 +- layouts/mini.vue | 2 +- pages/index.vue | 6 +++- src-tauri/src/auth.rs | 1 - src-tauri/src/lib.rs | 18 ++++++++---- src-tauri/src/remote.rs | 25 +++++++++++++++++ src-tauri/tauri.conf.json | 2 +- types.d.ts | 24 ++++++++++++---- 11 files changed, 113 insertions(+), 39 deletions(-) create mode 100644 composables/app-state.ts diff --git a/app.vue b/app.vue index c5500da..57aa3c1 100644 --- a/app.vue +++ b/app.vue @@ -1,22 +1,26 @@ diff --git a/components/HeaderUserWidget.vue b/components/HeaderUserWidget.vue index 6b56303..cefe4bb 100644 --- a/components/HeaderUserWidget.vue +++ b/components/HeaderUserWidget.vue @@ -1,10 +1,12 @@ diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index af76da8..a76c5d9 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -6,11 +6,12 @@ use std::{ }; use directories::BaseDirs; +use log::info; use rustbreak::{deser::Bincode, PathDatabase}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::DB; +use crate::{AppState, DB}; #[derive(serde::Serialize, Clone, Deserialize)] #[serde(rename_all = "camelCase")] @@ -91,7 +92,9 @@ impl DatabaseImpls for DatabaseInterface { } } -fn change_root_directory>(new_dir: T) { +#[tauri::command] +pub fn change_root_directory(new_dir: String) { + info!("Changed root directory to {}", new_dir); let mut lock = DATA_ROOT_DIR.lock().unwrap(); *lock = new_dir.into(); } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 24a8837..0b868b7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,7 +10,7 @@ mod tests; use crate::db::DatabaseImpls; use crate::downloads::download_agent::GameDownloadAgent; use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; -use db::{DatabaseInterface, DATA_ROOT_DIR}; +use db::{change_root_directory, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; @@ -119,6 +119,7 @@ pub fn run() { // Library fetch_library, fetch_game, + change_root_directory, // Downloads queue_game_download, start_game_downloads, From 4983b25702d57a706217dc93650f25e4b0b85d20 Mon Sep 17 00:00:00 2001 From: quexeky Date: Sun, 10 Nov 2024 13:21:37 +1100 Subject: [PATCH 085/164] refactor: Ran cargo clippy & cargo fmt Signed-off-by: quexeky --- src-tauri/src/db.rs | 6 ++---- src-tauri/src/downloads/download_agent.rs | 7 ++++++- src-tauri/src/downloads/download_commands.rs | 2 +- src-tauri/src/downloads/download_logic.rs | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index a76c5d9..afa6a74 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, fs::{self, create_dir_all}, path::PathBuf, - sync::{LazyLock, Mutex, RwLock}, + sync::{LazyLock, Mutex}, }; use directories::BaseDirs; @@ -11,8 +11,6 @@ use rustbreak::{deser::Bincode, PathDatabase}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::{AppState, DB}; - #[derive(serde::Serialize, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DatabaseAuth { @@ -97,4 +95,4 @@ pub fn change_root_directory(new_dir: String) { info!("Changed root directory to {}", new_dir); let mut lock = DATA_ROOT_DIR.lock().unwrap(); *lock = new_dir.into(); -} \ No newline at end of file +} diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index e861dcd..e61a37a 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -151,7 +151,12 @@ impl GameDownloadAgent { game_id: String, ) -> Result<(), GameDownloadError> { let mut contexts = Vec::new(); - let base_path = DATA_ROOT_DIR.lock().unwrap().join("games").join(game_id.clone()).clone(); + let base_path = DATA_ROOT_DIR + .lock() + .unwrap() + .join("games") + .join(game_id.clone()) + .clone(); create_dir_all(base_path.clone()).unwrap(); info!("Generating contexts"); for (raw_path, chunk) in manifest { diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 9109842..7d63e9a 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -120,7 +120,7 @@ pub async fn get_game_download_progress( pub async fn set_download_state( state: tauri::State<'_, Mutex>, game_id: String, - status: GameDownloadState + status: GameDownloadState, ) -> Result<(), String> { info!("Setting game state"); get_game_download(state, game_id).change_state(status); diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 1f6ac90..4dbf632 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -1,7 +1,7 @@ -use crate::{auth::generate_authorization_header, GAME_PAUSE_CHECK_INTERVAL}; use crate::db::DatabaseImpls; use crate::downloads::manifest::DropDownloadContext; use crate::DB; +use crate::{auth::generate_authorization_header, GAME_PAUSE_CHECK_INTERVAL}; use atomic_counter::{AtomicCounter, RelaxedCounter}; use log::{error, info}; use md5::{Context, Digest}; From 6a38ea306bca409990276fdf58adfedc003b012e Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sun, 10 Nov 2024 22:25:54 +1100 Subject: [PATCH 086/164] feat(downloads): reduce scope of download agent due to a miscommunication, the scope of the download agent has grown too much. this commit reduces that scopes, and intends for a lot of the heavy lifting to be done by the soon-to-be-implemented download manager. --- pages/settings.vue | 2 +- pages/store/index.vue | 136 ++--------- src-tauri/src/auth.rs | 1 - src-tauri/src/db.rs | 42 +++- src-tauri/src/downloads/download_agent.rs | 219 ++++++++++------- src-tauri/src/downloads/download_commands.rs | 141 +++-------- src-tauri/src/downloads/download_logic.rs | 241 ++++++++++--------- src-tauri/src/downloads/mod.rs | 3 +- src-tauri/src/downloads/progress.rs | 69 ------ src-tauri/src/lib.rs | 9 +- 10 files changed, 352 insertions(+), 511 deletions(-) delete mode 100644 src-tauri/src/downloads/progress.rs diff --git a/pages/settings.vue b/pages/settings.vue index b897920..35b9783 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -47,7 +47,7 @@ import { RectangleGroupIcon, } from "@heroicons/vue/16/solid"; import type { Component } from "vue"; -import type { NavigationItem } from "~/components/types"; +import type { NavigationItem } from "~/types"; const navigation: Array = [ { diff --git a/pages/store/index.vue b/pages/store/index.vue index 7b7665f..ee5f7b2 100644 --- a/pages/store/index.vue +++ b/pages/store/index.vue @@ -1,139 +1,39 @@ diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs index 0c57c49..7619e8a 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/auth.rs @@ -113,7 +113,6 @@ 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()?; - info!("server responded with {}", response.status()); let response_struct = response.json::()?; { diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index afa6a74..0858e1b 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -1,7 +1,9 @@ use std::{ + borrow::BorrowMut, collections::HashMap, + fmt::format, fs::{self, create_dir_all}, - path::PathBuf, + path::{Path, PathBuf}, sync::{LazyLock, Mutex}, }; @@ -9,8 +11,11 @@ use directories::BaseDirs; use log::info; use rustbreak::{deser::Bincode, PathDatabase}; use serde::{Deserialize, Serialize}; +use tokio::fs::metadata; use url::Url; +use crate::DB; + #[derive(serde::Serialize, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DatabaseAuth { @@ -32,7 +37,7 @@ pub enum DatabaseGameStatus { #[derive(Serialize, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DatabaseGames { - pub games_base_dir: String, + pub install_dirs: Vec, pub games_statuses: HashMap, } @@ -67,7 +72,7 @@ impl DatabaseImpls for DatabaseInterface { auth: None, base_url: "".to_string(), games: DatabaseGames { - games_base_dir: games_base_dir.to_str().unwrap().to_string(), + install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], games_statuses: HashMap::new(), }, }; @@ -91,8 +96,31 @@ impl DatabaseImpls for DatabaseInterface { } #[tauri::command] -pub fn change_root_directory(new_dir: String) { - info!("Changed root directory to {}", new_dir); - let mut lock = DATA_ROOT_DIR.lock().unwrap(); - *lock = new_dir.into(); +pub fn add_new_download_dir(new_dir: String) -> Result<(), String> { + // Check the new directory is all good + let new_dir_path = Path::new(&new_dir); + if new_dir_path.exists() { + let metadata = new_dir_path + .metadata() + .map_err(|e| format!("Unable to access file or directory: {}", e.to_string()))?; + if metadata.is_dir() { + return Err("Invalid path: not a directory".to_string()); + } + let dir_contents = new_dir_path + .read_dir() + .map_err(|e| format!("Unable to check directory contents: {}", e.to_string()))?; + if dir_contents.count() == 0 { + return Err("Path is not empty".to_string()); + } + } else { + create_dir_all(new_dir_path) + .map_err(|e| format!("Unable to create directories to path: {}", e.to_string()))?; + } + + // Add it to the dictionary + let mut lock = DB.borrow_data_mut().unwrap(); + lock.games.install_dirs.push(new_dir); + drop(lock); + + Ok(()) } diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index e61a37a..80ede6b 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -1,96 +1,121 @@ use crate::auth::generate_authorization_header; -use crate::db::{DatabaseImpls, DATA_ROOT_DIR}; -use crate::downloads::download_logic; +use crate::db::DatabaseImpls; use crate::downloads::manifest::{DropDownloadContext, DropManifest}; -use crate::downloads::progress::ProgressChecker; +use crate::remote::RemoteAccessError; use crate::DB; -use atomic_counter::RelaxedCounter; use log::info; -use rustix::fs::{fallocate, FallocateFlags}; +use rayon::ThreadPoolBuilder; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::path::Path; +use std::sync::atomic::AtomicU64; use std::sync::{Arc, Mutex, RwLock}; use urlencoding::encode; +#[cfg(target_os = "linux")] +use rustix::fs::{fallocate, FallocateFlags}; + +use super::download_logic::download_game_chunk; + pub struct GameDownloadAgent { pub id: String, pub version: String, - pub status: Arc>, + pub control_flag: Arc>, + pub target_download_dir: usize, contexts: Mutex>, - pub progress: ProgressChecker, + // pub progress: ProgressChecker, pub manifest: Mutex>, + pub progress: ProgressObject, } #[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] -pub enum GameDownloadState { - Uninitialised, - Queued, - Paused, - Manifest, - Downloading, - Finished, - Stalled, - Failed, - Cancelled, +pub enum DownloadThreadControlFlag { + Go, + Stop, } -#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] +#[derive(Debug)] pub enum GameDownloadError { - ManifestDownload, - FailedContextGeneration, - Status(u16), - System(SystemError), + CommunicationError(RemoteAccessError), + ChecksumError, + SetupError(String), + LockError, } -#[derive(Serialize, Deserialize, Clone, Eq, PartialEq, Debug)] -pub enum SystemError { - MutexLockFailed, + +impl Display for GameDownloadError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + GameDownloadError::CommunicationError(error) => write!(f, "{}", error), + GameDownloadError::SetupError(error) => write!(f, "{}", error), + GameDownloadError::LockError => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"), + GameDownloadError::ChecksumError => write!(f, "Checksum failed to validate for download"), + } + } +} + +static DOWNLOAD_MAX_THREADS: usize = 4; + +pub struct ProgressObject { + pub max: u64, + pub current: Arc, } impl GameDownloadAgent { - pub fn new(id: String, version: String) -> Self { - let status = Arc::new(RwLock::new(GameDownloadState::Uninitialised)); + pub fn new(id: String, version: String, target_download_dir: usize) -> Self { + // Don't run by default + let status = Arc::new(RwLock::new(DownloadThreadControlFlag::Stop)); Self { id, version, - status: status.clone(), + control_flag: status.clone(), manifest: Mutex::new(None), - progress: ProgressChecker::new( - Box::new(download_logic::download_game_chunk), - Arc::new(RelaxedCounter::new(0)), - status, - 0, - ), + target_download_dir, contexts: Mutex::new(Vec::new()), + progress: ProgressObject { + max: 0, + current: Arc::new(AtomicU64::new(0)), + }, } } - pub async fn queue(&self) -> Result<(), GameDownloadError> { - self.change_state(GameDownloadState::Queued); - if self.manifest.lock().unwrap().is_none() { - return Ok(()); - } - self.ensure_manifest_exists() + pub fn set_control_flag(&self, flag: DownloadThreadControlFlag) { + let mut lock = self.control_flag.write().unwrap(); + *lock = flag; + } + pub fn get_control_flag(&self) -> DownloadThreadControlFlag { + let lock = self.control_flag.read().unwrap(); + lock.clone() } - pub fn begin_download(&self, max_threads: usize) -> Result<(), GameDownloadError> { - self.change_state(GameDownloadState::Downloading); - // TODO we're coping the whole context thing - // It's not necessary, I just can't figure out to make the borrow checker happy - { - let lock = self.contexts.lock().unwrap().to_vec(); - self.progress.run_context_parallel(lock, max_threads); - } + // Blocking + // Requires mutable self + pub fn setup_download(&mut self) -> Result<(), GameDownloadError> { + self.ensure_manifest_exists()?; + + self.generate_contexts()?; + + self.set_control_flag(DownloadThreadControlFlag::Go); + Ok(()) } - pub fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> { + // Blocking + pub fn download(&mut self) -> Result<(), GameDownloadError> { + self.setup_download()?; + self.run(); + + Ok(()) + } + + pub fn ensure_manifest_exists(&mut self) -> Result<(), GameDownloadError> { if self.manifest.lock().unwrap().is_some() { return Ok(()); } - self.download_manifest() + // Explicitly propagate error + Ok(self.download_manifest()?) } - fn download_manifest(&self) -> Result<(), GameDownloadError> { + fn download_manifest(&mut self) -> Result<(), GameDownloadError> { let base_url = DB.fetch_base_url(); let manifest_url = base_url .join( @@ -104,8 +129,6 @@ impl GameDownloadAgent { .unwrap(); let header = generate_authorization_header(); - - info!("Generating & sending client"); let client = reqwest::blocking::Client::new(); let response = client .get(manifest_url.to_string()) @@ -114,8 +137,14 @@ impl GameDownloadAgent { .unwrap(); if response.status() != 200 { - info!("Error status: {}", response.status()); - return Err(GameDownloadError::Status(response.status().as_u16())); + return Err(GameDownloadError::CommunicationError( + format!( + "Failed to download game manifest: {} {}", + response.status(), + response.text().unwrap() + ) + .into(), + )); } let manifest_download = response.json::().unwrap(); @@ -125,42 +154,33 @@ impl GameDownloadAgent { return chunk.lengths.iter().sum::(); }) .sum::(); - self.progress.set_capacity(length); + self.progress.max = length.try_into().unwrap(); + if let Ok(mut manifest) = self.manifest.lock() { - *manifest = Some(manifest_download) - } else { - return Err(GameDownloadError::System(SystemError::MutexLockFailed)); + *manifest = Some(manifest_download); + return Ok(()); } - Ok(()) + return Err(GameDownloadError::LockError); } - pub fn change_state(&self, state: GameDownloadState) { - let mut lock = self.status.write().unwrap(); - *lock = state; - } - pub fn get_state(&self) -> GameDownloadState { - let lock = self.status.read().unwrap(); - lock.clone() - } + pub fn generate_contexts(&self) -> Result<(), GameDownloadError> { + let db_lock = DB.borrow_data().unwrap(); + let data_base_dir = db_lock.games.install_dirs[self.target_download_dir].clone(); + drop(db_lock); + + let manifest = self.manifest.lock().unwrap().clone().unwrap(); + let version = self.version.clone(); + let game_id = self.id.clone(); + + let data_base_dir_path = Path::new(&data_base_dir); - pub fn generate_job_contexts( - &self, - manifest: &DropManifest, - version: String, - game_id: String, - ) -> Result<(), GameDownloadError> { let mut contexts = Vec::new(); - let base_path = DATA_ROOT_DIR - .lock() - .unwrap() - .join("games") - .join(game_id.clone()) - .clone(); + let base_path = data_base_dir_path.join(game_id.clone()).clone(); create_dir_all(base_path.clone()).unwrap(); - info!("Generating contexts"); + for (raw_path, chunk) in manifest { - let path = base_path.join(Path::new(raw_path)); + let path = base_path.join(Path::new(&raw_path)); let container = path.parent().unwrap(); create_dir_all(container).unwrap(); @@ -181,17 +201,44 @@ impl GameDownloadAgent { running_offset += *length as u64; } + #[cfg(target_os = "linux")] if running_offset > 0 { fallocate(file, FallocateFlags::empty(), 0, running_offset).unwrap(); } } - info!("Finished generating"); + if let Ok(mut context_lock) = self.contexts.lock() { *context_lock = contexts; - } else { - return Err(GameDownloadError::FailedContextGeneration); + return Ok(()); } - Ok(()) + return Err(GameDownloadError::SetupError( + "Failed to generate download contexts".to_owned(), + )); + } + + pub fn run(&self) { + let pool = ThreadPoolBuilder::new() + .num_threads(DOWNLOAD_MAX_THREADS) + .build() + .unwrap(); + + pool.scope(move |scope| { + let contexts = self.contexts.lock().unwrap(); + + for context in contexts.iter() { + let context = context.clone(); + let control_flag = self.control_flag.clone(); // Clone arcs + let progress = self.progress.current.clone(); // Clone arcs + info!( + "starting download for file {} {}", + context.file_name, context.index + ); + + scope.spawn(move |_| { + download_game_chunk(context, control_flag, progress).unwrap(); + }); + } + }) } } diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 7d63e9a..86f1c71 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -1,137 +1,62 @@ use std::{ + borrow::Borrow, sync::{Arc, Mutex}, - thread, }; use log::info; +use rayon::spawn; use crate::{downloads::download_agent::GameDownloadAgent, AppState}; -use super::download_agent::{GameDownloadError, GameDownloadState}; - #[tauri::command] -pub async fn queue_game_download( +pub fn download_game( game_id: String, game_version: String, state: tauri::State<'_, Mutex>, -) -> Result<(), GameDownloadError> { - info!("Queuing Game Download"); - let download_agent = Arc::new(GameDownloadAgent::new( - game_id.clone(), - game_version.clone(), - )); - download_agent.queue().await?; - - let mut queue = state.lock().unwrap(); - queue.game_downloads.insert(game_id, download_agent); - Ok(()) -} - -#[tauri::command] -pub async fn start_game_downloads( - max_threads: usize, - state: tauri::State<'_, Mutex>, -) -> Result<(), GameDownloadError> { - info!("Downloading Games"); - let lock = state.lock().unwrap(); - let mut game_downloads = lock.game_downloads.clone(); - drop(lock); - thread::spawn(move || loop { - let mut current_id = String::new(); - let mut download_agent = None; - { - for (id, agent) in &game_downloads { - if agent.get_state() == GameDownloadState::Queued { - download_agent = Some(agent.clone()); - current_id = id.clone(); - info!("Got queued game to download"); - break; - } - } - if download_agent.is_none() { - info!("No more games left to download"); - return; - } - }; - info!("Downloading game"); - { - start_game_download(max_threads, download_agent.unwrap()).unwrap(); - game_downloads.remove_entry(¤t_id); - } - }); - info!("Spawned download"); - Ok(()) -} - -pub fn start_game_download( - max_threads: usize, - download_agent: Arc, -) -> Result<(), GameDownloadError> { - info!("Triggered Game Download"); - - download_agent.ensure_manifest_exists()?; - - let local_manifest = { - let manifest = download_agent.manifest.lock().unwrap(); - (*manifest).clone().unwrap() - }; - - download_agent - .generate_job_contexts( - &local_manifest, - download_agent.version.clone(), - download_agent.id.clone(), - ) - .unwrap(); - - download_agent.begin_download(max_threads).unwrap(); - - Ok(()) -} - -#[tauri::command] -pub async fn cancel_specific_game_download( - state: tauri::State<'_, Mutex>, - game_id: String, ) -> Result<(), String> { - info!("called stop_specific_game_download"); - get_game_download(state, game_id).change_state(GameDownloadState::Cancelled); + info!("beginning game download..."); - //TODO: Drop the game download instance + let mut download_agent = GameDownloadAgent::new(game_id.clone(), game_version.clone(), 0); + // Setup download requires mutable + download_agent.setup_download().unwrap(); - info!("Stopping callback"); + let mut lock: std::sync::MutexGuard<'_, AppState> = state.lock().unwrap(); + let download_agent_ref = Arc::new(download_agent); + lock.game_downloads + .insert(game_id, download_agent_ref.clone()); + + // Run it in another thread + spawn(move || { + // Run doesn't require mutable + download_agent_ref.clone().run(); + }); Ok(()) } #[tauri::command] -pub async fn get_game_download_progress( +pub fn get_game_download_progress( state: tauri::State<'_, Mutex>, game_id: String, ) -> Result { - let progress = get_game_download(state, game_id) - .progress - .get_progress_percentage(); - info!("{}", progress); - Ok(progress) + let da = use_download_agent(state, game_id)?; + + let progress = &da.progress; + let current: f64 = progress + .current + .fetch_add(0, std::sync::atomic::Ordering::Relaxed) as f64; + let max = progress.max as f64; + + let current_progress = current / max; + + Ok(current_progress) } -#[tauri::command] -pub async fn set_download_state( +fn use_download_agent( state: tauri::State<'_, Mutex>, game_id: String, - status: GameDownloadState, -) -> Result<(), String> { - info!("Setting game state"); - get_game_download(state, game_id).change_state(status); - Ok(()) -} - -fn get_game_download( - state: tauri::State<'_, Mutex>, - game_id: String, -) -> Arc { +) -> Result, String> { let lock = state.lock().unwrap(); - let download_agent = lock.game_downloads.get(&game_id).unwrap(); - download_agent.clone() + let download_agent = lock.game_downloads.get(&game_id).ok_or("Invalid game ID")?; + Ok(download_agent.clone()) // Clones the Arc, not the underlying data structure } diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 4dbf632..69d1f8f 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -1,122 +1,136 @@ +use crate::auth::generate_authorization_header; use crate::db::DatabaseImpls; use crate::downloads::manifest::DropDownloadContext; +use crate::remote::RemoteAccessError; use crate::DB; -use crate::{auth::generate_authorization_header, GAME_PAUSE_CHECK_INTERVAL}; use atomic_counter::{AtomicCounter, RelaxedCounter}; use log::{error, info}; use md5::{Context, Digest}; +use reqwest::blocking::Response; +use serde::de::Error; +use std::io::Read; +use std::sync::atomic::AtomicU64; +use std::sync::RwLock; use std::{ fs::{File, OpenOptions}, - io::{self, BufWriter, Error, ErrorKind, Seek, SeekFrom, Write}, + io::{self, BufWriter, ErrorKind, Seek, SeekFrom, Write}, path::PathBuf, - sync::{Arc, RwLock}, - thread::sleep, + sync::Arc, }; use urlencoding::encode; -use super::download_agent::GameDownloadState; +use super::download_agent::{DownloadThreadControlFlag, GameDownloadError}; -pub struct DropFileWriter { - file: File, +pub struct DropWriter { hasher: Context, - progress: Arc, - status: Arc>, + destination: W, } -impl DropFileWriter { - fn new( - path: PathBuf, - status: Arc>, - progress: Arc, - ) -> Self { +impl DropWriter { + fn new(path: PathBuf) -> Self { Self { - file: OpenOptions::new().write(true).open(path).unwrap(), + destination: OpenOptions::new().write(true).open(path).unwrap(), hasher: Context::new(), - progress, - status, } } + fn finish(mut self) -> io::Result { self.flush().unwrap(); Ok(self.hasher.compute()) } - - fn manage_state(&mut self) -> Option> { - match self.status.read().unwrap().clone() { - GameDownloadState::Uninitialised => todo!(), - GameDownloadState::Queued => { - return Some(Err(Error::new( - ErrorKind::NotConnected, - "Download has not yet been started", - ))) - } - GameDownloadState::Manifest => { - return Some(Err(Error::new( - ErrorKind::NotFound, - "Manifest still not finished downloading", - ))) - } - GameDownloadState::Downloading => {} - GameDownloadState::Finished => { - return Some(Err(Error::new( - ErrorKind::AlreadyExists, - "Download already finished", - ))) - } - GameDownloadState::Stalled => { - return Some(Err(Error::new(ErrorKind::Interrupted, "Download Stalled"))) - } - GameDownloadState::Failed => { - return Some(Err(Error::new(ErrorKind::BrokenPipe, "Download Failed"))) - } - GameDownloadState::Cancelled => { - return Some(Err(Error::new( - ErrorKind::ConnectionAborted, - "Interrupt command recieved", - ))); - } - GameDownloadState::Paused => { - info!("Game download paused"); - sleep(GAME_PAUSE_CHECK_INTERVAL); - } - }; - None - } } -// TODO: Implement error handling -impl Write for DropFileWriter { +// Write automatically pushes to file and hasher +impl Write for DropWriter { fn write(&mut self, buf: &[u8]) -> io::Result { - // TODO: Tidy up these error messages / types because these ones don't really seem to fit - if let Some(value) = self.manage_state() { - return value; - } - let len = buf.len(); - self.progress.add(len); - - //info!("Writing data to writer"); - self.hasher.write_all(buf).unwrap(); - self.file.write(buf) + 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.file.flush() + self.destination.flush() } } -impl Seek for DropFileWriter { +// Seek moves around destination output +impl Seek for DropWriter { fn seek(&mut self, pos: SeekFrom) -> io::Result { - self.file.seek(pos) + self.destination.seek(pos) } } + +pub struct DropDownloadPipeline { + pub source: R, + pub destination: DropWriter, + pub control_flag: Arc>, + pub progress: Arc, + pub size: usize, +} +impl DropDownloadPipeline { + fn new( + source: Response, + destination: DropWriter, + control_flag: Arc>, + progress: Arc, + size: usize, + ) -> Self { + return Self { + source, + destination, + control_flag, + progress, + size, + }; + } + + fn copy(&mut self) -> Result { + let copy_buf_size = 512; + let mut copy_buf = vec![0; copy_buf_size]; + let mut buf_writer = BufWriter::with_capacity(1024 * 1024, &mut self.destination); + + let mut current_size = 0; + loop { + if *self.control_flag.read().unwrap() == DownloadThreadControlFlag::Stop { + return Ok(false); + } + + let bytes_read = self.source.read(&mut copy_buf)?; + current_size += bytes_read; + + buf_writer.write(©_buf[0..bytes_read])?; + self.progress.fetch_add( + bytes_read.try_into().unwrap(), + std::sync::atomic::Ordering::Relaxed, + ); + + if current_size == self.size { + break; + } + } + + Ok(true) + } + + fn finish(self) -> Result { + let checksum = self.destination.finish()?; + return Ok(checksum); + } +} + pub fn download_game_chunk( ctx: DropDownloadContext, - status: Arc>, - progress: Arc, -) { - if *status.read().unwrap() == GameDownloadState::Cancelled { - info!("Callback stopped download at start"); - return; + control_flag: Arc>, + progress: Arc, +) -> Result { + // If we're paused + if *control_flag.read().unwrap() == DownloadThreadControlFlag::Stop { + return Ok(false); } + let base_url = DB.fetch_base_url(); let client = reqwest::blocking::Client::new(); @@ -133,47 +147,48 @@ pub fn download_game_chunk( let header = generate_authorization_header(); - let mut response = match client.get(chunk_url).header("Authorization", header).send() { - Ok(response) => response, - Err(e) => { - info!("{}", e); - return; - } - }; + let response = client + .get(chunk_url) + .header("Authorization", header) + .send() + .map_err(|e| GameDownloadError::CommunicationError(RemoteAccessError::FetchError(e)))?; - let mut file: DropFileWriter = DropFileWriter::new(ctx.path, status, progress); + let mut destination = DropWriter::new(ctx.path); if ctx.offset != 0 { - file.seek(SeekFrom::Start(ctx.offset)) + destination + .seek(SeekFrom::Start(ctx.offset)) .expect("Failed to seek to file offset"); } - // Writing everything to disk directly is probably slightly faster in terms of disk - // speed because it balances out the writes, but this is better than the performance - // loss from constantly reading the callbacks - - let mut writer = BufWriter::with_capacity(1024 * 1024, file); - - match io::copy(&mut response, &mut writer) { - Ok(_) => {} - Err(e) => { - info!("Copy errored with error {}", e) - } + let content_length = response.content_length(); + if content_length.is_none() { + return Err(GameDownloadError::CommunicationError( + RemoteAccessError::GenericErrror( + "Invalid download endpoint, missing Content-Length header.".to_owned(), + ), + )); } - writer.flush().unwrap(); - let file = match writer.into_inner() { - Ok(file) => file, - Err(_) => { - error!("Failed to acquire writer from BufWriter"); - return; - } + + let mut pipeline = DropDownloadPipeline::new( + response, + destination, + control_flag, + progress, + content_length.unwrap().try_into().unwrap(), + ); + + let completed = pipeline.copy().unwrap(); + if !completed { + return Ok(false); }; - let res = hex::encode(file.finish().unwrap().0); + let checksum = pipeline.finish().unwrap(); + + let res = hex::encode(checksum.0); if res != ctx.checksum { - info!( - "Checksum failed. Original: {}, Calculated: {} for {}", - ctx.checksum, res, ctx.file_name - ); + return Err(GameDownloadError::ChecksumError); } + + return Ok(true); } diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index 146ed81..4e59d68 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -1,5 +1,4 @@ pub mod download_agent; pub mod download_commands; mod download_logic; -mod manifest; -pub mod progress; +mod manifest; \ No newline at end of file diff --git a/src-tauri/src/downloads/progress.rs b/src-tauri/src/downloads/progress.rs deleted file mode 100644 index 850dba8..0000000 --- a/src-tauri/src/downloads/progress.rs +++ /dev/null @@ -1,69 +0,0 @@ -use atomic_counter::{AtomicCounter, RelaxedCounter}; -use log::info; -use rayon::ThreadPoolBuilder; -use std::sync::{Arc, Mutex, RwLock}; - -use super::download_agent::GameDownloadState; - -pub struct ProgressChecker -where - T: 'static + Send + Sync, -{ - counter: Arc, - f: Arc< - Box>, Arc) + Send + Sync + 'static>, - >, - status: Arc>, - capacity: Mutex, -} - -impl ProgressChecker -where - T: Send + Sync, -{ - pub fn new( - f: Box< - dyn Fn(T, Arc>, Arc) + Send + Sync + 'static, - >, - counter: Arc, - status: Arc>, - capacity: usize, - ) -> Self { - Self { - f: f.into(), - counter, - status, - capacity: capacity.into(), - } - } - pub fn run_context_parallel(&self, contexts: Vec, max_threads: usize) { - let threads = ThreadPoolBuilder::new() - .num_threads(max_threads) - .build() - .unwrap(); - - threads.scope(|s| { - for context in contexts { - let status = self.status.clone(); - let counter = self.counter.clone(); - let f = self.f.clone(); - s.spawn(move |_| { - info!("Running thread"); - f(context, status, counter) - }); - } - }); - info!("Concluded scope"); - } - pub fn set_capacity(&self, capacity: usize) { - let mut lock = self.capacity.lock().unwrap(); - *lock = capacity; - } - pub fn get_progress(&self) -> usize { - self.counter.get() - } - // I strongly dislike type casting in my own code, so I've shovelled it into here - pub fn get_progress_percentage(&self) -> f64 { - (self.get_progress() as f64) / (*self.capacity.lock().unwrap() as f64) - } -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0b868b7..91061ef 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,7 +10,7 @@ mod tests; use crate::db::DatabaseImpls; use crate::downloads::download_agent::GameDownloadAgent; use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; -use db::{change_root_directory, DatabaseInterface, DATA_ROOT_DIR}; +use db::{add_new_download_dir, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; @@ -119,13 +119,10 @@ pub fn run() { // Library fetch_library, fetch_game, - change_root_directory, + add_new_download_dir, // Downloads - queue_game_download, - start_game_downloads, - cancel_specific_game_download, + download_game, get_game_download_progress, - set_download_state ]) .plugin(tauri_plugin_shell::init()) .setup(|app| { From 04368ff5496c06bbda0f763ee2949c4d14f2f499 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sun, 10 Nov 2024 22:31:11 +1100 Subject: [PATCH 087/164] fix(download dir): fix logic error in detecting dir --- src-tauri/src/db.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 0858e1b..75657e2 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -103,7 +103,7 @@ pub fn add_new_download_dir(new_dir: String) -> Result<(), String> { let metadata = new_dir_path .metadata() .map_err(|e| format!("Unable to access file or directory: {}", e.to_string()))?; - if metadata.is_dir() { + if !metadata.is_dir() { return Err("Invalid path: not a directory".to_string()); } let dir_contents = new_dir_path From 4fc13a1c8f0afe87ed2d57ae5abfb9cfec73f228 Mon Sep 17 00:00:00 2001 From: quexeky Date: Mon, 11 Nov 2024 07:58:49 +1100 Subject: [PATCH 088/164] refactor(downloads): Convert DOWNLOAD_MAX_THREADS to const Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 80ede6b..3297d1b 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -53,7 +53,7 @@ impl Display for GameDownloadError { } } -static DOWNLOAD_MAX_THREADS: usize = 4; +pub const DOWNLOAD_MAX_THREADS: usize = 4; pub struct ProgressObject { pub max: u64, From b47b7ea935b44e39e6beff89563b9c64108bee64 Mon Sep 17 00:00:00 2001 From: quexeky Date: Mon, 11 Nov 2024 09:11:46 +1100 Subject: [PATCH 089/164] refactor: Created file settings.rs Add constant values here to have a central management point for any relevant constants Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 4 +--- src-tauri/src/lib.rs | 3 +-- src-tauri/src/settings.rs | 1 + 3 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 src-tauri/src/settings.rs diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 3297d1b..c03ac3f 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -2,6 +2,7 @@ use crate::auth::generate_authorization_header; use crate::db::DatabaseImpls; use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::remote::RemoteAccessError; +use crate::settings::DOWNLOAD_MAX_THREADS; use crate::DB; use log::info; use rayon::ThreadPoolBuilder; @@ -24,7 +25,6 @@ pub struct GameDownloadAgent { pub control_flag: Arc>, pub target_download_dir: usize, contexts: Mutex>, - // pub progress: ProgressChecker, pub manifest: Mutex>, pub progress: ProgressObject, } @@ -53,8 +53,6 @@ impl Display for GameDownloadError { } } -pub const DOWNLOAD_MAX_THREADS: usize = 4; - pub struct ProgressObject { pub max: u64, pub current: Arc, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 91061ef..3780909 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,6 +4,7 @@ mod downloads; mod library; mod p2p; mod remote; +mod settings; #[cfg(test)] mod tests; @@ -26,8 +27,6 @@ use std::{ }; use tauri_plugin_deep_link::DeepLinkExt; -pub const GAME_PAUSE_CHECK_INTERVAL: Duration = Duration::from_secs(1); - #[derive(Clone, Copy, Serialize)] pub enum AppStatus { NotConfigured, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs new file mode 100644 index 0000000..fadef14 --- /dev/null +++ b/src-tauri/src/settings.rs @@ -0,0 +1 @@ +pub const DOWNLOAD_MAX_THREADS: usize = 4; \ No newline at end of file From f25bfed336dbe1dd58e85a3c6879b6198df6dfcd Mon Sep 17 00:00:00 2001 From: quexeky Date: Mon, 11 Nov 2024 09:39:25 +1100 Subject: [PATCH 090/164] feat(downloads): Convert DownloadThreadControlFlag to AtomicBool Also ran cargo fmt & cargo clipy Signed-off-by: quexeky --- src-tauri/src/db.rs | 10 ++---- src-tauri/src/downloads/download_agent.rs | 36 +++++++++----------- src-tauri/src/downloads/download_commands.rs | 5 +-- src-tauri/src/downloads/download_logic.rs | 15 ++++---- src-tauri/src/downloads/mod.rs | 2 +- src-tauri/src/lib.rs | 1 - src-tauri/src/settings.rs | 2 +- 7 files changed, 29 insertions(+), 42 deletions(-) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 75657e2..f5e5ea4 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -1,17 +1,13 @@ use std::{ - borrow::BorrowMut, collections::HashMap, - fmt::format, fs::{self, create_dir_all}, path::{Path, PathBuf}, sync::{LazyLock, Mutex}, }; use directories::BaseDirs; -use log::info; use rustbreak::{deser::Bincode, PathDatabase}; use serde::{Deserialize, Serialize}; -use tokio::fs::metadata; use url::Url; use crate::DB; @@ -102,19 +98,19 @@ pub fn add_new_download_dir(new_dir: String) -> Result<(), String> { if new_dir_path.exists() { let metadata = new_dir_path .metadata() - .map_err(|e| format!("Unable to access file or directory: {}", e.to_string()))?; + .map_err(|e| format!("Unable to access file or directory: {}", e))?; if !metadata.is_dir() { return Err("Invalid path: not a directory".to_string()); } let dir_contents = new_dir_path .read_dir() - .map_err(|e| format!("Unable to check directory contents: {}", e.to_string()))?; + .map_err(|e| format!("Unable to check directory contents: {}", e))?; if dir_contents.count() == 0 { return Err("Path is not empty".to_string()); } } else { create_dir_all(new_dir_path) - .map_err(|e| format!("Unable to create directories to path: {}", e.to_string()))?; + .map_err(|e| format!("Unable to create directories to path: {}", e))?; } // Add it to the dictionary diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index c03ac3f..8e22729 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::path::Path; -use std::sync::atomic::AtomicU64; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex, RwLock}; use urlencoding::encode; @@ -22,17 +22,17 @@ use super::download_logic::download_game_chunk; pub struct GameDownloadAgent { pub id: String, pub version: String, - pub control_flag: Arc>, + pub control_flag: Arc, pub target_download_dir: usize, contexts: Mutex>, pub manifest: Mutex>, pub progress: ProgressObject, } -#[derive(Serialize, Deserialize, Clone, Eq, PartialEq)] -pub enum DownloadThreadControlFlag { - Go, - Stop, -} + +/// Faster alternative to a RwLock Enum. +/// true = Go +/// false = Stop +pub type DownloadThreadControlFlag = AtomicBool; #[derive(Debug)] pub enum GameDownloadError { @@ -61,7 +61,7 @@ pub struct ProgressObject { impl GameDownloadAgent { pub fn new(id: String, version: String, target_download_dir: usize) -> Self { // Don't run by default - let status = Arc::new(RwLock::new(DownloadThreadControlFlag::Stop)); + let status = Arc::new(DownloadThreadControlFlag::new(false)); Self { id, version, @@ -75,13 +75,11 @@ impl GameDownloadAgent { }, } } - pub fn set_control_flag(&self, flag: DownloadThreadControlFlag) { - let mut lock = self.control_flag.write().unwrap(); - *lock = flag; + pub fn set_control_flag(&self, flag: bool) { + self.control_flag.store(flag, Ordering::Relaxed); } - pub fn get_control_flag(&self) -> DownloadThreadControlFlag { - let lock = self.control_flag.read().unwrap(); - lock.clone() + pub fn get_control_flag(&self) -> bool { + self.control_flag.load(Ordering::Relaxed) } // Blocking @@ -91,7 +89,7 @@ impl GameDownloadAgent { self.generate_contexts()?; - self.set_control_flag(DownloadThreadControlFlag::Go); + self.set_control_flag(true); Ok(()) } @@ -110,7 +108,7 @@ impl GameDownloadAgent { } // Explicitly propagate error - Ok(self.download_manifest()?) + self.download_manifest() } fn download_manifest(&mut self) -> Result<(), GameDownloadError> { @@ -159,7 +157,7 @@ impl GameDownloadAgent { return Ok(()); } - return Err(GameDownloadError::LockError); + Err(GameDownloadError::LockError) } pub fn generate_contexts(&self) -> Result<(), GameDownloadError> { @@ -210,9 +208,9 @@ impl GameDownloadAgent { return Ok(()); } - return Err(GameDownloadError::SetupError( + Err(GameDownloadError::SetupError( "Failed to generate download contexts".to_owned(), - )); + )) } pub fn run(&self) { diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 86f1c71..b770b9b 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -1,7 +1,4 @@ -use std::{ - borrow::Borrow, - sync::{Arc, Mutex}, -}; +use std::sync::{Arc, Mutex}; use log::info; use rayon::spawn; diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 69d1f8f..83a9fb1 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -3,15 +3,12 @@ use crate::db::DatabaseImpls; use crate::downloads::manifest::DropDownloadContext; use crate::remote::RemoteAccessError; use crate::DB; -use atomic_counter::{AtomicCounter, RelaxedCounter}; use log::{error, info}; use md5::{Context, Digest}; use reqwest::blocking::Response; -use serde::de::Error; use std::io::Read; -use std::sync::atomic::AtomicU64; -use std::sync::RwLock; +use std::sync::atomic::{AtomicU64, Ordering}; use std::{ fs::{File, OpenOptions}, io::{self, BufWriter, ErrorKind, Seek, SeekFrom, Write}, @@ -66,7 +63,7 @@ impl Seek for DropWriter { pub struct DropDownloadPipeline { pub source: R, pub destination: DropWriter, - pub control_flag: Arc>, + pub control_flag: Arc, pub progress: Arc, pub size: usize, } @@ -74,7 +71,7 @@ impl DropDownloadPipeline { fn new( source: Response, destination: DropWriter, - control_flag: Arc>, + control_flag: Arc, progress: Arc, size: usize, ) -> Self { @@ -94,7 +91,7 @@ impl DropDownloadPipeline { let mut current_size = 0; loop { - if *self.control_flag.read().unwrap() == DownloadThreadControlFlag::Stop { + if self.control_flag.load(Ordering::Relaxed) == false { return Ok(false); } @@ -123,11 +120,11 @@ impl DropDownloadPipeline { pub fn download_game_chunk( ctx: DropDownloadContext, - control_flag: Arc>, + control_flag: Arc, progress: Arc, ) -> Result { // If we're paused - if *control_flag.read().unwrap() == DownloadThreadControlFlag::Stop { + if control_flag.load(Ordering::Relaxed) { return Ok(false); } diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index 4e59d68..7f1b227 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -1,4 +1,4 @@ pub mod download_agent; pub mod download_commands; mod download_logic; -mod manifest; \ No newline at end of file +mod manifest; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3780909..bef2fab 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -20,7 +20,6 @@ use log::info; use remote::{gen_drop_url, use_remote}; use serde::{Deserialize, Serialize}; use std::sync::Arc; -use std::time::Duration; use std::{ collections::HashMap, sync::{LazyLock, Mutex}, diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index fadef14..183ba76 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -1 +1 @@ -pub const DOWNLOAD_MAX_THREADS: usize = 4; \ No newline at end of file +pub const DOWNLOAD_MAX_THREADS: usize = 4; From 5e05e6873db0b82db020d3c3e43962f6456b408e Mon Sep 17 00:00:00 2001 From: quexeky Date: Mon, 11 Nov 2024 10:05:49 +1100 Subject: [PATCH 091/164] feat(downloads): Added DownloadThreadControl struct Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 22 +++-------- src-tauri/src/downloads/download_logic.rs | 18 ++++----- .../downloads/download_thread_control_flag.rs | 37 +++++++++++++++++++ src-tauri/src/downloads/mod.rs | 1 + 4 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 src-tauri/src/downloads/download_thread_control_flag.rs diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 8e22729..1acd7f0 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -10,30 +10,26 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::path::Path; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::atomic::{AtomicU64}; +use std::sync::{Arc, Mutex}; use urlencoding::encode; #[cfg(target_os = "linux")] use rustix::fs::{fallocate, FallocateFlags}; use super::download_logic::download_game_chunk; +use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; pub struct GameDownloadAgent { pub id: String, pub version: String, - pub control_flag: Arc, + pub control_flag: DownloadThreadControl, pub target_download_dir: usize, contexts: Mutex>, pub manifest: Mutex>, pub progress: ProgressObject, } -/// Faster alternative to a RwLock Enum. -/// true = Go -/// false = Stop -pub type DownloadThreadControlFlag = AtomicBool; - #[derive(Debug)] pub enum GameDownloadError { CommunicationError(RemoteAccessError), @@ -61,7 +57,7 @@ pub struct ProgressObject { impl GameDownloadAgent { pub fn new(id: String, version: String, target_download_dir: usize) -> Self { // Don't run by default - let status = Arc::new(DownloadThreadControlFlag::new(false)); + let status = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); Self { id, version, @@ -75,12 +71,6 @@ impl GameDownloadAgent { }, } } - pub fn set_control_flag(&self, flag: bool) { - self.control_flag.store(flag, Ordering::Relaxed); - } - pub fn get_control_flag(&self) -> bool { - self.control_flag.load(Ordering::Relaxed) - } // Blocking // Requires mutable self @@ -89,7 +79,7 @@ impl GameDownloadAgent { self.generate_contexts()?; - self.set_control_flag(true); + self.control_flag.set(DownloadThreadControlFlag::Go); Ok(()) } diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 83a9fb1..639643d 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -3,12 +3,11 @@ use crate::db::DatabaseImpls; use crate::downloads::manifest::DropDownloadContext; use crate::remote::RemoteAccessError; use crate::DB; -use log::{error, info}; use md5::{Context, Digest}; use reqwest::blocking::Response; use std::io::Read; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::atomic::AtomicU64; use std::{ fs::{File, OpenOptions}, io::{self, BufWriter, ErrorKind, Seek, SeekFrom, Write}, @@ -17,7 +16,8 @@ use std::{ }; use urlencoding::encode; -use super::download_agent::{DownloadThreadControlFlag, GameDownloadError}; +use super::download_agent::GameDownloadError; +use super::download_thread_control_flag::DownloadThreadControl; pub struct DropWriter { hasher: Context, @@ -63,7 +63,7 @@ impl Seek for DropWriter { pub struct DropDownloadPipeline { pub source: R, pub destination: DropWriter, - pub control_flag: Arc, + pub control_flag: DownloadThreadControl, pub progress: Arc, pub size: usize, } @@ -71,7 +71,7 @@ impl DropDownloadPipeline { fn new( source: Response, destination: DropWriter, - control_flag: Arc, + control_flag: DownloadThreadControl, progress: Arc, size: usize, ) -> Self { @@ -91,14 +91,14 @@ impl DropDownloadPipeline { let mut current_size = 0; loop { - if self.control_flag.load(Ordering::Relaxed) == false { + if self.control_flag.get() == false { return Ok(false); } let bytes_read = self.source.read(&mut copy_buf)?; current_size += bytes_read; - buf_writer.write(©_buf[0..bytes_read])?; + buf_writer.write_all(©_buf[0..bytes_read])?; self.progress.fetch_add( bytes_read.try_into().unwrap(), std::sync::atomic::Ordering::Relaxed, @@ -120,11 +120,11 @@ impl DropDownloadPipeline { pub fn download_game_chunk( ctx: DropDownloadContext, - control_flag: Arc, + control_flag: DownloadThreadControl, progress: Arc, ) -> Result { // If we're paused - if control_flag.load(Ordering::Relaxed) { + if control_flag.get() { return Ok(false); } diff --git a/src-tauri/src/downloads/download_thread_control_flag.rs b/src-tauri/src/downloads/download_thread_control_flag.rs new file mode 100644 index 0000000..793a903 --- /dev/null +++ b/src-tauri/src/downloads/download_thread_control_flag.rs @@ -0,0 +1,37 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +pub enum DownloadThreadControlFlag { + Stop, + Go, +} +impl From for bool { + fn from(value: DownloadThreadControlFlag) -> Self { + match value { + DownloadThreadControlFlag::Stop => false, + DownloadThreadControlFlag::Go => true, + } + } +} + + +#[derive(Clone)] +pub struct DownloadThreadControl { + inner: Arc, +} + +impl DownloadThreadControl { + pub fn new(flag: DownloadThreadControlFlag) -> Self { + Self { + inner: Arc::new(AtomicBool::new(flag.into())), + } + } + pub fn get(&self) -> bool { + self.inner.load(Ordering::Relaxed) + } + pub fn set(&self, flag: DownloadThreadControlFlag) { + self.inner.store(flag.into(), Ordering::Relaxed); + } +} diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index 7f1b227..2848567 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -1,4 +1,5 @@ pub mod download_agent; pub mod download_commands; mod download_logic; +mod download_thread_control_flag; mod manifest; From 7d3c6011df4ca3fbd93b122c7bdd7cd3e3b35c22 Mon Sep 17 00:00:00 2001 From: quexeky Date: Mon, 11 Nov 2024 18:07:45 +1100 Subject: [PATCH 092/164] feat(downloads): Separated chunk updates into individual counters Also added a From for DownloadThreadControlFlag because I accidentally was calling the wrong one before and had meant to add it anyway Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 35 +++++++++---------- src-tauri/src/downloads/download_commands.rs | 10 ++---- src-tauri/src/downloads/download_logic.rs | 15 ++++---- .../downloads/download_thread_control_flag.rs | 13 +++++-- src-tauri/src/downloads/mod.rs | 1 + src-tauri/src/downloads/progress_object.rs | 29 +++++++++++++++ src-tauri/src/settings.rs | 1 - 7 files changed, 68 insertions(+), 36 deletions(-) create mode 100644 src-tauri/src/downloads/progress_object.rs diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 1acd7f0..b58d944 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -2,7 +2,6 @@ use crate::auth::generate_authorization_header; use crate::db::DatabaseImpls; use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::remote::RemoteAccessError; -use crate::settings::DOWNLOAD_MAX_THREADS; use crate::DB; use log::info; use rayon::ThreadPoolBuilder; @@ -10,7 +9,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::path::Path; -use std::sync::atomic::{AtomicU64}; +use std::sync::atomic::AtomicUsize; use std::sync::{Arc, Mutex}; use urlencoding::encode; @@ -19,6 +18,7 @@ use rustix::fs::{fallocate, FallocateFlags}; use super::download_logic::download_game_chunk; use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; +use super::progress_object::ProgressObject; pub struct GameDownloadAgent { pub id: String, @@ -49,11 +49,6 @@ impl Display for GameDownloadError { } } -pub struct ProgressObject { - pub max: u64, - pub current: Arc, -} - impl GameDownloadAgent { pub fn new(id: String, version: String, target_download_dir: usize) -> Self { // Don't run by default @@ -65,10 +60,7 @@ impl GameDownloadAgent { manifest: Mutex::new(None), target_download_dir, contexts: Mutex::new(Vec::new()), - progress: ProgressObject { - max: 0, - current: Arc::new(AtomicU64::new(0)), - }, + progress: ProgressObject::new(0, 0), } } @@ -76,8 +68,10 @@ impl GameDownloadAgent { // Requires mutable self pub fn setup_download(&mut self) -> Result<(), GameDownloadError> { self.ensure_manifest_exists()?; + info!("Ensured manifest exists"); self.generate_contexts()?; + info!("Generated contexts"); self.control_flag.set(DownloadThreadControlFlag::Go); @@ -140,7 +134,10 @@ impl GameDownloadAgent { return chunk.lengths.iter().sum::(); }) .sum::(); - self.progress.max = length.try_into().unwrap(); + let chunk_count = manifest_download.iter().map(|(_, chunk)| { + chunk.lengths.len() + }).sum(); + self.progress = ProgressObject::new(length.try_into().unwrap(), chunk_count); if let Ok(mut manifest) = self.manifest.lock() { *manifest = Some(manifest_download); @@ -204,6 +201,8 @@ impl GameDownloadAgent { } pub fn run(&self) { + const DOWNLOAD_MAX_THREADS: usize = 4; + let pool = ThreadPoolBuilder::new() .num_threads(DOWNLOAD_MAX_THREADS) .build() @@ -212,16 +211,16 @@ impl GameDownloadAgent { pool.scope(move |scope| { let contexts = self.contexts.lock().unwrap(); - for context in contexts.iter() { + for (index, context) in contexts.iter().enumerate() { let context = context.clone(); let control_flag = self.control_flag.clone(); // Clone arcs - let progress = self.progress.current.clone(); // Clone arcs - info!( - "starting download for file {} {}", - context.file_name, context.index - ); + let progress = self.progress.get(index); // Clone arcs scope.spawn(move |_| { + info!( + "starting download for file {} {}", + context.file_name, context.index + ); download_game_chunk(context, control_flag, progress).unwrap(); }); } diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index b770b9b..749705c 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, Mutex}; +use std::sync::{atomic::Ordering, Arc, Mutex}; use log::info; use rayon::spawn; @@ -39,14 +39,8 @@ pub fn get_game_download_progress( let da = use_download_agent(state, game_id)?; let progress = &da.progress; - let current: f64 = progress - .current - .fetch_add(0, std::sync::atomic::Ordering::Relaxed) as f64; - let max = progress.max as f64; - let current_progress = current / max; - - Ok(current_progress) + Ok(progress.get_progress()) } fn use_download_agent( diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 639643d..d772af4 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -3,11 +3,12 @@ use crate::db::DatabaseImpls; use crate::downloads::manifest::DropDownloadContext; use crate::remote::RemoteAccessError; use crate::DB; +use log::info; use md5::{Context, Digest}; use reqwest::blocking::Response; use std::io::Read; -use std::sync::atomic::AtomicU64; +use std::sync::atomic::AtomicUsize; use std::{ fs::{File, OpenOptions}, io::{self, BufWriter, ErrorKind, Seek, SeekFrom, Write}, @@ -17,7 +18,7 @@ use std::{ use urlencoding::encode; use super::download_agent::GameDownloadError; -use super::download_thread_control_flag::DownloadThreadControl; +use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; pub struct DropWriter { hasher: Context, @@ -64,7 +65,7 @@ pub struct DropDownloadPipeline { pub source: R, pub destination: DropWriter, pub control_flag: DownloadThreadControl, - pub progress: Arc, + pub progress: Arc, pub size: usize, } impl DropDownloadPipeline { @@ -72,7 +73,7 @@ impl DropDownloadPipeline { source: Response, destination: DropWriter, control_flag: DownloadThreadControl, - progress: Arc, + progress: Arc, size: usize, ) -> Self { return Self { @@ -91,7 +92,7 @@ impl DropDownloadPipeline { let mut current_size = 0; loop { - if self.control_flag.get() == false { + if self.control_flag.get() == DownloadThreadControlFlag::Stop { return Ok(false); } @@ -121,10 +122,10 @@ impl DropDownloadPipeline { pub fn download_game_chunk( ctx: DropDownloadContext, control_flag: DownloadThreadControl, - progress: Arc, + progress: Arc, ) -> Result { // If we're paused - if control_flag.get() { + if control_flag.get() == DownloadThreadControlFlag::Stop { return Ok(false); } diff --git a/src-tauri/src/downloads/download_thread_control_flag.rs b/src-tauri/src/downloads/download_thread_control_flag.rs index 793a903..6a18aef 100644 --- a/src-tauri/src/downloads/download_thread_control_flag.rs +++ b/src-tauri/src/downloads/download_thread_control_flag.rs @@ -3,6 +3,7 @@ use std::sync::{ Arc, }; +#[derive(PartialEq, Eq, PartialOrd, Ord)] pub enum DownloadThreadControlFlag { Stop, Go, @@ -15,6 +16,14 @@ impl From for bool { } } } +impl From for DownloadThreadControlFlag { + fn from(value: bool) -> Self { + match value { + true => DownloadThreadControlFlag::Go, + false => DownloadThreadControlFlag::Stop, + } + } +} #[derive(Clone)] @@ -28,8 +37,8 @@ impl DownloadThreadControl { inner: Arc::new(AtomicBool::new(flag.into())), } } - pub fn get(&self) -> bool { - self.inner.load(Ordering::Relaxed) + pub fn get(&self) -> DownloadThreadControlFlag { + self.inner.load(Ordering::Relaxed).into() } pub fn set(&self, flag: DownloadThreadControlFlag) { self.inner.store(flag.into(), Ordering::Relaxed); diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index 2848567..9559d8a 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -3,3 +3,4 @@ pub mod download_commands; mod download_logic; mod download_thread_control_flag; mod manifest; +mod progress_object; \ No newline at end of file diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs new file mode 100644 index 0000000..a9625b5 --- /dev/null +++ b/src-tauri/src/downloads/progress_object.rs @@ -0,0 +1,29 @@ +use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; + +#[derive(Clone)] +pub struct ProgressObject { + max: usize, + progress_instances: Arc>>, +} + +impl ProgressObject { + pub fn new(max: usize, length: usize) -> Self { + let arr = (0..length).map(|_| { Arc::new(AtomicUsize::new(0)) }).collect(); + Self { + max, + progress_instances: Arc::new(arr) + } + } + pub fn sum(&self) -> usize { + self.progress_instances.iter().map(|instance| { + instance.load(Ordering::Relaxed) + }).sum() + } + + pub fn get_progress(&self) -> f64 { + self.sum() as f64 / self.max as f64 + } + pub fn get(&self, index: usize) -> Arc { + self.progress_instances[index].clone() + } +} \ No newline at end of file diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index 183ba76..e69de29 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -1 +0,0 @@ -pub const DOWNLOAD_MAX_THREADS: usize = 4; From 17244496ecd3bae4174b6b6bc86fb78ab51780b6 Mon Sep 17 00:00:00 2001 From: quexeky Date: Mon, 11 Nov 2024 18:25:26 +1100 Subject: [PATCH 093/164] refactor: Removed unnecessary dependencies Signed-off-by: quexeky --- src-tauri/Cargo.lock | 7 ------- src-tauri/Cargo.toml | 1 - src-tauri/src/downloads/download_agent.rs | 4 +--- src-tauri/src/downloads/download_commands.rs | 6 +++--- src-tauri/src/downloads/download_logic.rs | 1 - 5 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c67067b..0b03042 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -298,12 +298,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "atomic-counter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f447d68cfa5a9ab0c1c862a703da2a65b5ed1b7ce1153c9eb0169506d56019" - [[package]] name = "atomic-waker" version = "1.1.2" @@ -997,7 +991,6 @@ dependencies = [ name = "drop-app" version = "0.1.0" dependencies = [ - "atomic-counter", "directories", "env_logger", "hex", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9ef8f6c..8e6cff1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -42,7 +42,6 @@ http = "1.1.0" tokio = { version = "1.40.0", features = ["rt", "tokio-macros", "signal"] } urlencoding = "2.1.3" md5 = "0.7.0" -atomic-counter = "1.0.1" [dependencies.rustix] version = "0.38.37" diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index b58d944..c612025 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -5,12 +5,10 @@ use crate::remote::RemoteAccessError; use crate::DB; use log::info; use rayon::ThreadPoolBuilder; -use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::path::Path; -use std::sync::atomic::AtomicUsize; -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; use urlencoding::encode; #[cfg(target_os = "linux")] diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 749705c..54b9315 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -1,4 +1,4 @@ -use std::sync::{atomic::Ordering, Arc, Mutex}; +use std::sync::{Arc, Mutex}; use log::info; use rayon::spawn; @@ -36,9 +36,9 @@ pub fn get_game_download_progress( state: tauri::State<'_, Mutex>, game_id: String, ) -> Result { - let da = use_download_agent(state, game_id)?; + let download_agent = use_download_agent(state, game_id)?; - let progress = &da.progress; + let progress = &download_agent.progress; Ok(progress.get_progress()) } diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index d772af4..7b4ca78 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -3,7 +3,6 @@ use crate::db::DatabaseImpls; use crate::downloads::manifest::DropDownloadContext; use crate::remote::RemoteAccessError; use crate::DB; -use log::info; use md5::{Context, Digest}; use reqwest::blocking::Response; From 5e3d26b3ca32229f3ba250a39a4ef2051017a62e Mon Sep 17 00:00:00 2001 From: quexeky Date: Mon, 11 Nov 2024 18:27:39 +1100 Subject: [PATCH 094/164] refactor(downloads): ran cargo clippy & cargo fmt Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 7 ++++--- src-tauri/src/downloads/download_logic.rs | 14 ++++++-------- .../downloads/download_thread_control_flag.rs | 1 - src-tauri/src/downloads/mod.rs | 2 +- src-tauri/src/downloads/progress_object.rs | 18 +++++++++++------- src-tauri/src/settings.rs | 1 + 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index c612025..a46eceb 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -132,9 +132,10 @@ impl GameDownloadAgent { return chunk.lengths.iter().sum::(); }) .sum::(); - let chunk_count = manifest_download.iter().map(|(_, chunk)| { - chunk.lengths.len() - }).sum(); + let chunk_count = manifest_download + .values() + .map(|chunk| chunk.lengths.len()) + .sum(); self.progress = ProgressObject::new(length.try_into().unwrap(), chunk_count); if let Ok(mut manifest) = self.manifest.lock() { diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 7b4ca78..2a0c21f 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -75,13 +75,13 @@ impl DropDownloadPipeline { progress: Arc, size: usize, ) -> Self { - return Self { + Self { source, destination, control_flag, progress, size, - }; + } } fn copy(&mut self) -> Result { @@ -99,10 +99,8 @@ impl DropDownloadPipeline { current_size += bytes_read; buf_writer.write_all(©_buf[0..bytes_read])?; - self.progress.fetch_add( - bytes_read.try_into().unwrap(), - std::sync::atomic::Ordering::Relaxed, - ); + self.progress + .fetch_add(bytes_read, std::sync::atomic::Ordering::Relaxed); if current_size == self.size { break; @@ -114,7 +112,7 @@ impl DropDownloadPipeline { fn finish(self) -> Result { let checksum = self.destination.finish()?; - return Ok(checksum); + Ok(checksum) } } @@ -187,5 +185,5 @@ pub fn download_game_chunk( return Err(GameDownloadError::ChecksumError); } - return Ok(true); + Ok(true) } diff --git a/src-tauri/src/downloads/download_thread_control_flag.rs b/src-tauri/src/downloads/download_thread_control_flag.rs index 6a18aef..30da7c6 100644 --- a/src-tauri/src/downloads/download_thread_control_flag.rs +++ b/src-tauri/src/downloads/download_thread_control_flag.rs @@ -25,7 +25,6 @@ impl From for DownloadThreadControlFlag { } } - #[derive(Clone)] pub struct DownloadThreadControl { inner: Arc, diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index 9559d8a..d4ade96 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -3,4 +3,4 @@ pub mod download_commands; mod download_logic; mod download_thread_control_flag; mod manifest; -mod progress_object; \ No newline at end of file +mod progress_object; diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs index a9625b5..f3be4fb 100644 --- a/src-tauri/src/downloads/progress_object.rs +++ b/src-tauri/src/downloads/progress_object.rs @@ -1,4 +1,7 @@ -use std::sync::{atomic::{AtomicUsize, Ordering}, Arc}; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; #[derive(Clone)] pub struct ProgressObject { @@ -8,16 +11,17 @@ pub struct ProgressObject { impl ProgressObject { pub fn new(max: usize, length: usize) -> Self { - let arr = (0..length).map(|_| { Arc::new(AtomicUsize::new(0)) }).collect(); + let arr = (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect(); Self { max, - progress_instances: Arc::new(arr) + progress_instances: Arc::new(arr), } } pub fn sum(&self) -> usize { - self.progress_instances.iter().map(|instance| { - instance.load(Ordering::Relaxed) - }).sum() + self.progress_instances + .iter() + .map(|instance| instance.load(Ordering::Relaxed)) + .sum() } pub fn get_progress(&self) -> f64 { @@ -26,4 +30,4 @@ impl ProgressObject { pub fn get(&self, index: usize) -> Arc { self.progress_instances[index].clone() } -} \ No newline at end of file +} diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index e69de29..8b13789 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -0,0 +1 @@ + From 5ba151f8131fb1cfcc16bfd0c1c9fc304d6f28ec Mon Sep 17 00:00:00 2001 From: quexeky Date: Tue, 12 Nov 2024 09:02:58 +1100 Subject: [PATCH 095/164] fix(downloads): Chunk counting logic error Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index a46eceb..9ae8ba8 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -136,7 +136,7 @@ impl GameDownloadAgent { .values() .map(|chunk| chunk.lengths.len()) .sum(); - self.progress = ProgressObject::new(length.try_into().unwrap(), chunk_count); + self.progress = ProgressObject::new(length, chunk_count); if let Ok(mut manifest) = self.manifest.lock() { *manifest = Some(manifest_download); @@ -170,15 +170,15 @@ impl GameDownloadAgent { let file = File::create(path.clone()).unwrap(); let mut running_offset = 0; - for (i, length) in chunk.lengths.iter().enumerate() { + for (index, length) in chunk.lengths.iter().enumerate() { contexts.push(DropDownloadContext { file_name: raw_path.to_string(), version: version.to_string(), offset: running_offset, - index: i, + index, game_id: game_id.to_string(), path: path.clone(), - checksum: chunk.checksums[i].clone(), + checksum: chunk.checksums[index].clone(), }); running_offset += *length as u64; } @@ -195,7 +195,7 @@ impl GameDownloadAgent { } Err(GameDownloadError::SetupError( - "Failed to generate download contexts".to_owned(), + String::from("Failed to generate download contexts"), )) } @@ -213,7 +213,7 @@ impl GameDownloadAgent { for (index, context) in contexts.iter().enumerate() { let context = context.clone(); let control_flag = self.control_flag.clone(); // Clone arcs - let progress = self.progress.get(index); // Clone arcs + let progress = self.progress.get(index); scope.spawn(move |_| { info!( From ab606e8e330f9f6929bca20f6aef5afc38f56a1d Mon Sep 17 00:00:00 2001 From: quexeky Date: Tue, 12 Nov 2024 09:03:36 +1100 Subject: [PATCH 096/164] refactor(downloads): Reordered DownloadThreadControlFlag to agree with From Signed-off-by: quexeky --- src-tauri/src/downloads/download_thread_control_flag.rs | 6 +++++- src-tauri/src/downloads/mod.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/downloads/download_thread_control_flag.rs b/src-tauri/src/downloads/download_thread_control_flag.rs index 30da7c6..154fcfc 100644 --- a/src-tauri/src/downloads/download_thread_control_flag.rs +++ b/src-tauri/src/downloads/download_thread_control_flag.rs @@ -8,14 +8,18 @@ pub enum DownloadThreadControlFlag { Stop, Go, } +/// Go => true +/// Stop => false impl From for bool { fn from(value: DownloadThreadControlFlag) -> Self { match value { - DownloadThreadControlFlag::Stop => false, DownloadThreadControlFlag::Go => true, + DownloadThreadControlFlag::Stop => false, } } } +/// true => Go +/// false => Stop impl From for DownloadThreadControlFlag { fn from(value: bool) -> Self { match value { diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index d4ade96..9559d8a 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -3,4 +3,4 @@ pub mod download_commands; mod download_logic; mod download_thread_control_flag; mod manifest; -mod progress_object; +mod progress_object; \ No newline at end of file From 3dbf5ab5737857f961b7b6e17cc01ffc7c801e25 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 12 Nov 2024 09:06:28 +1100 Subject: [PATCH 097/164] chore(downloads): partial download manager --- src-tauri/src/downloads/download_agent.rs | 9 +++-- src-tauri/src/downloads/download_commands.rs | 13 +++++-- src-tauri/src/downloads/download_manager.rs | 40 ++++++++++++++++++++ src-tauri/src/downloads/mod.rs | 1 + src-tauri/src/lib.rs | 14 ++++--- 5 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 src-tauri/src/downloads/download_manager.rs diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index a46eceb..8bcd2b5 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -4,11 +4,13 @@ use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::remote::RemoteAccessError; use crate::DB; use log::info; -use rayon::ThreadPoolBuilder; +use rayon::{spawn, ThreadPool, ThreadPoolBuilder}; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::path::Path; -use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread::Thread; use urlencoding::encode; #[cfg(target_os = "linux")] @@ -207,6 +209,7 @@ impl GameDownloadAgent { .build() .unwrap(); + pool.scope(move |scope| { let contexts = self.contexts.lock().unwrap(); @@ -223,6 +226,6 @@ impl GameDownloadAgent { download_game_chunk(context, control_flag, progress).unwrap(); }); } - }) + }); } } diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 54b9315..9449ab2 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -11,6 +11,7 @@ pub fn download_game( game_version: String, state: tauri::State<'_, Mutex>, ) -> Result<(), String> { + /* info!("beginning game download..."); let mut download_agent = GameDownloadAgent::new(game_id.clone(), game_version.clone(), 0); @@ -19,7 +20,7 @@ pub fn download_game( let mut lock: std::sync::MutexGuard<'_, AppState> = state.lock().unwrap(); let download_agent_ref = Arc::new(download_agent); - lock.game_downloads + lock.download_manager .insert(game_id, download_agent_ref.clone()); // Run it in another thread @@ -27,6 +28,7 @@ pub fn download_game( // Run doesn't require mutable download_agent_ref.clone().run(); }); + */ Ok(()) } @@ -36,18 +38,23 @@ pub fn get_game_download_progress( state: tauri::State<'_, Mutex>, game_id: String, ) -> Result { + /* let download_agent = use_download_agent(state, game_id)?; let progress = &download_agent.progress; Ok(progress.get_progress()) -} + */ + Ok(0.0) +} +/* fn use_download_agent( state: tauri::State<'_, Mutex>, game_id: String, ) -> Result, String> { let lock = state.lock().unwrap(); - let download_agent = lock.game_downloads.get(&game_id).ok_or("Invalid game ID")?; + let download_agent = lock.download_manager.get(&game_id).ok_or("Invalid game ID")?; Ok(download_agent.clone()) // Clones the Arc, not the underlying data structure } +*/ \ No newline at end of file diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs new file mode 100644 index 0000000..3c14fa8 --- /dev/null +++ b/src-tauri/src/downloads/download_manager.rs @@ -0,0 +1,40 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, + thread::JoinHandle, +}; + +use super::{download_agent::GameDownloadAgent, download_thread_control_flag::DownloadThreadControlFlag}; + +pub struct DownloadManager { + download_agent_registry: HashMap>>, + download_queue: Vec, + + current_thread: Option>, + current_game_id: Option, // Should be the only game download agent in the map with the "Go" flag +} + +impl DownloadManager { + pub fn new() -> Self { + return Self { + download_agent_registry: HashMap::new(), + download_queue: Vec::new(), + current_thread: None, + current_game_id: None, + }; + } + + pub fn queue_game(&mut self, game_id: String, version_name: String) { + let existing_da = self.download_agent_registry.get(&game_id); + + if let Some(da_mutex) = existing_da { + let da = da_mutex.lock().unwrap(); + if da.version == version_name { + return; // We're already queued + } + + da.control_flag.set(DownloadThreadControlFlag::Stop); + + } + } +} diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index d4ade96..ea2edfd 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -1,5 +1,6 @@ pub mod download_agent; pub mod download_commands; +pub mod download_manager; mod download_logic; mod download_thread_control_flag; mod manifest; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bef2fab..5e880a2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,6 +13,7 @@ use crate::downloads::download_agent::GameDownloadAgent; use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; use db::{add_new_download_dir, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; +use downloads::download_manager::DownloadManager; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; use library::{fetch_game, fetch_library, Game}; @@ -53,7 +54,7 @@ pub struct AppState { games: HashMap, #[serde(skip_serializing)] - game_downloads: HashMap>, + download_manager: Arc, } #[tauri::command] @@ -67,13 +68,16 @@ fn fetch_state(state: tauri::State<'_, Mutex>) -> Result AppState { env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); + let games = HashMap::new(); + let download_manager = Arc::new(DownloadManager::new()); + let is_set_up = DB.database_is_set_up(); if !is_set_up { return AppState { status: AppStatus::NotConfigured, user: None, - games: HashMap::new(), - game_downloads: HashMap::new(), + games, + download_manager, }; } @@ -81,8 +85,8 @@ fn setup() -> AppState { AppState { status: app_status, user, - games: HashMap::new(), - game_downloads: HashMap::new(), + games, + download_manager, } } From a1ada07690b24811b7458ed61bc8540ee361fdfc Mon Sep 17 00:00:00 2001 From: quexeky Date: Wed, 13 Nov 2024 20:38:00 +1100 Subject: [PATCH 098/164] feat(downloads): Added Download Manager Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 12 +- src-tauri/src/downloads/download_commands.rs | 10 +- src-tauri/src/downloads/download_logic.rs | 2 + src-tauri/src/downloads/download_manager.rs | 196 ++++++++++++++++--- src-tauri/src/downloads/mod.rs | 2 +- src-tauri/src/downloads/progress_object.rs | 25 ++- src-tauri/src/lib.rs | 6 +- 7 files changed, 206 insertions(+), 47 deletions(-) diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 8bcd2b5..c977ecd 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -66,7 +66,7 @@ impl GameDownloadAgent { // Blocking // Requires mutable self - pub fn setup_download(&mut self) -> Result<(), GameDownloadError> { + pub fn setup_download(&self) -> Result<(), GameDownloadError> { self.ensure_manifest_exists()?; info!("Ensured manifest exists"); @@ -79,23 +79,22 @@ impl GameDownloadAgent { } // Blocking - pub fn download(&mut self) -> Result<(), GameDownloadError> { + pub fn download(&self) -> Result<(), GameDownloadError> { self.setup_download()?; self.run(); Ok(()) } - pub fn ensure_manifest_exists(&mut self) -> Result<(), GameDownloadError> { + pub fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> { if self.manifest.lock().unwrap().is_some() { return Ok(()); } - // Explicitly propagate error self.download_manifest() } - fn download_manifest(&mut self) -> Result<(), GameDownloadError> { + fn download_manifest(&self) -> Result<(), GameDownloadError> { let base_url = DB.fetch_base_url(); let manifest_url = base_url .join( @@ -138,7 +137,8 @@ impl GameDownloadAgent { .values() .map(|chunk| chunk.lengths.len()) .sum(); - self.progress = ProgressObject::new(length.try_into().unwrap(), chunk_count); + self.progress.set_max(length.try_into().unwrap()); + self.progress.set_size(chunk_count); if let Ok(mut manifest) = self.manifest.lock() { *manifest = Some(manifest_download); diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 9449ab2..c1ab541 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Mutex}; use log::info; use rayon::spawn; -use crate::{downloads::download_agent::GameDownloadAgent, AppState}; +use crate::{AppState}; #[tauri::command] pub fn download_game( @@ -29,7 +29,7 @@ pub fn download_game( download_agent_ref.clone().run(); }); */ - + state.lock().unwrap().download_manager.queue_game(game_id, game_version, 0).unwrap(); Ok(()) } @@ -45,8 +45,12 @@ pub fn get_game_download_progress( Ok(progress.get_progress()) */ + let progress = match state.lock().unwrap().download_manager.get_current_game_download_progress() { + Some(progress) => progress, + None => 0.0 + }; - Ok(0.0) + Ok(progress) } /* fn use_download_agent( diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 2a0c21f..dc95f2f 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -3,6 +3,7 @@ use crate::db::DatabaseImpls; use crate::downloads::manifest::DropDownloadContext; use crate::remote::RemoteAccessError; use crate::DB; +use log::info; use md5::{Context, Digest}; use reqwest::blocking::Response; @@ -123,6 +124,7 @@ pub fn download_game_chunk( ) -> Result { // If we're paused if control_flag.get() == DownloadThreadControlFlag::Stop { + info!("Control flag is Stop"); return Ok(false); } diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 3c14fa8..dcd7bf5 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -1,40 +1,182 @@ use std::{ - collections::HashMap, - sync::{Arc, Mutex}, - thread::JoinHandle, + collections::{HashMap, VecDeque}, sync::{mpsc::{channel, Receiver, SendError, Sender}, Arc, Mutex, MutexGuard}, thread::{spawn, JoinHandle}, }; -use super::{download_agent::GameDownloadAgent, download_thread_control_flag::DownloadThreadControlFlag}; +use log::info; + +use super::{download_agent::GameDownloadAgent, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject}; pub struct DownloadManager { - download_agent_registry: HashMap>>, - download_queue: Vec, + download_agent_registry: HashMap>, + download_queue: Arc>>, + receiver: Receiver, + sender: Sender, + progress: Arc>>, - current_thread: Option>, current_game_id: Option, // Should be the only game download agent in the map with the "Go" flag + active_control_flag: Option +} +pub struct DownloadManagerInterface { + terminator: JoinHandle>, + download_queue: Arc>>, + progress: Arc>>, + sender: Sender, +} +pub enum DownloadManagerSignal { + Go, + Stop, + Completed(String), + Queue(String, String, usize) } -impl DownloadManager { - pub fn new() -> Self { - return Self { - download_agent_registry: HashMap::new(), - download_queue: Vec::new(), - current_thread: None, - current_game_id: None, - }; +impl DownloadManagerInterface { + pub fn queue_game(&self, game_id: String, version: String, target_download_dir: usize) -> Result<(), SendError> { + info!("Adding game id {}", game_id); + self.sender.send(DownloadManagerSignal::Queue(game_id, version, target_download_dir))?; + self.sender.send(DownloadManagerSignal::Go) } - - pub fn queue_game(&mut self, game_id: String, version_name: String) { - let existing_da = self.download_agent_registry.get(&game_id); - - if let Some(da_mutex) = existing_da { - let da = da_mutex.lock().unwrap(); - if da.version == version_name { - return; // We're already queued - } - - da.control_flag.set(DownloadThreadControlFlag::Stop); - + pub fn edit(&self) -> MutexGuard<'_, VecDeque> { + self.download_queue.lock().unwrap() + } + pub fn get_current_game_download_progress(&self) -> Option { + let progress_object = (*self.progress.lock().unwrap()).clone()?; + Some(progress_object.get_progress()) + } + pub fn rearrange_string(&self, id: String, new_index: usize) { + let mut queue = self.edit(); + let current_index = get_index_from_id(&mut queue, id).unwrap(); + let to_move = queue.remove(current_index).unwrap(); + queue.insert(new_index, to_move); + } + pub fn rearrange(&self, current_index: usize, new_index: usize) { + let mut queue = self.edit(); + let to_move = queue.remove(current_index).unwrap(); + queue.insert(new_index, to_move); + } + pub fn remove_from_queue(&self, index: usize) { + self.edit().remove(index); + } + pub fn remove_from_queue_string(&self, game_id: String) { + let mut queue = self.edit(); + let current_index = get_index_from_id(&mut queue, game_id).unwrap(); + queue.remove(current_index); + } + pub fn pause_downloads(&self) -> Result<(), SendError> { + self.sender.send(DownloadManagerSignal::Stop) + } + pub fn resume_downloads(&self) -> Result<(), SendError> { + self.sender.send(DownloadManagerSignal::Go) + } + pub fn ensure_terminated(self) -> Result<(), ()> { + match self.terminator.join() { + Ok(o) => o, + Err(_) => Err(()), } } } + +impl DownloadManager { + pub fn generate() -> DownloadManagerInterface { + let queue = Arc::new(Mutex::new(VecDeque::new())); + let (sender, receiver) = channel(); + let active_progress = Arc::new(Mutex::new(None)); + + let manager = Self { + download_agent_registry: HashMap::new(), + download_queue: queue.clone(), + receiver, + current_game_id: None, + active_control_flag: None, + sender: sender.clone(), + progress: active_progress.clone(), + }; + + let terminator = spawn(|| {manager.manage_queue()}); + + let interface = DownloadManagerInterface { + terminator, + download_queue: queue, + sender, + progress: active_progress + }; + return interface; + } + + fn manage_queue(mut self) -> Result<(), ()> { + loop { + let signal = match self.receiver.recv() { + Ok(signal) => signal, + Err(e) => { + return Err(()) + }, + }; + + match signal { + DownloadManagerSignal::Go => { + info!("Got signal 'Go'"); + if self.active_control_flag.is_none() && !self.download_agent_registry.is_empty() { + info!("Starting download agent"); + let download_agent = { + let lock = self.download_queue.lock().unwrap(); + self.download_agent_registry.get(&lock.front().unwrap().clone()).unwrap().clone() + }; + self.current_game_id = Some(download_agent.id.clone()); + + let progress_object = download_agent.progress.clone(); + *self.progress.lock().unwrap() = Some(progress_object); + + let active_control_flag = download_agent.control_flag.clone(); + self.active_control_flag = Some(active_control_flag.clone()); + + let sender = self.sender.clone(); + + info!("Spawning download"); + spawn(move || { + download_agent.download().unwrap(); + sender.send(DownloadManagerSignal::Completed(download_agent.id.clone())).unwrap(); + }); + info!("Finished spawning Download"); + + active_control_flag.set(DownloadThreadControlFlag::Go); + } + else if let Some(active_control_flag) = self.active_control_flag.clone() { + info!("Restarting current download"); + active_control_flag.set(DownloadThreadControlFlag::Go); + } + else { + info!("Nothing was set"); + } + }, + DownloadManagerSignal::Stop => { + info!("Got signal 'Stop'"); + if let Some(active_control_flag) = self.active_control_flag.clone() { + active_control_flag.set(DownloadThreadControlFlag::Stop); + } + }, + DownloadManagerSignal::Completed(game_id) => { + info!("Got signal 'Completed'"); + if self.current_game_id == Some(game_id.clone()) { + info!("Popping consumed data"); + self.download_queue.lock().unwrap().pop_front(); + self.download_agent_registry.remove(&game_id); + self.active_control_flag = None; + *self.progress.lock().unwrap() = None; + } + self.sender.send(DownloadManagerSignal::Go).unwrap(); + } + DownloadManagerSignal::Queue(game_id, version, target_download_dir) => { + info!("Got signal Queue"); + let download_agent = Arc::new(GameDownloadAgent::new(game_id.clone(), version, target_download_dir)); + self.download_agent_registry.insert(game_id.clone(), download_agent); + self.download_queue.lock().unwrap().push_back(game_id); + }, + }; + } + } +} + +pub fn get_index_from_id(queue: &mut MutexGuard<'_, VecDeque>, id: String) -> Option { + queue.iter().position(|download_agent| { + download_agent == &id + }) +} diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index ea2edfd..e166e3f 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -4,4 +4,4 @@ pub mod download_manager; mod download_logic; mod download_thread_control_flag; mod manifest; -mod progress_object; +mod progress_object; \ No newline at end of file diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs index f3be4fb..6ed2400 100644 --- a/src-tauri/src/downloads/progress_object.rs +++ b/src-tauri/src/downloads/progress_object.rs @@ -1,33 +1,44 @@ use std::sync::{ atomic::{AtomicUsize, Ordering}, - Arc, + Arc, Mutex, }; #[derive(Clone)] pub struct ProgressObject { - max: usize, - progress_instances: Arc>>, + max: Arc>, + progress_instances: Arc>>>, } impl ProgressObject { pub fn new(max: usize, length: usize) -> Self { - let arr = (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect(); + let arr = Mutex::new((0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect()); Self { - max, + max: Arc::new(Mutex::new(max)), progress_instances: Arc::new(arr), } } pub fn sum(&self) -> usize { self.progress_instances + .lock() + .unwrap() .iter() .map(|instance| instance.load(Ordering::Relaxed)) .sum() } + pub fn get_max(&self) -> usize { + self.max.lock().unwrap().clone() + } + pub fn set_max(&self, new_max: usize) { + *self.max.lock().unwrap() = new_max + } + pub fn set_size(&self, length: usize) { + *self.progress_instances.lock().unwrap() = (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect(); + } pub fn get_progress(&self) -> f64 { - self.sum() as f64 / self.max as f64 + self.sum() as f64 / self.get_max() as f64 } pub fn get(&self, index: usize) -> Arc { - self.progress_instances[index].clone() + self.progress_instances.lock().unwrap()[index].clone() } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5e880a2..deb7a4c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,7 +13,7 @@ use crate::downloads::download_agent::GameDownloadAgent; use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; use db::{add_new_download_dir, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; -use downloads::download_manager::DownloadManager; +use downloads::download_manager::{DownloadManager, DownloadManagerInterface}; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; use library::{fetch_game, fetch_library, Game}; @@ -54,7 +54,7 @@ pub struct AppState { games: HashMap, #[serde(skip_serializing)] - download_manager: Arc, + download_manager: Arc, } #[tauri::command] @@ -69,7 +69,7 @@ fn setup() -> AppState { env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); let games = HashMap::new(); - let download_manager = Arc::new(DownloadManager::new()); + let download_manager = Arc::new(DownloadManager::generate()); let is_set_up = DB.database_is_set_up(); if !is_set_up { From 075d6ecf3c46e90c63da49739d51914b07e33277 Mon Sep 17 00:00:00 2001 From: quexeky Date: Wed, 13 Nov 2024 21:05:25 +1100 Subject: [PATCH 099/164] refactor(downloads): Ran cargo clippy & moved DownloadManagerInterface Created file "download_manager_interface.rs" to contain the DownloadManagerInterface Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 8 +- src-tauri/src/downloads/download_commands.rs | 9 +- src-tauri/src/downloads/download_manager.rs | 184 +++++++----------- .../downloads/download_manager_interface.rs | 73 +++++++ src-tauri/src/downloads/mod.rs | 1 + src-tauri/src/downloads/progress_object.rs | 2 +- src-tauri/src/lib.rs | 1 - 7 files changed, 151 insertions(+), 127 deletions(-) create mode 100644 src-tauri/src/downloads/download_manager_interface.rs diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index c977ecd..b582c7a 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -4,13 +4,11 @@ use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::remote::RemoteAccessError; use crate::DB; use log::info; -use rayon::{spawn, ThreadPool, ThreadPoolBuilder}; +use rayon::ThreadPoolBuilder; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::path::Path; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread::Thread; +use std::sync::Mutex; use urlencoding::encode; #[cfg(target_os = "linux")] @@ -137,7 +135,7 @@ impl GameDownloadAgent { .values() .map(|chunk| chunk.lengths.len()) .sum(); - self.progress.set_max(length.try_into().unwrap()); + self.progress.set_max(length); self.progress.set_size(chunk_count); if let Ok(mut manifest) = self.manifest.lock() { diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index c1ab541..74a1bd3 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -1,7 +1,5 @@ -use std::sync::{Arc, Mutex}; +use std::sync::Mutex; -use log::info; -use rayon::spawn; use crate::{AppState}; @@ -45,10 +43,7 @@ pub fn get_game_download_progress( Ok(progress.get_progress()) */ - let progress = match state.lock().unwrap().download_manager.get_current_game_download_progress() { - Some(progress) => progress, - None => 0.0 - }; + let progress = state.lock().unwrap().download_manager.get_current_game_download_progress().unwrap_or(0.0); Ok(progress) } diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index dcd7bf5..7d2e44b 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -4,7 +4,7 @@ use std::{ use log::info; -use super::{download_agent::GameDownloadAgent, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject}; +use super::{download_agent::GameDownloadAgent, download_manager_interface::DownloadManagerInterface, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject}; pub struct DownloadManager { download_agent_registry: HashMap>, @@ -16,12 +16,6 @@ pub struct DownloadManager { current_game_id: Option, // Should be the only game download agent in the map with the "Go" flag active_control_flag: Option } -pub struct DownloadManagerInterface { - terminator: JoinHandle>, - download_queue: Arc>>, - progress: Arc>>, - sender: Sender, -} pub enum DownloadManagerSignal { Go, Stop, @@ -29,51 +23,6 @@ pub enum DownloadManagerSignal { Queue(String, String, usize) } -impl DownloadManagerInterface { - pub fn queue_game(&self, game_id: String, version: String, target_download_dir: usize) -> Result<(), SendError> { - info!("Adding game id {}", game_id); - self.sender.send(DownloadManagerSignal::Queue(game_id, version, target_download_dir))?; - self.sender.send(DownloadManagerSignal::Go) - } - pub fn edit(&self) -> MutexGuard<'_, VecDeque> { - self.download_queue.lock().unwrap() - } - pub fn get_current_game_download_progress(&self) -> Option { - let progress_object = (*self.progress.lock().unwrap()).clone()?; - Some(progress_object.get_progress()) - } - pub fn rearrange_string(&self, id: String, new_index: usize) { - let mut queue = self.edit(); - let current_index = get_index_from_id(&mut queue, id).unwrap(); - let to_move = queue.remove(current_index).unwrap(); - queue.insert(new_index, to_move); - } - pub fn rearrange(&self, current_index: usize, new_index: usize) { - let mut queue = self.edit(); - let to_move = queue.remove(current_index).unwrap(); - queue.insert(new_index, to_move); - } - pub fn remove_from_queue(&self, index: usize) { - self.edit().remove(index); - } - pub fn remove_from_queue_string(&self, game_id: String) { - let mut queue = self.edit(); - let current_index = get_index_from_id(&mut queue, game_id).unwrap(); - queue.remove(current_index); - } - pub fn pause_downloads(&self) -> Result<(), SendError> { - self.sender.send(DownloadManagerSignal::Stop) - } - pub fn resume_downloads(&self) -> Result<(), SendError> { - self.sender.send(DownloadManagerSignal::Go) - } - pub fn ensure_terminated(self) -> Result<(), ()> { - match self.terminator.join() { - Ok(o) => o, - Err(_) => Err(()), - } - } -} impl DownloadManager { pub fn generate() -> DownloadManagerInterface { @@ -93,13 +42,12 @@ impl DownloadManager { let terminator = spawn(|| {manager.manage_queue()}); - let interface = DownloadManagerInterface { + DownloadManagerInterface::new( terminator, - download_queue: queue, + queue, + active_progress, sender, - progress: active_progress - }; - return interface; + ) } fn manage_queue(mut self) -> Result<(), ()> { @@ -113,70 +61,80 @@ impl DownloadManager { match signal { DownloadManagerSignal::Go => { - info!("Got signal 'Go'"); - if self.active_control_flag.is_none() && !self.download_agent_registry.is_empty() { - info!("Starting download agent"); - let download_agent = { - let lock = self.download_queue.lock().unwrap(); - self.download_agent_registry.get(&lock.front().unwrap().clone()).unwrap().clone() - }; - self.current_game_id = Some(download_agent.id.clone()); - - let progress_object = download_agent.progress.clone(); - *self.progress.lock().unwrap() = Some(progress_object); - - let active_control_flag = download_agent.control_flag.clone(); - self.active_control_flag = Some(active_control_flag.clone()); - - let sender = self.sender.clone(); - - info!("Spawning download"); - spawn(move || { - download_agent.download().unwrap(); - sender.send(DownloadManagerSignal::Completed(download_agent.id.clone())).unwrap(); - }); - info!("Finished spawning Download"); - - active_control_flag.set(DownloadThreadControlFlag::Go); - } - else if let Some(active_control_flag) = self.active_control_flag.clone() { - info!("Restarting current download"); - active_control_flag.set(DownloadThreadControlFlag::Go); - } - else { - info!("Nothing was set"); - } + self.manage_go_signal(); }, DownloadManagerSignal::Stop => { - info!("Got signal 'Stop'"); - if let Some(active_control_flag) = self.active_control_flag.clone() { - active_control_flag.set(DownloadThreadControlFlag::Stop); - } + self.manage_stop_signal(); }, DownloadManagerSignal::Completed(game_id) => { - info!("Got signal 'Completed'"); - if self.current_game_id == Some(game_id.clone()) { - info!("Popping consumed data"); - self.download_queue.lock().unwrap().pop_front(); - self.download_agent_registry.remove(&game_id); - self.active_control_flag = None; - *self.progress.lock().unwrap() = None; - } - self.sender.send(DownloadManagerSignal::Go).unwrap(); + self.manage_completed_signal(game_id); } DownloadManagerSignal::Queue(game_id, version, target_download_dir) => { - info!("Got signal Queue"); - let download_agent = Arc::new(GameDownloadAgent::new(game_id.clone(), version, target_download_dir)); - self.download_agent_registry.insert(game_id.clone(), download_agent); - self.download_queue.lock().unwrap().push_back(game_id); + self.manage_queue_signal(game_id, version, target_download_dir); }, }; } } -} -pub fn get_index_from_id(queue: &mut MutexGuard<'_, VecDeque>, id: String) -> Option { - queue.iter().position(|download_agent| { - download_agent == &id - }) -} + fn manage_stop_signal(&mut self) { + info!("Got signal 'Stop'"); + if let Some(active_control_flag) = self.active_control_flag.clone() { + active_control_flag.set(DownloadThreadControlFlag::Stop); + } + } + + fn manage_completed_signal(&mut self, game_id: String) { + info!("Got signal 'Completed'"); + if self.current_game_id == Some(game_id.clone()) { + info!("Popping consumed data"); + self.download_queue.lock().unwrap().pop_front(); + self.download_agent_registry.remove(&game_id); + self.active_control_flag = None; + *self.progress.lock().unwrap() = None; + } + self.sender.send(DownloadManagerSignal::Go).unwrap(); + } + + fn manage_queue_signal(&mut self, game_id: String, version: String, target_download_dir: usize) { + info!("Got signal Queue"); + let download_agent = Arc::new(GameDownloadAgent::new(game_id.clone(), version, target_download_dir)); + self.download_agent_registry.insert(game_id.clone(), download_agent); + self.download_queue.lock().unwrap().push_back(game_id); + } + + fn manage_go_signal(&mut self) { + info!("Got signal 'Go'"); + if self.active_control_flag.is_none() && !self.download_agent_registry.is_empty() { + info!("Starting download agent"); + let download_agent = { + let lock = self.download_queue.lock().unwrap(); + self.download_agent_registry.get(&lock.front().unwrap().clone()).unwrap().clone() + }; + self.current_game_id = Some(download_agent.id.clone()); + + let progress_object = download_agent.progress.clone(); + *self.progress.lock().unwrap() = Some(progress_object); + + let active_control_flag = download_agent.control_flag.clone(); + self.active_control_flag = Some(active_control_flag.clone()); + + let sender = self.sender.clone(); + + info!("Spawning download"); + spawn(move || { + download_agent.download().unwrap(); + sender.send(DownloadManagerSignal::Completed(download_agent.id.clone())).unwrap(); + }); + info!("Finished spawning Download"); + + active_control_flag.set(DownloadThreadControlFlag::Go); + } + else if let Some(active_control_flag) = self.active_control_flag.clone() { + info!("Restarting current download"); + active_control_flag.set(DownloadThreadControlFlag::Go); + } + else { + info!("Nothing was set"); + } + } +} \ No newline at end of file diff --git a/src-tauri/src/downloads/download_manager_interface.rs b/src-tauri/src/downloads/download_manager_interface.rs new file mode 100644 index 0000000..40efdd6 --- /dev/null +++ b/src-tauri/src/downloads/download_manager_interface.rs @@ -0,0 +1,73 @@ +use std::{collections::VecDeque, sync::{mpsc::{SendError, Sender}, Arc, Mutex, MutexGuard}, thread::JoinHandle}; + +use log::info; + +use super::{download_manager::DownloadManagerSignal, progress_object::ProgressObject}; + +pub struct DownloadManagerInterface { + terminator: JoinHandle>, + download_queue: Arc>>, + progress: Arc>>, + sender: Sender, +} + +impl DownloadManagerInterface { + + pub fn new( + terminator: JoinHandle>, + download_queue: Arc>>, + progress: Arc>>, + sender: Sender) -> Self { + Self { terminator, download_queue, progress, sender } + } + + pub fn queue_game(&self, game_id: String, version: String, target_download_dir: usize) -> Result<(), SendError> { + info!("Adding game id {}", game_id); + self.sender.send(DownloadManagerSignal::Queue(game_id, version, target_download_dir))?; + self.sender.send(DownloadManagerSignal::Go) + } + pub fn edit(&self) -> MutexGuard<'_, VecDeque> { + self.download_queue.lock().unwrap() + } + pub fn get_current_game_download_progress(&self) -> Option { + let progress_object = (*self.progress.lock().unwrap()).clone()?; + Some(progress_object.get_progress()) + } + pub fn rearrange_string(&self, id: String, new_index: usize) { + let mut queue = self.edit(); + let current_index = get_index_from_id(&mut queue, id).unwrap(); + let to_move = queue.remove(current_index).unwrap(); + queue.insert(new_index, to_move); + } + pub fn rearrange(&self, current_index: usize, new_index: usize) { + let mut queue = self.edit(); + let to_move = queue.remove(current_index).unwrap(); + queue.insert(new_index, to_move); + } + pub fn remove_from_queue(&self, index: usize) { + self.edit().remove(index); + } + pub fn remove_from_queue_string(&self, game_id: String) { + let mut queue = self.edit(); + let current_index = get_index_from_id(&mut queue, game_id).unwrap(); + queue.remove(current_index); + } + pub fn pause_downloads(&self) -> Result<(), SendError> { + self.sender.send(DownloadManagerSignal::Stop) + } + pub fn resume_downloads(&self) -> Result<(), SendError> { + self.sender.send(DownloadManagerSignal::Go) + } + pub fn ensure_terminated(self) -> Result<(), ()> { + match self.terminator.join() { + Ok(o) => o, + Err(_) => Err(()), + } + } +} + +pub fn get_index_from_id(queue: &mut MutexGuard<'_, VecDeque>, id: String) -> Option { + queue.iter().position(|download_agent| { + download_agent == &id + }) +} diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index e166e3f..cf0f548 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -1,6 +1,7 @@ pub mod download_agent; pub mod download_commands; pub mod download_manager; +mod download_manager_interface; mod download_logic; mod download_thread_control_flag; mod manifest; diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs index 6ed2400..884d126 100644 --- a/src-tauri/src/downloads/progress_object.rs +++ b/src-tauri/src/downloads/progress_object.rs @@ -26,7 +26,7 @@ impl ProgressObject { .sum() } pub fn get_max(&self) -> usize { - self.max.lock().unwrap().clone() + *self.max.lock().unwrap() } pub fn set_max(&self, new_max: usize) { *self.max.lock().unwrap() = new_max diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index deb7a4c..b7e0945 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,7 +9,6 @@ mod settings; mod tests; use crate::db::DatabaseImpls; -use crate::downloads::download_agent::GameDownloadAgent; use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; use db::{add_new_download_dir, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; From b8cf44c0b204ba45097e031257f179214642c9e4 Mon Sep 17 00:00:00 2001 From: quexeky Date: Wed, 13 Nov 2024 21:28:24 +1100 Subject: [PATCH 100/164] refactor(downloads): Ran cargo fmt Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 1 - src-tauri/src/downloads/download_commands.rs | 19 +++- src-tauri/src/downloads/download_manager.rs | 97 ++++++++++++------- .../downloads/download_manager_interface.rs | 52 +++++++--- src-tauri/src/downloads/mod.rs | 6 +- src-tauri/src/downloads/progress_object.rs | 3 +- src-tauri/src/lib.rs | 3 +- 7 files changed, 119 insertions(+), 62 deletions(-) diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index b582c7a..7ef6f47 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -207,7 +207,6 @@ impl GameDownloadAgent { .build() .unwrap(); - pool.scope(move |scope| { let contexts = self.contexts.lock().unwrap(); diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 74a1bd3..0a34e52 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -1,7 +1,6 @@ use std::sync::Mutex; - -use crate::{AppState}; +use crate::AppState; #[tauri::command] pub fn download_game( @@ -27,7 +26,12 @@ pub fn download_game( download_agent_ref.clone().run(); }); */ - state.lock().unwrap().download_manager.queue_game(game_id, game_version, 0).unwrap(); + state + .lock() + .unwrap() + .download_manager + .queue_game(game_id, game_version, 0) + .unwrap(); Ok(()) } @@ -43,7 +47,12 @@ pub fn get_game_download_progress( Ok(progress.get_progress()) */ - let progress = state.lock().unwrap().download_manager.get_current_game_download_progress().unwrap_or(0.0); + let progress = state + .lock() + .unwrap() + .download_manager + .get_current_game_download_progress() + .unwrap_or(0.0); Ok(progress) } @@ -56,4 +65,4 @@ fn use_download_agent( let download_agent = lock.download_manager.get(&game_id).ok_or("Invalid game ID")?; Ok(download_agent.clone()) // Clones the Arc, not the underlying data structure } -*/ \ No newline at end of file +*/ diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 7d2e44b..0eb5d91 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -1,10 +1,20 @@ use std::{ - collections::{HashMap, VecDeque}, sync::{mpsc::{channel, Receiver, SendError, Sender}, Arc, Mutex, MutexGuard}, thread::{spawn, JoinHandle}, + collections::{HashMap, VecDeque}, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, + thread::spawn, }; use log::info; -use super::{download_agent::GameDownloadAgent, download_manager_interface::DownloadManagerInterface, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject}; +use super::{ + download_agent::GameDownloadAgent, + download_manager_interface::DownloadManagerInterface, + download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, + progress_object::ProgressObject, +}; pub struct DownloadManager { download_agent_registry: HashMap>, @@ -14,16 +24,16 @@ pub struct DownloadManager { progress: Arc>>, current_game_id: Option, // Should be the only game download agent in the map with the "Go" flag - active_control_flag: Option + active_control_flag: Option, } pub enum DownloadManagerSignal { Go, Stop, Completed(String), - Queue(String, String, usize) + Queue(String, String, usize), + Finish, } - impl DownloadManager { pub fn generate() -> DownloadManagerInterface { let queue = Arc::new(Mutex::new(VecDeque::new())); @@ -40,38 +50,40 @@ impl DownloadManager { progress: active_progress.clone(), }; - let terminator = spawn(|| {manager.manage_queue()}); + let terminator = spawn(|| manager.manage_queue()); - DownloadManagerInterface::new( - terminator, - queue, - active_progress, - sender, - ) + DownloadManagerInterface::new(terminator, queue, active_progress, sender) } fn manage_queue(mut self) -> Result<(), ()> { loop { let signal = match self.receiver.recv() { Ok(signal) => signal, - Err(e) => { - return Err(()) - }, + Err(e) => return Err(()), }; match signal { DownloadManagerSignal::Go => { self.manage_go_signal(); - }, + } DownloadManagerSignal::Stop => { self.manage_stop_signal(); - }, + } DownloadManagerSignal::Completed(game_id) => { self.manage_completed_signal(game_id); } DownloadManagerSignal::Queue(game_id, version, target_download_dir) => { self.manage_queue_signal(game_id, version, target_download_dir); - }, + } + DownloadManagerSignal::Finish => { + match self.active_control_flag { + Some(active_control_flag) => { + active_control_flag.set(DownloadThreadControlFlag::Stop) + } + None => {} + } + return Ok(()); + } }; } } @@ -82,7 +94,7 @@ impl DownloadManager { active_control_flag.set(DownloadThreadControlFlag::Stop); } } - + fn manage_completed_signal(&mut self, game_id: String) { info!("Got signal 'Completed'"); if self.current_game_id == Some(game_id.clone()) { @@ -94,47 +106,60 @@ impl DownloadManager { } self.sender.send(DownloadManagerSignal::Go).unwrap(); } - - fn manage_queue_signal(&mut self, game_id: String, version: String, target_download_dir: usize) { + + fn manage_queue_signal( + &mut self, + game_id: String, + version: String, + target_download_dir: usize, + ) { info!("Got signal Queue"); - let download_agent = Arc::new(GameDownloadAgent::new(game_id.clone(), version, target_download_dir)); - self.download_agent_registry.insert(game_id.clone(), download_agent); + let download_agent = Arc::new(GameDownloadAgent::new( + game_id.clone(), + version, + target_download_dir, + )); + self.download_agent_registry + .insert(game_id.clone(), download_agent); self.download_queue.lock().unwrap().push_back(game_id); } - + fn manage_go_signal(&mut self) { info!("Got signal 'Go'"); if self.active_control_flag.is_none() && !self.download_agent_registry.is_empty() { info!("Starting download agent"); let download_agent = { let lock = self.download_queue.lock().unwrap(); - self.download_agent_registry.get(&lock.front().unwrap().clone()).unwrap().clone() + self.download_agent_registry + .get(&lock.front().unwrap().clone()) + .unwrap() + .clone() }; self.current_game_id = Some(download_agent.id.clone()); - + let progress_object = download_agent.progress.clone(); *self.progress.lock().unwrap() = Some(progress_object); - + let active_control_flag = download_agent.control_flag.clone(); self.active_control_flag = Some(active_control_flag.clone()); - + let sender = self.sender.clone(); - + info!("Spawning download"); spawn(move || { download_agent.download().unwrap(); - sender.send(DownloadManagerSignal::Completed(download_agent.id.clone())).unwrap(); + sender + .send(DownloadManagerSignal::Completed(download_agent.id.clone())) + .unwrap(); }); info!("Finished spawning Download"); - + active_control_flag.set(DownloadThreadControlFlag::Go); - } - else if let Some(active_control_flag) = self.active_control_flag.clone() { + } else if let Some(active_control_flag) = self.active_control_flag.clone() { info!("Restarting current download"); active_control_flag.set(DownloadThreadControlFlag::Go); - } - else { + } else { info!("Nothing was set"); } } -} \ No newline at end of file +} diff --git a/src-tauri/src/downloads/download_manager_interface.rs b/src-tauri/src/downloads/download_manager_interface.rs index 40efdd6..6b552cc 100644 --- a/src-tauri/src/downloads/download_manager_interface.rs +++ b/src-tauri/src/downloads/download_manager_interface.rs @@ -1,29 +1,50 @@ -use std::{collections::VecDeque, sync::{mpsc::{SendError, Sender}, Arc, Mutex, MutexGuard}, thread::JoinHandle}; +use std::{ + collections::VecDeque, + sync::{ + mpsc::{SendError, Sender}, + Arc, Mutex, MutexGuard, + }, + thread::JoinHandle, +}; use log::info; use super::{download_manager::DownloadManagerSignal, progress_object::ProgressObject}; pub struct DownloadManagerInterface { - terminator: JoinHandle>, + terminator: JoinHandle>, download_queue: Arc>>, progress: Arc>>, sender: Sender, } impl DownloadManagerInterface { - pub fn new( - terminator: JoinHandle>, - download_queue: Arc>>, - progress: Arc>>, - sender: Sender) -> Self { - Self { terminator, download_queue, progress, sender } + terminator: JoinHandle>, + download_queue: Arc>>, + progress: Arc>>, + sender: Sender, + ) -> Self { + Self { + terminator, + download_queue, + progress, + sender, + } } - - pub fn queue_game(&self, game_id: String, version: String, target_download_dir: usize) -> Result<(), SendError> { + + pub fn queue_game( + &self, + game_id: String, + version: String, + target_download_dir: usize, + ) -> Result<(), SendError> { info!("Adding game id {}", game_id); - self.sender.send(DownloadManagerSignal::Queue(game_id, version, target_download_dir))?; + self.sender.send(DownloadManagerSignal::Queue( + game_id, + version, + target_download_dir, + ))?; self.sender.send(DownloadManagerSignal::Go) } pub fn edit(&self) -> MutexGuard<'_, VecDeque> { @@ -59,6 +80,7 @@ impl DownloadManagerInterface { self.sender.send(DownloadManagerSignal::Go) } pub fn ensure_terminated(self) -> Result<(), ()> { + self.sender.send(DownloadManagerSignal::Finish).unwrap(); match self.terminator.join() { Ok(o) => o, Err(_) => Err(()), @@ -66,8 +88,8 @@ impl DownloadManagerInterface { } } -pub fn get_index_from_id(queue: &mut MutexGuard<'_, VecDeque>, id: String) -> Option { - queue.iter().position(|download_agent| { - download_agent == &id - }) +fn get_index_from_id(queue: &mut MutexGuard<'_, VecDeque>, id: String) -> Option { + queue + .iter() + .position(|download_agent| download_agent == &id) } diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index cf0f548..08472ea 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -1,8 +1,8 @@ pub mod download_agent; pub mod download_commands; -pub mod download_manager; -mod download_manager_interface; mod download_logic; +pub mod download_manager; +pub mod download_manager_interface; mod download_thread_control_flag; mod manifest; -mod progress_object; \ No newline at end of file +mod progress_object; diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs index 884d126..fb8a4dd 100644 --- a/src-tauri/src/downloads/progress_object.rs +++ b/src-tauri/src/downloads/progress_object.rs @@ -32,7 +32,8 @@ impl ProgressObject { *self.max.lock().unwrap() = new_max } pub fn set_size(&self, length: usize) { - *self.progress_instances.lock().unwrap() = (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect(); + *self.progress_instances.lock().unwrap() = + (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect(); } pub fn get_progress(&self) -> f64 { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b7e0945..6574c7c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,7 +12,8 @@ use crate::db::DatabaseImpls; use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; use db::{add_new_download_dir, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; -use downloads::download_manager::{DownloadManager, DownloadManagerInterface}; +use downloads::download_manager::DownloadManager; +use downloads::download_manager_interface::DownloadManagerInterface; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; use library::{fetch_game, fetch_library, Game}; From 27e5a8e31c6808704365bb806a1bb8a054a703cd Mon Sep 17 00:00:00 2001 From: quexeky Date: Wed, 13 Nov 2024 21:55:28 +1100 Subject: [PATCH 101/164] style(downloads): Fixing some references to "id" vs "game_id" Signed-off-by: quexeky --- src-tauri/src/downloads/download_manager.rs | 20 ++++---- .../downloads/download_manager_interface.rs | 51 +++++++++++-------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 0eb5d91..007839f 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -19,7 +19,7 @@ use super::{ pub struct DownloadManager { download_agent_registry: HashMap>, download_queue: Arc>>, - receiver: Receiver, + command_receiver: Receiver, sender: Sender, progress: Arc>>, @@ -37,27 +37,27 @@ pub enum DownloadManagerSignal { impl DownloadManager { pub fn generate() -> DownloadManagerInterface { let queue = Arc::new(Mutex::new(VecDeque::new())); - let (sender, receiver) = channel(); + let (command_sender, command_receiver) = channel(); let active_progress = Arc::new(Mutex::new(None)); let manager = Self { download_agent_registry: HashMap::new(), download_queue: queue.clone(), - receiver, + command_receiver, current_game_id: None, active_control_flag: None, - sender: sender.clone(), + sender: command_sender.clone(), progress: active_progress.clone(), }; let terminator = spawn(|| manager.manage_queue()); - DownloadManagerInterface::new(terminator, queue, active_progress, sender) + DownloadManagerInterface::new(terminator, queue, active_progress, command_sender) } fn manage_queue(mut self) -> Result<(), ()> { loop { - let signal = match self.receiver.recv() { + let signal = match self.command_receiver.recv() { Ok(signal) => signal, Err(e) => return Err(()), }; @@ -109,19 +109,19 @@ impl DownloadManager { fn manage_queue_signal( &mut self, - game_id: String, + id: String, version: String, target_download_dir: usize, ) { info!("Got signal Queue"); let download_agent = Arc::new(GameDownloadAgent::new( - game_id.clone(), + id.clone(), version, target_download_dir, )); self.download_agent_registry - .insert(game_id.clone(), download_agent); - self.download_queue.lock().unwrap().push_back(game_id); + .insert(id.clone(), download_agent); + self.download_queue.lock().unwrap().push_back(id); } fn manage_go_signal(&mut self) { diff --git a/src-tauri/src/downloads/download_manager_interface.rs b/src-tauri/src/downloads/download_manager_interface.rs index 6b552cc..71e1779 100644 --- a/src-tauri/src/downloads/download_manager_interface.rs +++ b/src-tauri/src/downloads/download_manager_interface.rs @@ -1,21 +1,29 @@ use std::{ - collections::VecDeque, - sync::{ + any::Any, collections::VecDeque, sync::{ mpsc::{SendError, Sender}, Arc, Mutex, MutexGuard, - }, - thread::JoinHandle, + }, thread::JoinHandle }; use log::info; use super::{download_manager::DownloadManagerSignal, progress_object::ProgressObject}; +/// Accessible front-end for the DownloadManager +/// +/// The system works entirely through signals, both internally and externally, +/// all of which are accessible through the DownloadManagerSignal type, but +/// should not be used directly. Rather, signals are abstracted through this +/// interface. +/// +/// The actual download queue may be accessed through the .edit() function, +/// which provides raw access to the underlying queue. +/// THIS EDITING IS BLOCKING!!! pub struct DownloadManagerInterface { terminator: JoinHandle>, download_queue: Arc>>, progress: Arc>>, - sender: Sender, + command_sender: Sender, } impl DownloadManagerInterface { @@ -23,29 +31,29 @@ impl DownloadManagerInterface { terminator: JoinHandle>, download_queue: Arc>>, progress: Arc>>, - sender: Sender, + command_sender: Sender, ) -> Self { Self { terminator, download_queue, progress, - sender, + command_sender, } } pub fn queue_game( &self, - game_id: String, + id: String, version: String, target_download_dir: usize, ) -> Result<(), SendError> { - info!("Adding game id {}", game_id); - self.sender.send(DownloadManagerSignal::Queue( - game_id, + info!("Adding game id {}", id); + self.command_sender.send(DownloadManagerSignal::Queue( + id, version, target_download_dir, ))?; - self.sender.send(DownloadManagerSignal::Go) + self.command_sender.send(DownloadManagerSignal::Go) } pub fn edit(&self) -> MutexGuard<'_, VecDeque> { self.download_queue.lock().unwrap() @@ -68,26 +76,25 @@ impl DownloadManagerInterface { pub fn remove_from_queue(&self, index: usize) { self.edit().remove(index); } - pub fn remove_from_queue_string(&self, game_id: String) { + pub fn remove_from_queue_string(&self, id: String) { let mut queue = self.edit(); - let current_index = get_index_from_id(&mut queue, game_id).unwrap(); + let current_index = get_index_from_id(&mut queue, id).unwrap(); queue.remove(current_index); } pub fn pause_downloads(&self) -> Result<(), SendError> { - self.sender.send(DownloadManagerSignal::Stop) + self.command_sender.send(DownloadManagerSignal::Stop) } pub fn resume_downloads(&self) -> Result<(), SendError> { - self.sender.send(DownloadManagerSignal::Go) + self.command_sender.send(DownloadManagerSignal::Go) } - pub fn ensure_terminated(self) -> Result<(), ()> { - self.sender.send(DownloadManagerSignal::Finish).unwrap(); - match self.terminator.join() { - Ok(o) => o, - Err(_) => Err(()), - } + pub fn ensure_terminated(self) -> Result, Box> { + self.command_sender.send(DownloadManagerSignal::Finish).unwrap(); + self.terminator.join() } } +/// Takes in the locked value from .edit() and attempts to +/// get the index of whatever game_id is passed in fn get_index_from_id(queue: &mut MutexGuard<'_, VecDeque>, id: String) -> Option { queue .iter() From f029cbf0b3a6c8c4acece7f148eab64f0e2cb88d Mon Sep 17 00:00:00 2001 From: quexeky Date: Wed, 13 Nov 2024 22:17:30 +1100 Subject: [PATCH 102/164] docs(download manager): Added description on how the DownloadManager works Signed-off-by: quexeky --- src-tauri/src/downloads/download_manager.rs | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 007839f..378d989 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -16,6 +16,43 @@ use super::{ progress_object::ProgressObject, }; +/* + +Welcome to the download manager, the most overengineered, glorious piece of bullshit. + +The download manager takes a queue of game_ids and their associated +GameDownloadAgents, and then, one-by-one, executes them. It provides an interface +to interact with the currently downloading agent, and manage the queue. + +When the DownloadManager is initialised, it is designed to provide a reference +which can be used to provide some instructions (the DownloadManagerInterface), +but other than that, it runs without any sort of interruptions. + +It does this by opening up two data structures. Primarily is the command_receiver, +and mpsc (multi-channel-single-producer) which allows commands to be sent from +the Interface, and queued up for the Manager to process. + +These have been mapped in the DownloadManagerSignal docs. + +The other way to interact with the DownloadManager is via the donwload_queue, +which is just a collection of ids which may be rearranged to suit +whichever download queue order is required. + ++----------------------------------------------------------------------------+ +| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! | +| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE | +| WHICH HAS NOT BEEN ACCOUNTED FOR | ++----------------------------------------------------------------------------+ + +This download queue does not actually own any of the GameDownloadAgents. It is +simply a id-based reference system. The actual Agents are stored in the +download_agent_registry HashMap, as ordering is no issue here. This is why +appending or removing from the download_queue must be done via signals. + +Behold, my madness - quexeky + +*/ + pub struct DownloadManager { download_agent_registry: HashMap>, download_queue: Arc>>, @@ -27,10 +64,18 @@ pub struct DownloadManager { active_control_flag: Option, } pub enum DownloadManagerSignal { + /// Resumes (or starts) the DownloadManager Go, + /// Pauses the DownloadManager Stop, + /// Called when a GameDownloadAgent has finished. + /// Triggers the next download cycle to begin Completed(String), + /// Generates and appends a GameDownloadAgent + /// to the registry and queue Queue(String, String, usize), + /// Tells the Manager to stop the current + /// download and return Finish, } From 63c3cc109601e9f0b374efae01f4bbb2e1636dcb Mon Sep 17 00:00:00 2001 From: quexeky Date: Sat, 16 Nov 2024 17:03:37 +1100 Subject: [PATCH 103/164] feat(downloads): Added AgentInterfaceData to get information about all downloads in queue Signed-off-by: quexeky --- pages/store/index.vue | 2 +- src-tauri/src/downloads/download_agent.rs | 7 +- src-tauri/src/downloads/download_commands.rs | 10 +-- src-tauri/src/downloads/download_logic.rs | 2 +- src-tauri/src/downloads/download_manager.rs | 84 ++++++++++++++----- .../downloads/download_manager_interface.rs | 27 ++++-- src-tauri/src/lib.rs | 2 +- src-tauri/src/remote.rs | 8 +- 8 files changed, 98 insertions(+), 44 deletions(-) diff --git a/pages/store/index.vue b/pages/store/index.vue index ee5f7b2..18124ed 100644 --- a/pages/store/index.vue +++ b/pages/store/index.vue @@ -26,7 +26,7 @@ async function startGameDownload() { setInterval(() => { (async () => { const currentProgress = await invoke( - "get_game_download_progress", + "get_current_game_download_progress", { gameId: gameId.value, } diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 7ef6f47..789d0fa 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -28,7 +28,7 @@ pub struct GameDownloadAgent { pub progress: ProgressObject, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum GameDownloadError { CommunicationError(RemoteAccessError), ChecksumError, @@ -50,11 +50,11 @@ impl Display for GameDownloadError { impl GameDownloadAgent { pub fn new(id: String, version: String, target_download_dir: usize) -> Self { // Don't run by default - let status = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); + let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); Self { id, version, - control_flag: status.clone(), + control_flag, manifest: Mutex::new(None), target_download_dir, contexts: Mutex::new(Vec::new()), @@ -207,6 +207,7 @@ impl GameDownloadAgent { .build() .unwrap(); + pool.scope(move |scope| { let contexts = self.contexts.lock().unwrap(); diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 0a34e52..df48f4e 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -36,17 +36,9 @@ pub fn download_game( } #[tauri::command] -pub fn get_game_download_progress( +pub fn get_current_game_download_progress( state: tauri::State<'_, Mutex>, - game_id: String, ) -> Result { - /* - let download_agent = use_download_agent(state, game_id)?; - - let progress = &download_agent.progress; - - Ok(progress.get_progress()) - */ let progress = state .lock() .unwrap() diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index dc95f2f..9478b1e 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -148,7 +148,7 @@ pub fn download_game_chunk( .get(chunk_url) .header("Authorization", header) .send() - .map_err(|e| GameDownloadError::CommunicationError(RemoteAccessError::FetchError(e)))?; + .map_err(|e| GameDownloadError::CommunicationError(e.into()))?; let mut destination = DropWriter::new(ctx.path); diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 378d989..330b983 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -10,8 +10,8 @@ use std::{ use log::info; use super::{ - download_agent::GameDownloadAgent, - download_manager_interface::DownloadManagerInterface, + download_agent::{GameDownloadAgent, GameDownloadError}, + download_manager_interface::{AgentInterfaceData, DownloadManagerInterface}, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, }; @@ -55,12 +55,13 @@ Behold, my madness - quexeky pub struct DownloadManager { download_agent_registry: HashMap>, - download_queue: Arc>>, + download_queue: Arc>>>, command_receiver: Receiver, sender: Sender, progress: Arc>>, + status: Arc>, - current_game_id: Option, // Should be the only game download agent in the map with the "Go" flag + current_game_interface: Option>, // Should be the only game download agent in the map with the "Go" flag active_control_flag: Option, } pub enum DownloadManagerSignal { @@ -77,6 +78,21 @@ pub enum DownloadManagerSignal { /// Tells the Manager to stop the current /// download and return Finish, + /// Any error which occurs in the agent + Error(GameDownloadError) +} +pub enum DownloadManagerStatus { + Downloading, + Paused, + Empty, + Error(GameDownloadError), +} +#[derive(Clone)] +pub enum GameDownloadStatus { + Downloading, + Paused, + Uninitialised, + Error(GameDownloadError), } impl DownloadManager { @@ -84,13 +100,15 @@ impl DownloadManager { let queue = Arc::new(Mutex::new(VecDeque::new())); let (command_sender, command_receiver) = channel(); let active_progress = Arc::new(Mutex::new(None)); + let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty)); let manager = Self { download_agent_registry: HashMap::new(), download_queue: queue.clone(), command_receiver, - current_game_id: None, + current_game_interface: None, active_control_flag: None, + status: status.clone(), sender: command_sender.clone(), progress: active_progress.clone(), }; @@ -110,6 +128,7 @@ impl DownloadManager { match signal { DownloadManagerSignal::Go => { self.manage_go_signal(); + } DownloadManagerSignal::Stop => { self.manage_stop_signal(); @@ -129,6 +148,9 @@ impl DownloadManager { } return Ok(()); } + DownloadManagerSignal::Error(game_download_error) => { + self.manage_error_signal(game_download_error); + }, }; } } @@ -142,12 +164,15 @@ impl DownloadManager { fn manage_completed_signal(&mut self, game_id: String) { info!("Got signal 'Completed'"); - if self.current_game_id == Some(game_id.clone()) { - info!("Popping consumed data"); - self.download_queue.lock().unwrap().pop_front(); - self.download_agent_registry.remove(&game_id); - self.active_control_flag = None; - *self.progress.lock().unwrap() = None; + if let Some(interface) = &self.current_game_interface { + // When if let chains are stabilised, combine these two statements + if interface.id == game_id { + info!("Popping consumed data"); + self.download_queue.lock().unwrap().pop_front(); + self.download_agent_registry.remove(&game_id); + self.active_control_flag = None; + *self.progress.lock().unwrap() = None; + } } self.sender.send(DownloadManagerSignal::Go).unwrap(); } @@ -164,9 +189,14 @@ impl DownloadManager { version, target_download_dir, )); + let agent_status = GameDownloadStatus::Uninitialised; + let interface_data = Arc::new(AgentInterfaceData { + id, + status: Mutex::new(agent_status), + }); self.download_agent_registry - .insert(id.clone(), download_agent); - self.download_queue.lock().unwrap().push_back(id); + .insert(interface_data.id.clone(), download_agent); + self.download_queue.lock().unwrap().push_back(interface_data); } fn manage_go_signal(&mut self) { @@ -176,11 +206,12 @@ impl DownloadManager { let download_agent = { let lock = self.download_queue.lock().unwrap(); self.download_agent_registry - .get(&lock.front().unwrap().clone()) + .get(&lock.front().unwrap().id) .unwrap() .clone() }; - self.current_game_id = Some(download_agent.id.clone()); + let download_agent_interface = Arc::new(AgentInterfaceData::from(download_agent.clone())); + self.current_game_interface = Some(download_agent_interface); let progress_object = download_agent.progress.clone(); *self.progress.lock().unwrap() = Some(progress_object); @@ -192,14 +223,20 @@ impl DownloadManager { info!("Spawning download"); spawn(move || { - download_agent.download().unwrap(); - sender - .send(DownloadManagerSignal::Completed(download_agent.id.clone())) - .unwrap(); + let signal = match download_agent.download() { + Ok(_) => { + DownloadManagerSignal::Completed(download_agent.id.clone()) + }, + Err(e) => { + DownloadManagerSignal::Error(e) + }, + }; + sender.send(signal).unwrap(); }); info!("Finished spawning Download"); active_control_flag.set(DownloadThreadControlFlag::Go); + self.set_status(DownloadManagerStatus::Downloading); } else if let Some(active_control_flag) = self.active_control_flag.clone() { info!("Restarting current download"); active_control_flag.set(DownloadThreadControlFlag::Go); @@ -207,4 +244,13 @@ impl DownloadManager { info!("Nothing was set"); } } + fn manage_error_signal(&self, error: GameDownloadError) { + let current_status = self.current_game_interface.clone().unwrap(); + let mut lock = current_status.status.lock().unwrap(); + *lock = GameDownloadStatus::Error(error.clone()); + self.set_status(DownloadManagerStatus::Error(error)); + } + fn set_status(&self, status: DownloadManagerStatus) { + *self.status.lock().unwrap() = status; + } } diff --git a/src-tauri/src/downloads/download_manager_interface.rs b/src-tauri/src/downloads/download_manager_interface.rs index 71e1779..d68c92b 100644 --- a/src-tauri/src/downloads/download_manager_interface.rs +++ b/src-tauri/src/downloads/download_manager_interface.rs @@ -7,7 +7,7 @@ use std::{ use log::info; -use super::{download_manager::DownloadManagerSignal, progress_object::ProgressObject}; +use super::{download_agent::GameDownloadAgent, download_manager::{DownloadManagerSignal, DownloadManagerStatus, GameDownloadStatus}, progress_object::ProgressObject}; /// Accessible front-end for the DownloadManager /// @@ -21,15 +21,27 @@ use super::{download_manager::DownloadManagerSignal, progress_object::ProgressOb /// THIS EDITING IS BLOCKING!!! pub struct DownloadManagerInterface { terminator: JoinHandle>, - download_queue: Arc>>, + download_queue: Arc>>>, progress: Arc>>, command_sender: Sender, } +pub struct AgentInterfaceData { + pub id: String, + pub status: Mutex, +} +impl From> for AgentInterfaceData { + fn from(value: Arc) -> Self { + Self { + id: value.id.clone(), + status: Mutex::from(GameDownloadStatus::Uninitialised) + } + } +} impl DownloadManagerInterface { pub fn new( terminator: JoinHandle>, - download_queue: Arc>>, + download_queue: Arc>>>, progress: Arc>>, command_sender: Sender, ) -> Self { @@ -55,9 +67,12 @@ impl DownloadManagerInterface { ))?; self.command_sender.send(DownloadManagerSignal::Go) } - pub fn edit(&self) -> MutexGuard<'_, VecDeque> { + pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { self.download_queue.lock().unwrap() } + pub fn read_queue(&self) -> VecDeque> { + self.download_queue.lock().unwrap().clone() + } pub fn get_current_game_download_progress(&self) -> Option { let progress_object = (*self.progress.lock().unwrap()).clone()?; Some(progress_object.get_progress()) @@ -95,8 +110,8 @@ impl DownloadManagerInterface { /// Takes in the locked value from .edit() and attempts to /// get the index of whatever game_id is passed in -fn get_index_from_id(queue: &mut MutexGuard<'_, VecDeque>, id: String) -> Option { +fn get_index_from_id(queue: &mut MutexGuard<'_, VecDeque>>, id: String) -> Option { queue .iter() - .position(|download_agent| download_agent == &id) + .position(|download_agent| download_agent.id == id) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6574c7c..8982d9d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -124,7 +124,7 @@ pub fn run() { add_new_download_dir, // Downloads download_game, - get_game_download_progress, + get_current_game_download_progress, ]) .plugin(tauri_plugin_shell::init()) .setup(|app| { diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs index e415c52..303a3db 100644 --- a/src-tauri/src/remote.rs +++ b/src-tauri/src/remote.rs @@ -1,6 +1,6 @@ use std::{ fmt::{Display, Formatter}, - sync::Mutex, + sync::{Arc, Mutex}, }; use log::{info, warn}; @@ -9,9 +9,9 @@ use url::{ParseError, Url}; use crate::{AppState, AppStatus, DB}; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum RemoteAccessError { - FetchError(reqwest::Error), + FetchError(Arc), ParsingError(ParseError), InvalidCodeError(u16), GenericErrror(String), @@ -32,7 +32,7 @@ impl Display for RemoteAccessError { impl From for RemoteAccessError { fn from(err: reqwest::Error) -> Self { - RemoteAccessError::FetchError(err) + RemoteAccessError::FetchError(Arc::new(err)) } } impl From for RemoteAccessError { From bd3deacf38cb725e684c46f8d4238b66b2e5fc32 Mon Sep 17 00:00:00 2001 From: quexeky Date: Sat, 16 Nov 2024 17:05:24 +1100 Subject: [PATCH 104/164] chore(downloads): Ran cargo clippy & cargo fmt Side note, I'm going to start using chore to declare these rather than refactor because I don't think that it actually qualifies Signed-off-by: quexeky --- src-tauri/src/downloads/download_agent.rs | 1 - src-tauri/src/downloads/download_manager.rs | 39 +++++++------------ .../downloads/download_manager_interface.rs | 34 ++++++++++------ 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 789d0fa..14a61e0 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -207,7 +207,6 @@ impl GameDownloadAgent { .build() .unwrap(); - pool.scope(move |scope| { let contexts = self.contexts.lock().unwrap(); diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 330b983..4877995 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -20,7 +20,7 @@ use super::{ Welcome to the download manager, the most overengineered, glorious piece of bullshit. -The download manager takes a queue of game_ids and their associated +The download manager takes a queue of game_ids and their associated GameDownloadAgents, and then, one-by-one, executes them. It provides an interface to interact with the currently downloading agent, and manage the queue. @@ -79,7 +79,7 @@ pub enum DownloadManagerSignal { /// download and return Finish, /// Any error which occurs in the agent - Error(GameDownloadError) + Error(GameDownloadError), } pub enum DownloadManagerStatus { Downloading, @@ -128,7 +128,6 @@ impl DownloadManager { match signal { DownloadManagerSignal::Go => { self.manage_go_signal(); - } DownloadManagerSignal::Stop => { self.manage_stop_signal(); @@ -140,17 +139,14 @@ impl DownloadManager { self.manage_queue_signal(game_id, version, target_download_dir); } DownloadManagerSignal::Finish => { - match self.active_control_flag { - Some(active_control_flag) => { - active_control_flag.set(DownloadThreadControlFlag::Stop) - } - None => {} + if let Some(active_control_flag) = self.active_control_flag { + active_control_flag.set(DownloadThreadControlFlag::Stop) } return Ok(()); } DownloadManagerSignal::Error(game_download_error) => { self.manage_error_signal(game_download_error); - }, + } }; } } @@ -171,18 +167,13 @@ impl DownloadManager { self.download_queue.lock().unwrap().pop_front(); self.download_agent_registry.remove(&game_id); self.active_control_flag = None; - *self.progress.lock().unwrap() = None; + *self.progress.lock().unwrap() = None; } } self.sender.send(DownloadManagerSignal::Go).unwrap(); } - fn manage_queue_signal( - &mut self, - id: String, - version: String, - target_download_dir: usize, - ) { + fn manage_queue_signal(&mut self, id: String, version: String, target_download_dir: usize) { info!("Got signal Queue"); let download_agent = Arc::new(GameDownloadAgent::new( id.clone(), @@ -196,7 +187,10 @@ impl DownloadManager { }); self.download_agent_registry .insert(interface_data.id.clone(), download_agent); - self.download_queue.lock().unwrap().push_back(interface_data); + self.download_queue + .lock() + .unwrap() + .push_back(interface_data); } fn manage_go_signal(&mut self) { @@ -210,7 +204,8 @@ impl DownloadManager { .unwrap() .clone() }; - let download_agent_interface = Arc::new(AgentInterfaceData::from(download_agent.clone())); + let download_agent_interface = + Arc::new(AgentInterfaceData::from(download_agent.clone())); self.current_game_interface = Some(download_agent_interface); let progress_object = download_agent.progress.clone(); @@ -224,12 +219,8 @@ impl DownloadManager { info!("Spawning download"); spawn(move || { let signal = match download_agent.download() { - Ok(_) => { - DownloadManagerSignal::Completed(download_agent.id.clone()) - }, - Err(e) => { - DownloadManagerSignal::Error(e) - }, + Ok(_) => DownloadManagerSignal::Completed(download_agent.id.clone()), + Err(e) => DownloadManagerSignal::Error(e), }; sender.send(signal).unwrap(); }); diff --git a/src-tauri/src/downloads/download_manager_interface.rs b/src-tauri/src/downloads/download_manager_interface.rs index d68c92b..b5d5619 100644 --- a/src-tauri/src/downloads/download_manager_interface.rs +++ b/src-tauri/src/downloads/download_manager_interface.rs @@ -1,23 +1,30 @@ use std::{ - any::Any, collections::VecDeque, sync::{ + any::Any, + collections::VecDeque, + sync::{ mpsc::{SendError, Sender}, Arc, Mutex, MutexGuard, - }, thread::JoinHandle + }, + thread::JoinHandle, }; use log::info; -use super::{download_agent::GameDownloadAgent, download_manager::{DownloadManagerSignal, DownloadManagerStatus, GameDownloadStatus}, progress_object::ProgressObject}; +use super::{ + download_agent::GameDownloadAgent, + download_manager::{DownloadManagerSignal, GameDownloadStatus}, + progress_object::ProgressObject, +}; /// Accessible front-end for the DownloadManager -/// +/// /// The system works entirely through signals, both internally and externally, -/// all of which are accessible through the DownloadManagerSignal type, but +/// all of which are accessible through the DownloadManagerSignal type, but /// should not be used directly. Rather, signals are abstracted through this /// interface. -/// +/// /// The actual download queue may be accessed through the .edit() function, -/// which provides raw access to the underlying queue. +/// which provides raw access to the underlying queue. /// THIS EDITING IS BLOCKING!!! pub struct DownloadManagerInterface { terminator: JoinHandle>, @@ -33,7 +40,7 @@ impl From> for AgentInterfaceData { fn from(value: Arc) -> Self { Self { id: value.id.clone(), - status: Mutex::from(GameDownloadStatus::Uninitialised) + status: Mutex::from(GameDownloadStatus::Uninitialised), } } } @@ -102,15 +109,20 @@ impl DownloadManagerInterface { pub fn resume_downloads(&self) -> Result<(), SendError> { self.command_sender.send(DownloadManagerSignal::Go) } - pub fn ensure_terminated(self) -> Result, Box> { - self.command_sender.send(DownloadManagerSignal::Finish).unwrap(); + pub fn ensure_terminated(self) -> Result, Box> { + self.command_sender + .send(DownloadManagerSignal::Finish) + .unwrap(); self.terminator.join() } } /// Takes in the locked value from .edit() and attempts to /// get the index of whatever game_id is passed in -fn get_index_from_id(queue: &mut MutexGuard<'_, VecDeque>>, id: String) -> Option { +fn get_index_from_id( + queue: &mut MutexGuard<'_, VecDeque>>, + id: String, +) -> Option { queue .iter() .position(|download_agent| download_agent.id == id) From ec2f4148e812f4897a570013d00a6bc1ef6f98d6 Mon Sep 17 00:00:00 2001 From: quexeky Date: Mon, 18 Nov 2024 13:21:20 +1100 Subject: [PATCH 105/164] style(downloads): Made all errors type-based Signed-off-by: quexeky --- src-tauri/src/auth.rs | 8 +--- src-tauri/src/downloads/download_agent.rs | 38 +++++++++---------- src-tauri/src/downloads/download_commands.rs | 38 ++++++------------- src-tauri/src/downloads/download_logic.rs | 10 ++--- src-tauri/src/downloads/download_manager.rs | 17 ++++----- .../downloads/download_manager_interface.rs | 4 +- src-tauri/src/lib.rs | 19 +++++++--- src-tauri/src/library.rs | 24 +++++------- src-tauri/src/remote.rs | 31 ++++++++++----- 9 files changed, 89 insertions(+), 100 deletions(-) diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs index 7619e8a..78da5b2 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/auth.rs @@ -93,9 +93,7 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc let path_chunks: Vec<&str> = path.split("/").collect(); if path_chunks.len() != 3 { app.emit("auth/failed", ()).unwrap(); - return Err(RemoteAccessError::GenericErrror( - "Invalid number of handshake chunks".to_string(), - )); + return Err(RemoteAccessError::InvalidResponse); } let base_url = { @@ -165,9 +163,7 @@ async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> { let response = client.post(endpoint.to_string()).json(&body).send().await?; if response.status() != 200 { - return Err("Failed to create redirect URL. Please try again later." - .to_string() - .into()); + return Err(RemoteAccessError::InvalidRedirect); } let redir_url = response.text().await?; diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 14a61e0..0b5e610 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -28,21 +28,26 @@ pub struct GameDownloadAgent { pub progress: ProgressObject, } -#[derive(Debug, Clone)] +#[derive(Debug)] pub enum GameDownloadError { - CommunicationError(RemoteAccessError), - ChecksumError, - SetupError(String), - LockError, + Communication(RemoteAccessError), + Checksum, + Setup(SetupError), + Lock, +} + +#[derive(Debug)] +pub enum SetupError { + Context } impl Display for GameDownloadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - GameDownloadError::CommunicationError(error) => write!(f, "{}", error), - GameDownloadError::SetupError(error) => write!(f, "{}", error), - GameDownloadError::LockError => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"), - GameDownloadError::ChecksumError => write!(f, "Checksum failed to validate for download"), + GameDownloadError::Communication(error) => write!(f, "{}", error), + GameDownloadError::Setup(error) => write!(f, "{:?}", error), + GameDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"), + GameDownloadError::Checksum => write!(f, "Checksum failed to validate for download"), } } } @@ -114,13 +119,8 @@ impl GameDownloadAgent { .unwrap(); if response.status() != 200 { - return Err(GameDownloadError::CommunicationError( - format!( - "Failed to download game manifest: {} {}", - response.status(), - response.text().unwrap() - ) - .into(), + return Err(GameDownloadError::Communication( + RemoteAccessError::ManifestDownloadFailed(response.status(), response.text().unwrap()) )); } @@ -143,7 +143,7 @@ impl GameDownloadAgent { return Ok(()); } - Err(GameDownloadError::LockError) + Err(GameDownloadError::Lock) } pub fn generate_contexts(&self) -> Result<(), GameDownloadError> { @@ -194,9 +194,7 @@ impl GameDownloadAgent { return Ok(()); } - Err(GameDownloadError::SetupError( - "Failed to generate download contexts".to_owned(), - )) + Err(GameDownloadError::Setup(SetupError::Context)) } pub fn run(&self) { diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index df48f4e..d7d9155 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -1,52 +1,38 @@ use std::sync::Mutex; -use crate::AppState; +use serde::Serialize; + +use crate::{AppError, AppState}; #[tauri::command] pub fn download_game( game_id: String, game_version: String, state: tauri::State<'_, Mutex>, -) -> Result<(), String> { - /* - info!("beginning game download..."); - - let mut download_agent = GameDownloadAgent::new(game_id.clone(), game_version.clone(), 0); - // Setup download requires mutable - download_agent.setup_download().unwrap(); - - let mut lock: std::sync::MutexGuard<'_, AppState> = state.lock().unwrap(); - let download_agent_ref = Arc::new(download_agent); - lock.download_manager - .insert(game_id, download_agent_ref.clone()); - - // Run it in another thread - spawn(move || { - // Run doesn't require mutable - download_agent_ref.clone().run(); - }); - */ +) -> Result<(), AppError> { + state .lock() .unwrap() .download_manager .queue_game(game_id, game_version, 0) - .unwrap(); - Ok(()) + .map_err(|_| AppError::Signal) } #[tauri::command] pub fn get_current_game_download_progress( state: tauri::State<'_, Mutex>, -) -> Result { - let progress = state +) -> Result { + match state .lock() .unwrap() .download_manager .get_current_game_download_progress() - .unwrap_or(0.0); + { + Some(progress) => Ok(progress), + None => Err(AppError::DoesNotExist), + } - Ok(progress) } /* fn use_download_agent( diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 9478b1e..f094c96 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -148,7 +148,7 @@ pub fn download_game_chunk( .get(chunk_url) .header("Authorization", header) .send() - .map_err(|e| GameDownloadError::CommunicationError(e.into()))?; + .map_err(|e| GameDownloadError::Communication(e.into()))?; let mut destination = DropWriter::new(ctx.path); @@ -160,11 +160,7 @@ pub fn download_game_chunk( let content_length = response.content_length(); if content_length.is_none() { - return Err(GameDownloadError::CommunicationError( - RemoteAccessError::GenericErrror( - "Invalid download endpoint, missing Content-Length header.".to_owned(), - ), - )); + return Err(GameDownloadError::Communication(RemoteAccessError::InvalidResponse)); } let mut pipeline = DropDownloadPipeline::new( @@ -184,7 +180,7 @@ pub fn download_game_chunk( let res = hex::encode(checksum.0); if res != ctx.checksum { - return Err(GameDownloadError::ChecksumError); + return Err(GameDownloadError::Checksum); } Ok(true) diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 4877995..885ed44 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -11,7 +11,7 @@ use log::info; use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, - download_manager_interface::{AgentInterfaceData, DownloadManagerInterface}, + download_manager_interface::{AgentInterfaceData, DownloadManager}, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, }; @@ -53,7 +53,7 @@ Behold, my madness - quexeky */ -pub struct DownloadManager { +pub struct DownloadManagerBuilder { download_agent_registry: HashMap>, download_queue: Arc>>>, command_receiver: Receiver, @@ -85,9 +85,8 @@ pub enum DownloadManagerStatus { Downloading, Paused, Empty, - Error(GameDownloadError), + Error, } -#[derive(Clone)] pub enum GameDownloadStatus { Downloading, Paused, @@ -95,8 +94,8 @@ pub enum GameDownloadStatus { Error(GameDownloadError), } -impl DownloadManager { - pub fn generate() -> DownloadManagerInterface { +impl DownloadManagerBuilder { + pub fn build() -> DownloadManager { let queue = Arc::new(Mutex::new(VecDeque::new())); let (command_sender, command_receiver) = channel(); let active_progress = Arc::new(Mutex::new(None)); @@ -115,7 +114,7 @@ impl DownloadManager { let terminator = spawn(|| manager.manage_queue()); - DownloadManagerInterface::new(terminator, queue, active_progress, command_sender) + DownloadManager::new(terminator, queue, active_progress, command_sender) } fn manage_queue(mut self) -> Result<(), ()> { @@ -238,8 +237,8 @@ impl DownloadManager { fn manage_error_signal(&self, error: GameDownloadError) { let current_status = self.current_game_interface.clone().unwrap(); let mut lock = current_status.status.lock().unwrap(); - *lock = GameDownloadStatus::Error(error.clone()); - self.set_status(DownloadManagerStatus::Error(error)); + *lock = GameDownloadStatus::Error(error); + self.set_status(DownloadManagerStatus::Error); } fn set_status(&self, status: DownloadManagerStatus) { *self.status.lock().unwrap() = status; diff --git a/src-tauri/src/downloads/download_manager_interface.rs b/src-tauri/src/downloads/download_manager_interface.rs index b5d5619..00fd204 100644 --- a/src-tauri/src/downloads/download_manager_interface.rs +++ b/src-tauri/src/downloads/download_manager_interface.rs @@ -26,7 +26,7 @@ use super::{ /// The actual download queue may be accessed through the .edit() function, /// which provides raw access to the underlying queue. /// THIS EDITING IS BLOCKING!!! -pub struct DownloadManagerInterface { +pub struct DownloadManager { terminator: JoinHandle>, download_queue: Arc>>>, progress: Arc>>, @@ -45,7 +45,7 @@ impl From> for AgentInterfaceData { } } -impl DownloadManagerInterface { +impl DownloadManager { pub fn new( terminator: JoinHandle>, download_queue: Arc>>>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8982d9d..5f9bd1f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,13 +12,13 @@ use crate::db::DatabaseImpls; use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; use db::{add_new_download_dir, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; -use downloads::download_manager::DownloadManager; -use downloads::download_manager_interface::DownloadManagerInterface; +use downloads::download_manager::DownloadManagerBuilder; +use downloads::download_manager_interface::DownloadManager; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; use library::{fetch_game, fetch_library, Game}; use log::info; -use remote::{gen_drop_url, use_remote}; +use remote::{gen_drop_url, use_remote, RemoteAccessError}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use std::{ @@ -36,6 +36,13 @@ pub enum AppStatus { SignedInNeedsReauth, ServerUnavailable, } +#[derive(Debug, Serialize)] +pub enum AppError { + DoesNotExist, + Signal, + RemoteAccess(String) +} + #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct User { @@ -54,11 +61,11 @@ pub struct AppState { games: HashMap, #[serde(skip_serializing)] - download_manager: Arc, + download_manager: Arc, } #[tauri::command] -fn fetch_state(state: tauri::State<'_, Mutex>) -> Result { +fn fetch_state(state: tauri::State<'_, Mutex>) -> Result { let guard = state.lock().unwrap(); let cloned_state = guard.clone(); drop(guard); @@ -69,7 +76,7 @@ fn setup() -> AppState { env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); let games = HashMap::new(); - let download_manager = Arc::new(DownloadManager::generate()); + let download_manager = Arc::new(DownloadManagerBuilder::build()); let is_set_up = DB.database_is_set_up(); if !is_set_up { diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 65f9ec1..1127749 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -7,6 +7,7 @@ use tauri::{AppHandle, Manager}; use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; use crate::remote::RemoteAccessError; +use crate::AppError; use crate::{auth::generate_authorization_header, AppState, DB}; #[derive(serde::Serialize)] @@ -18,7 +19,7 @@ struct FetchGameStruct { #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Game { - id: String, + game_id: String, m_name: String, m_short_description: String, m_description: String, @@ -54,12 +55,12 @@ fn fetch_library_logic(app: AppHandle) -> Result { let mut db_handle = DB.borrow_data_mut().unwrap(); for game in games.iter() { - handle.games.insert(game.id.clone(), game.clone()); - if !db_handle.games.games_statuses.contains_key(&game.id) { + handle.games.insert(game.game_id.clone(), game.clone()); + if !db_handle.games.games_statuses.contains_key(&game.game_id) { db_handle .games .games_statuses - .insert(game.id.clone(), DatabaseGameStatus::Remote); + .insert(game.game_id.clone(), DatabaseGameStatus::Remote); } } @@ -69,14 +70,9 @@ fn fetch_library_logic(app: AppHandle) -> Result { } #[tauri::command] -pub fn fetch_library(app: AppHandle) -> Result { - let result = fetch_library_logic(app); - - if result.is_err() { - return Err(result.err().unwrap().to_string()); - } - - Ok(result.unwrap()) +pub fn fetch_library(app: AppHandle) -> Result { + fetch_library_logic(app) + .map_err(|e| AppError::RemoteAccess(e.to_string())) } fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result { @@ -92,7 +88,7 @@ fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result Result), ParsingError(ParseError), InvalidCodeError(u16), - GenericErrror(String), + InvalidEndpoint, + HandshakeFailed, + GameNotFound, + InvalidResponse, + InvalidRedirect, + ManifestDownloadFailed(StatusCode, String) } impl Display for RemoteAccessError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { RemoteAccessError::FetchError(error) => write!(f, "{}", error), - RemoteAccessError::GenericErrror(error) => write!(f, "{}", error), RemoteAccessError::ParsingError(parse_error) => { write!(f, "{}", parse_error) } RemoteAccessError::InvalidCodeError(error) => write!(f, "HTTP {}", error), + RemoteAccessError::ParsingError(parse_error) => todo!(), + RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"), + RemoteAccessError::HandshakeFailed => write!(f, "Failed to complete handshake"), + RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"), + RemoteAccessError::InvalidResponse => write!(f, "Server returned an invalid response"), + RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"), + RemoteAccessError::ManifestDownloadFailed(status, response) => + write!(f, "Failed to download game manifest: {} {}", + status, + response + ), } } } @@ -35,11 +51,6 @@ impl From for RemoteAccessError { RemoteAccessError::FetchError(Arc::new(err)) } } -impl From for RemoteAccessError { - fn from(err: String) -> Self { - RemoteAccessError::GenericErrror(err) - } -} impl From for RemoteAccessError { fn from(err: ParseError) -> Self { RemoteAccessError::ParsingError(err) @@ -74,7 +85,7 @@ async fn use_remote_logic<'a>( if result.app_name != "Drop" { warn!("user entered drop endpoint that connected, but wasn't identified as Drop"); - return Err("Not a valid Drop endpoint".to_string().into()); + return Err(RemoteAccessError::InvalidEndpoint); } let mut app_state = state.lock().unwrap(); From f0c47d87fb83cbd1cba91d6b1f9d07f7b700b647 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 18 Nov 2024 20:13:10 +1100 Subject: [PATCH 106/164] fix(readme): update readme instructions --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f4b8306..b0df333 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Drop app is the companion app for [Drop](https://github.com/Drop-OSS/drop). It u Install dependencies with `yarn` -Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use the environment variable in `.env` +Run the app in development with `yarn tauri dev`. NVIDIA users on Linux, use shell script `./nvidia-prop-dev.sh` To manually specify the logging level, add the environment variable `RUST_LOG=[debug, info, warn, error]` to `yarn tauri dev`: From 7c8089ef644fe59099fee5c2a4ca54efbf78990f Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 19 Nov 2024 11:49:15 +1100 Subject: [PATCH 107/164] fix(openssl): use vendored flag --- src-tauri/Cargo.lock | 10 ++++++++++ src-tauri/Cargo.toml | 3 +++ 2 files changed, 13 insertions(+) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0b03042..a3aa006 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2586,6 +2586,15 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-src" +version = "300.4.1+3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa4eac4138c62414b5622d1b31c5c304f34b406b013c079c2bbc652fdd6678c" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.103" @@ -2594,6 +2603,7 @@ checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8e6cff1..e9a88e7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -59,6 +59,9 @@ features = [ [dependencies.openssl] version = "0.10.66" +features = [ + "vendored" +] [dependencies.rustbreak] version = "2" From 2d4a7e8f9cc84edeccc52e8d4712c5a3b3c010ba Mon Sep 17 00:00:00 2001 From: quexeky Date: Tue, 19 Nov 2024 13:23:42 +1100 Subject: [PATCH 108/164] feat: added file-based logging Signed-off-by: quexeky --- src-tauri/Cargo.lock | 116 +++++++++++++++++++++++++++++++++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 23 ++++++++- 3 files changed, 138 insertions(+), 2 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a3aa006..21d78ec 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -111,6 +111,12 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "ashpd" version = "0.9.2" @@ -885,6 +891,12 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "destructure_traitobject" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c877555693c14d2f84191cfd3ad8582790fc52b5e2274b40b59cf5f5cea25c7" + [[package]] name = "digest" version = "0.10.7" @@ -996,6 +1008,7 @@ dependencies = [ "hex", "http", "log", + "log4rs", "md5", "openssl", "rayon", @@ -2185,6 +2198,43 @@ name = "log" version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "serde", +] + +[[package]] +name = "log-mdc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94d21414c1f4a51209ad204c1776a3d0765002c76c6abcb602a6f09f1e881c7" + +[[package]] +name = "log4rs" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0816135ae15bd0391cf284eab37e6e3ee0a6ee63d2ceeb659862bd8d0a984ca6" +dependencies = [ + "anyhow", + "arc-swap", + "chrono", + "derivative", + "fnv", + "humantime", + "libc", + "log", + "log-mdc", + "once_cell", + "parking_lot", + "rand 0.8.5", + "serde", + "serde-value", + "serde_json", + "serde_yaml", + "thiserror", + "thread-id", + "typemap-ors", + "winapi", +] [[package]] name = "mac" @@ -2614,6 +2664,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -3493,6 +3552,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.210" @@ -3589,6 +3658,19 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.6.0", + "itoa 1.0.11", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serialize-to-javascript" version = "0.1.1" @@ -4308,6 +4390,16 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "thread-id" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "time" version = "0.3.36" @@ -4561,6 +4653,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e13db2e0ccd5e14a544e8a246ba2312cd25223f616442d7f2cb0e3db614236e" +[[package]] +name = "typemap-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68c24b707f02dd18f1e4ccceb9d49f2058c2fb86384ef9972592904d7a28867" +dependencies = [ + "unsafe-any-ors", +] + [[package]] name = "typenum" version = "1.17.0" @@ -4646,6 +4747,21 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unsafe-any-ors" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a303d30665362d9680d7d91d78b23f5f899504d4f08b3c4cf08d055d87c0ad" +dependencies = [ + "destructure_traitobject", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e9a88e7..bec7732 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -42,6 +42,7 @@ http = "1.1.0" tokio = { version = "1.40.0", features = ["rt", "tokio-macros", "signal"] } urlencoding = "2.1.3" md5 = "0.7.0" +log4rs = "1.3.0" [dependencies.rustix] version = "0.38.37" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bef2fab..437faf0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,9 +16,14 @@ use downloads::download_commands::*; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; use library::{fetch_game, fetch_library, Game}; -use log::info; +use log::{info, LevelFilter}; +use log4rs::append::file::FileAppender; +use log4rs::config::{Appender, Root}; +use log4rs::encode::pattern::PatternEncoder; +use log4rs::Config; use remote::{gen_drop_url, use_remote}; use serde::{Deserialize, Serialize}; +use std::borrow::Borrow; use std::sync::Arc; use std::{ collections::HashMap, @@ -65,7 +70,21 @@ fn fetch_state(state: tauri::State<'_, Mutex>) -> Result AppState { - env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); + + let logfile = FileAppender::builder() + .encoder(Box::new(PatternEncoder::new("{l} - {m}\n"))) + .build( DATA_ROOT_DIR.lock().unwrap().join("./drop.log")).unwrap(); + + let config = Config::builder() + .appender(Appender::builder().build("logfile", Box::new(logfile))) + .build(Root::builder() + .appender("logfile") + .build(LevelFilter::Info)).unwrap(); + + log4rs::init_config(config).unwrap(); + + //env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); + let is_set_up = DB.database_is_set_up(); if !is_set_up { From 469a2d69ebef73742eb123a87cffc60bf75249e5 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 19 Nov 2024 15:05:28 +1100 Subject: [PATCH 109/164] feat(auth): refactoring and error message --- app.vue | 39 +++++------------------------- composables/state-navigation.ts | 43 +++++++++++++++++++++++++++++++++ pages/auth/failed.vue | 8 ++++-- src-tauri/src/auth.rs | 5 ++-- 4 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 composables/state-navigation.ts diff --git a/app.vue b/app.vue index 4218fe2..8028eba 100644 --- a/app.vue +++ b/app.vue @@ -6,11 +6,12 @@ diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 81b1955..cf5192b 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -3,7 +3,7 @@ use crate::db::DatabaseImpls; use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::remote::RemoteAccessError; use crate::DB; -use log::info; +use log::{debug, error, info}; use rayon::ThreadPoolBuilder; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; @@ -239,7 +239,7 @@ impl GameDownloadAgent { } }, Err(e) => { - info!("GameDownloadError: {}", e); + error!("GameDownloadError: {}", e); self.sender.send(DownloadManagerSignal::Error(e)).unwrap(); new_contexts_ref.lock().unwrap().push(context); }, @@ -247,11 +247,10 @@ impl GameDownloadAgent { }); } }); - info!("Acquiring lock"); if !new_contexts.lock().unwrap().is_empty() { - info!("New contexts not empty"); + debug!("New contexts not empty"); *self.contexts.lock().unwrap() = Arc::into_inner(new_contexts).unwrap().into_inner().unwrap(); - info!("Contexts: {:?}", *self.contexts.lock().unwrap()); + debug!("Contexts: {:?}", *self.contexts.lock().unwrap()); return Err(()) } info!("Contexts: {:?}", *self.contexts.lock().unwrap()); diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index f98a9fc..8883836 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -1,5 +1,7 @@ use std::sync::Mutex; +use log::info; + use crate::{AppError, AppState}; #[tauri::command] @@ -37,6 +39,7 @@ pub fn stop_game_download( state: tauri::State<'_, Mutex>, game_id: String ) { + info!("Cancelling game download {}", game_id); state .lock() .unwrap() diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 0b674e3..a48034f 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -7,7 +7,7 @@ use std::{ thread::spawn, }; -use log::info; +use log::{info, warn}; use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, @@ -78,6 +78,7 @@ pub enum DownloadManagerSignal { /// Tells the Manager to stop the current /// download and return Finish, + Cancel(String), /// Any error which occurs in the agent Error(GameDownloadError), } @@ -146,6 +147,9 @@ impl DownloadManagerBuilder { DownloadManagerSignal::Error(e) => { self.manage_error_signal(e); }, + DownloadManagerSignal::Cancel(id) => { + self.manage_cancel_signal(id); + }, }; } } @@ -220,10 +224,11 @@ impl DownloadManagerBuilder { spawn(move || { match download_agent.download() { Ok(_) => { - sender.send(DownloadManagerSignal::Completed(download_agent.id.clone())); + sender.send(DownloadManagerSignal::Completed(download_agent.id.clone())).unwrap(); }, - Err(_) => { - todo!() // Account for if the setup_download function fails + Err(e) => { + warn!("Download failed"); + //todo!() // Account for if the setup_download function fails }, }; }); @@ -244,6 +249,22 @@ impl DownloadManagerBuilder { *lock = GameDownloadStatus::Error; self.set_status(DownloadManagerStatus::Error); } + fn manage_cancel_signal(&mut self, game_id: String) { + if let Some(current_flag) = &self.active_control_flag { + current_flag.set(DownloadThreadControlFlag::Stop); + self.active_control_flag = None; + *self.progress.lock().unwrap() = None; + } + self.download_agent_registry.remove(&game_id); + let mut lock = self.download_queue.lock().unwrap(); + let index = match lock.iter().position(|interface| interface.id == game_id) { + Some(index) => index, + None => return, + }; + lock.remove(index); + self.sender.send(DownloadManagerSignal::Go).unwrap(); + info!("{:?}", self.download_agent_registry.iter().map(|x| x.0.clone()).collect::()); + } fn set_status(&self, status: DownloadManagerStatus) { *self.status.lock().unwrap() = status; } diff --git a/src-tauri/src/downloads/download_manager_interface.rs b/src-tauri/src/downloads/download_manager_interface.rs index d626267..790f6cd 100644 --- a/src-tauri/src/downloads/download_manager_interface.rs +++ b/src-tauri/src/downloads/download_manager_interface.rs @@ -76,9 +76,9 @@ impl DownloadManager { } pub fn cancel_download( &self, - id: String + game_id: String ) { - todo!() + self.command_sender.send(DownloadManagerSignal::Cancel(game_id)).unwrap(); } pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { self.download_queue.lock().unwrap() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5f9bd1f..bd05f63 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -132,6 +132,7 @@ pub fn run() { // Downloads download_game, get_current_game_download_progress, + stop_game_download ]) .plugin(tauri_plugin_shell::init()) .setup(|app| { From 76b0975bcc27895e3d43228a9e4a671d132b7c12 Mon Sep 17 00:00:00 2001 From: quexeky Date: Sat, 23 Nov 2024 18:18:03 +1100 Subject: [PATCH 118/164] style(downloads): Abstracted queue system TODO: Still need to cleanup the rest of the legacy code which used to use the queue system Signed-off-by: quexeky --- src-tauri/src/downloads/download_logic.rs | 3 +- src-tauri/src/downloads/download_manager.rs | 23 +++---- .../downloads/download_manager_interface.rs | 10 +-- src-tauri/src/downloads/mod.rs | 1 + src-tauri/src/downloads/queue.rs | 64 +++++++++++++++++++ 5 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 src-tauri/src/downloads/queue.rs diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 6c99ec3..942423a 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -8,7 +8,7 @@ use md5::{Context, Digest}; use reqwest::blocking::Response; use std::io::Read; -use std::sync::atomic::AtomicUsize; +use std::sync::atomic::{AtomicUsize, Ordering}; use std::{ fs::{File, OpenOptions}, io::{self, BufWriter, ErrorKind, Seek, SeekFrom, Write}, @@ -125,6 +125,7 @@ pub fn download_game_chunk( // If we're paused if control_flag.get() == DownloadThreadControlFlag::Stop { info!("Control flag is Stop"); + progress.store(0, Ordering::Relaxed); return Ok(false); } diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index a48034f..d71d801 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -13,7 +13,7 @@ use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, download_manager_interface::{AgentInterfaceData, DownloadManager}, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, - progress_object::ProgressObject, + progress_object::ProgressObject, queue::Queue, }; /* @@ -55,7 +55,7 @@ Behold, my madness - quexeky pub struct DownloadManagerBuilder { download_agent_registry: HashMap>, - download_queue: Arc>>>, + download_queue: Queue, command_receiver: Receiver, sender: Sender, progress: Arc>>, @@ -97,7 +97,7 @@ pub enum GameDownloadStatus { impl DownloadManagerBuilder { pub fn build() -> DownloadManager { - let queue = Arc::new(Mutex::new(VecDeque::new())); + let queue = Queue::new(); let (command_sender, command_receiver) = channel(); let active_progress = Arc::new(Mutex::new(None)); let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty)); @@ -167,7 +167,7 @@ impl DownloadManagerBuilder { // When if let chains are stabilised, combine these two statements if interface.id == game_id { info!("Popping consumed data"); - self.download_queue.lock().unwrap().pop_front(); + self.download_queue.pop_front(); self.download_agent_registry.remove(&game_id); self.active_control_flag = None; *self.progress.lock().unwrap() = None; @@ -185,16 +185,13 @@ impl DownloadManagerBuilder { self.sender.clone() )); let agent_status = GameDownloadStatus::Uninitialised; - let interface_data = Arc::new(AgentInterfaceData { + let interface_data = AgentInterfaceData { id, status: Mutex::new(agent_status), - }); + }; self.download_agent_registry .insert(interface_data.id.clone(), download_agent); - self.download_queue - .lock() - .unwrap() - .push_back(interface_data); + self.download_queue.append(interface_data); } fn manage_go_signal(&mut self) { @@ -202,9 +199,9 @@ impl DownloadManagerBuilder { if self.active_control_flag.is_none() && !self.download_agent_registry.is_empty() { info!("Starting download agent"); let download_agent = { - let lock = self.download_queue.lock().unwrap(); + let front = self.download_queue.read().front().unwrap().clone(); self.download_agent_registry - .get(&lock.front().unwrap().id) + .get(&front.id) .unwrap() .clone() }; @@ -256,7 +253,7 @@ impl DownloadManagerBuilder { *self.progress.lock().unwrap() = None; } self.download_agent_registry.remove(&game_id); - let mut lock = self.download_queue.lock().unwrap(); + let mut lock = self.download_queue.edit(); let index = match lock.iter().position(|interface| interface.id == game_id) { Some(index) => index, None => return, diff --git a/src-tauri/src/downloads/download_manager_interface.rs b/src-tauri/src/downloads/download_manager_interface.rs index 790f6cd..0ecf2bd 100644 --- a/src-tauri/src/downloads/download_manager_interface.rs +++ b/src-tauri/src/downloads/download_manager_interface.rs @@ -13,7 +13,7 @@ use log::info; use super::{ download_agent::GameDownloadAgent, download_manager::{DownloadManagerSignal, GameDownloadStatus}, - progress_object::ProgressObject, + progress_object::ProgressObject, queue::Queue, }; /// Accessible front-end for the DownloadManager @@ -28,7 +28,7 @@ use super::{ /// THIS EDITING IS BLOCKING!!! pub struct DownloadManager { terminator: JoinHandle>, - download_queue: Arc>>>, + download_queue: Queue, progress: Arc>>, command_sender: Sender, } @@ -48,7 +48,7 @@ impl From> for AgentInterfaceData { impl DownloadManager { pub fn new( terminator: JoinHandle>, - download_queue: Arc>>>, + download_queue: Queue, progress: Arc>>, command_sender: Sender, ) -> Self { @@ -81,10 +81,10 @@ impl DownloadManager { self.command_sender.send(DownloadManagerSignal::Cancel(game_id)).unwrap(); } pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { - self.download_queue.lock().unwrap() + self.download_queue.edit() } pub fn read_queue(&self) -> VecDeque> { - self.download_queue.lock().unwrap().clone() + self.download_queue.read() } pub fn get_current_game_download_progress(&self) -> Option { let progress_object = (*self.progress.lock().unwrap()).clone()?; diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index 08472ea..3556f81 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -6,3 +6,4 @@ pub mod download_manager_interface; mod download_thread_control_flag; mod manifest; mod progress_object; +pub mod queue; \ No newline at end of file diff --git a/src-tauri/src/downloads/queue.rs b/src-tauri/src/downloads/queue.rs new file mode 100644 index 0000000..de1be17 --- /dev/null +++ b/src-tauri/src/downloads/queue.rs @@ -0,0 +1,64 @@ +use std::{collections::VecDeque, sync::{Arc, Mutex, MutexGuard}}; + +use super::download_manager_interface::AgentInterfaceData; + +#[derive(Clone)] +pub struct Queue { + inner: Arc>>> +} + +impl Queue { + pub fn new() -> Self { + Self { + inner: Arc::new(Mutex::new(VecDeque::new())) + } + } + pub fn read(&self) -> VecDeque> { + self.inner.lock().unwrap().clone() + } + pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { + self.inner.lock().unwrap() + } + pub fn pop_front(&self) -> Option> { + self.edit().pop_front() + } + /// Either inserts `interface` at the specified index, or appends to + /// the back of the deque if index is greater than the length of the deque + pub fn insert(&self, interface: AgentInterfaceData, index: usize) { + if self.read().len() > index { + self.append(interface); + } + else { + self.edit().insert(index, Arc::new(interface)); + } + } + pub fn append(&self, interface: AgentInterfaceData) { + self.edit().push_back(Arc::new(interface)); + } + pub fn pop_front_if_equal(&self, game_id: String) -> Option> { + let mut queue = self.edit(); + let front = match queue.front() { + Some(front) => front, + None => return None, + }; + if front.id == game_id { + return queue.pop_front(); + } + return None + } + pub fn get_by_id(&self, game_id: String) -> Option { + self.read().iter().position(|data| data.id == game_id) + } + pub fn move_to_index_by_id(&self, game_id: String, new_index: usize) -> Result<(), ()> { + let index = match self.get_by_id(game_id) { + Some(index) => index, + None => return Err(()), + }; + let existing = match self.edit().remove(index) { + Some(existing) => existing, + None => return Err(()), + }; + self.edit().insert(new_index, existing); + Ok(()) + } +} \ No newline at end of file From b065e101e6c6480ddb7ae7243cfd53fd90c2f015 Mon Sep 17 00:00:00 2001 From: Louis van Liefland <116044207+quexeky@users.noreply.github.com> Date: Sat, 23 Nov 2024 23:32:56 +1100 Subject: [PATCH 119/164] chore(downloads): Progress on write speeds & added debug statements --- src-tauri/src/db.rs | 13 +++++++-- src-tauri/src/downloads/download_agent.rs | 30 ++++++++++++-------- src-tauri/src/downloads/download_commands.rs | 8 ++++++ src-tauri/src/downloads/manifest.rs | 1 + src-tauri/src/downloads/progress_object.rs | 9 ++++-- src-tauri/src/lib.rs | 6 +++- src-tauri/src/p2p/registration.rs | 17 +++++++++++ 7 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 src-tauri/src/p2p/registration.rs diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index f5e5ea4..4daf984 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -6,7 +6,9 @@ use std::{ }; use directories::BaseDirs; +use log::debug; use rustbreak::{deser::Bincode, PathDatabase}; +use rustix::path::Arg; use serde::{Deserialize, Serialize}; use url::Url; @@ -61,7 +63,9 @@ impl DatabaseImpls for DatabaseInterface { let db_path = data_root_dir.join("drop.db"); let games_base_dir = data_root_dir.join("games"); + debug!("Creating data directory at {:?}", data_root_dir); create_dir_all(data_root_dir.clone()).unwrap(); + debug!("Creating games directory"); create_dir_all(games_base_dir.clone()).unwrap(); let default = Database { @@ -72,10 +76,15 @@ impl DatabaseImpls for DatabaseInterface { games_statuses: HashMap::new(), }, }; + #[allow(clippy::let_and_return)] - let db = match fs::exists(db_path.clone()).unwrap() { + let exists = fs::exists(db_path.clone()).unwrap(); + let db = match exists { true => PathDatabase::load_from_path(db_path).expect("Database loading failed"), - false => PathDatabase::create_at_path(db_path, default).unwrap(), + false => { + debug!("Creating database at path {}", db_path.as_str().unwrap()); + PathDatabase::create_at_path(db_path, default).expect("Database could not be created") + }, }; db diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index cf5192b..0bb3b71 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -92,6 +92,7 @@ impl GameDownloadAgent { // Blocking pub fn download(&self) -> Result<(), GameDownloadError> { self.setup_download()?; + self.set_progress_object_params(); self.run().map_err(|_| GameDownloadError::DownloadError)?; Ok(()) @@ -133,18 +134,6 @@ impl GameDownloadAgent { } let manifest_download = response.json::().unwrap(); - let length = manifest_download - .values() - .map(|chunk| { - return chunk.lengths.iter().sum::(); - }) - .sum::(); - let chunk_count = manifest_download - .values() - .map(|chunk| chunk.lengths.len()) - .sum(); - self.progress.set_max(length); - self.progress.set_size(chunk_count); if let Ok(mut manifest) = self.manifest.lock() { *manifest = Some(manifest_download); @@ -154,6 +143,22 @@ impl GameDownloadAgent { Err(GameDownloadError::Lock) } + fn set_progress_object_params(&self) { + let lock = self.contexts.lock().unwrap(); + let length = lock.len(); + + let chunk_count = lock.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(); + } + pub fn generate_contexts(&self) -> Result<(), GameDownloadError> { let db_lock = DB.borrow_data().unwrap(); let data_base_dir = db_lock.games.install_dirs[self.target_download_dir].clone(); @@ -187,6 +192,7 @@ impl GameDownloadAgent { game_id: game_id.to_string(), path: path.clone(), checksum: chunk.checksums[i].clone(), + length: *length }); running_offset += *length as u64; } diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 8883836..5b429df 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -46,6 +46,14 @@ pub fn stop_game_download( .download_manager .cancel_download(game_id); } +#[tauri::command] +pub fn get_current_write_speed( + state: tauri::State<'_, Mutex>, +) { + +} + + /* fn use_download_agent( state: tauri::State<'_, Mutex>, diff --git a/src-tauri/src/downloads/manifest.rs b/src-tauri/src/downloads/manifest.rs index 2bb1552..2c28ea5 100644 --- a/src-tauri/src/downloads/manifest.rs +++ b/src-tauri/src/downloads/manifest.rs @@ -20,4 +20,5 @@ pub struct DropDownloadContext { pub game_id: String, pub path: PathBuf, pub checksum: String, + pub length: usize } diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs index fb8a4dd..0848caf 100644 --- a/src-tauri/src/downloads/progress_object.rs +++ b/src-tauri/src/downloads/progress_object.rs @@ -1,12 +1,13 @@ -use std::sync::{ +use std::{sync::{ atomic::{AtomicUsize, Ordering}, Arc, Mutex, -}; +}, time::Instant}; #[derive(Clone)] pub struct ProgressObject { max: Arc>, progress_instances: Arc>>>, + start: Arc> } impl ProgressObject { @@ -15,8 +16,12 @@ impl ProgressObject { Self { max: Arc::new(Mutex::new(max)), progress_instances: Arc::new(arr), + start: Arc::new(Mutex::new(Instant::now())), } } + pub fn set_time_now(&self) { + *self.start.lock().unwrap() = Instant::now(); + } pub fn sum(&self) -> usize { self.progress_instances .lock() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index bd05f63..beb1aeb 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,7 +17,7 @@ use downloads::download_manager_interface::DownloadManager; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; use library::{fetch_game, fetch_library, Game}; -use log::info; +use log::{debug, info}; use remote::{gen_drop_url, use_remote, RemoteAccessError}; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -73,11 +73,13 @@ fn fetch_state(state: tauri::State<'_, Mutex>) -> Result AppState { + debug!("Starting env"); env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); let games = HashMap::new(); let download_manager = Arc::new(DownloadManagerBuilder::build()); + debug!("Checking if database is set up"); let is_set_up = DB.database_is_set_up(); if !is_set_up { return AppState { @@ -88,6 +90,8 @@ fn setup() -> AppState { }; } + debug!("Database is set up"); + let (app_status, user) = auth::setup().unwrap(); AppState { status: app_status, diff --git a/src-tauri/src/p2p/registration.rs b/src-tauri/src/p2p/registration.rs new file mode 100644 index 0000000..0926515 --- /dev/null +++ b/src-tauri/src/p2p/registration.rs @@ -0,0 +1,17 @@ +use crate::{auth::generate_authorization_header, db::DatabaseImpls, remote::RemoteAccessError, DB}; + + +pub async fn register() -> Result { + let base_url = DB.fetch_base_url(); + let registration_url = base_url.join("/api/v1/client/capability").unwrap(); + let header = generate_authorization_header(); + + + let client = reqwest::blocking::Client::new(); + client + .post(registration_url) + .header("Authorization", header) + .send()?; + + return Ok(String::new()) +} \ No newline at end of file From 7a3841bf0c676fb54dc7d629ff821641fcf2c1f6 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sun, 24 Nov 2024 09:01:11 +1100 Subject: [PATCH 120/164] fix(db): initialise doesn't recreate default install dir --- src-tauri/src/auth.rs | 1 - src-tauri/src/db.rs | 10 ++++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs index 24bbb37..82e8da1 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/auth.rs @@ -190,7 +190,6 @@ pub async fn auth_initiate<'a>() -> Result<(), String> { pub fn setup() -> Result<(AppStatus, Option), ()> { let data = DB.borrow_data().unwrap(); - // If we have certs, exit for now if data.auth.is_some() { let user_result = fetch_user(); if user_result.is_err() { diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index f5e5ea4..ea7f922 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -61,9 +61,6 @@ impl DatabaseImpls for DatabaseInterface { let db_path = data_root_dir.join("drop.db"); let games_base_dir = data_root_dir.join("games"); - create_dir_all(data_root_dir.clone()).unwrap(); - create_dir_all(games_base_dir.clone()).unwrap(); - let default = Database { auth: None, base_url: "".to_string(), @@ -75,7 +72,12 @@ impl DatabaseImpls for DatabaseInterface { #[allow(clippy::let_and_return)] let db = match fs::exists(db_path.clone()).unwrap() { true => PathDatabase::load_from_path(db_path).expect("Database loading failed"), - false => PathDatabase::create_at_path(db_path, default).unwrap(), + false => { + create_dir_all(data_root_dir.clone()).unwrap(); + create_dir_all(games_base_dir.clone()).unwrap(); + + PathDatabase::create_at_path(db_path, default).unwrap() + } }; db From 384f7a5be9ca044cda39dfc97b5e807a43cf0c9f Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sun, 24 Nov 2024 21:04:56 +1100 Subject: [PATCH 121/164] feat(settings): ability to add more download dirs --- components/HeaderUserWidget.vue | 2 +- composables/current-page-engine.ts | 2 +- package.json | 2 +- pages/library/[id]/index.vue | 2 +- pages/settings.vue | 8 +- pages/settings/downloads.vue | 218 ++++++++++++++++++- src-tauri/Cargo.lock | 28 +-- src-tauri/src/db.rs | 40 ++-- src-tauri/src/downloads/download_commands.rs | 29 +-- src-tauri/src/lib.rs | 12 +- src-tauri/src/library.rs | 17 +- src-tauri/src/remote.rs | 3 +- yarn.lock | 8 +- 13 files changed, 291 insertions(+), 80 deletions(-) diff --git a/components/HeaderUserWidget.vue b/components/HeaderUserWidget.vue index 410ba72..27f2867 100644 --- a/components/HeaderUserWidget.vue +++ b/components/HeaderUserWidget.vue @@ -21,7 +21,7 @@ leave-to-class="transform opacity-0 scale-95" > diff --git a/package.json b/package.json index 163e8f3..49c2009 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@prisma/client": "5.20.0", "@tauri-apps/api": ">=2.0.0", "@tauri-apps/plugin-deep-link": "~2", - "@tauri-apps/plugin-dialog": "~2", + "@tauri-apps/plugin-dialog": "^2.0.1", "@tauri-apps/plugin-shell": ">=2.0.0", "nuxt": "^3.13.0", "scss": "^0.2.4", diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index 2341cab..94275ba 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -3,7 +3,7 @@ class="mx-auto w-full relative flex flex-col justify-center pt-64 z-10 overflow-hidden" > -
+

@@ -22,8 +22,8 @@ :is="item.icon" :class="[ itemIdx === currentPageIndex - ? 'text-blue-600' - : 'text-zinc-400 group-hover:text-blue-600', + ? 'text-zinc-100' + : 'text-zinc-400 group-hover:text-zinc-200', 'transition h-6 w-6 shrink-0', ]" aria-hidden="true" diff --git a/pages/settings/downloads.vue b/pages/settings/downloads.vue index 27e0f69..0878be7 100644 --- a/pages/settings/downloads.vue +++ b/pages/settings/downloads.vue @@ -1,3 +1,217 @@ \ No newline at end of file +
+
+
+
+

+ Install directories +

+

+ This is where Drop will download game files to, and store them + indefinitely while you play. Drop and games may store other + information elsewhere, like saves or mods. +

+
+
+ +
+
+
+
    +
  • +
    + +
    +

    + {{ dir }} +

    +
    +
    +
    + +
    +
  • +
+
+ + + +
+ + +
+
+ + +
+
+
+ +
+ +
+

+ Select an empty directory to add. +

+
+
+
+
+ + Upload + + +
+
+
+
+
+
+

+ {{ error }} +

+
+
+
+
+
+
+
+
+
+ + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 0b03042..88505d9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -58,9 +58,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -73,36 +73,36 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -612,9 +612,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 4daf984..c6d32e7 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -6,7 +6,7 @@ use std::{ }; use directories::BaseDirs; -use log::debug; +use log::{debug, info}; use rustbreak::{deser::Bincode, PathDatabase}; use rustix::path::Arg; use serde::{Deserialize, Serialize}; @@ -68,23 +68,23 @@ impl DatabaseImpls for DatabaseInterface { debug!("Creating games directory"); create_dir_all(games_base_dir.clone()).unwrap(); - let default = Database { - auth: None, - base_url: "".to_string(), - games: DatabaseGames { - install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], - games_statuses: HashMap::new(), - }, - }; - #[allow(clippy::let_and_return)] let exists = fs::exists(db_path.clone()).unwrap(); let db = match exists { true => PathDatabase::load_from_path(db_path).expect("Database loading failed"), false => { + let default = Database { + auth: None, + base_url: "".to_string(), + games: DatabaseGames { + install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], + games_statuses: HashMap::new(), + }, + }; debug!("Creating database at path {}", db_path.as_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") + } }; db @@ -114,8 +114,8 @@ pub fn add_new_download_dir(new_dir: String) -> Result<(), String> { let dir_contents = new_dir_path .read_dir() .map_err(|e| format!("Unable to check directory contents: {}", e))?; - if dir_contents.count() == 0 { - return Err("Path is not empty".to_string()); + if dir_contents.count() != 0 { + return Err("Directory is not empty".to_string()); } } else { create_dir_all(new_dir_path) @@ -126,6 +126,18 @@ pub fn add_new_download_dir(new_dir: String) -> Result<(), String> { let mut lock = DB.borrow_data_mut().unwrap(); lock.games.install_dirs.push(new_dir); drop(lock); + DB.save().unwrap(); Ok(()) } + +// Will, in future, return disk/remaining size +// Just returns the directories that have been set up +#[tauri::command] +pub fn fetch_download_dir_stats() -> Result, String> { + let lock = DB.borrow_data().unwrap(); + let directories = lock.games.install_dirs.clone(); + drop(lock); + + Ok(directories) +} diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 5b429df..5644106 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -2,43 +2,39 @@ use std::sync::Mutex; use log::info; -use crate::{AppError, AppState}; +use crate::AppState; #[tauri::command] pub fn download_game( game_id: String, game_version: String, state: tauri::State<'_, Mutex>, -) -> Result<(), AppError> { - +) -> Result<(), String> { state .lock() .unwrap() .download_manager .queue_game(game_id, game_version, 0) - .map_err(|_| AppError::Signal) + .map_err(|_| "An error occurred while communicating with the download manager.".to_string()) } #[tauri::command] pub fn get_current_game_download_progress( state: tauri::State<'_, Mutex>, -) -> Result { +) -> Result { match state .lock() .unwrap() .download_manager .get_current_game_download_progress() - { - Some(progress) => Ok(progress), - None => Err(AppError::DoesNotExist), - } + { + Some(progress) => Ok(progress), + None => Err("Game does not exist".to_string()), + } } #[tauri::command] -pub fn stop_game_download( - state: tauri::State<'_, Mutex>, - game_id: String -) { +pub fn stop_game_download(state: tauri::State<'_, Mutex>, game_id: String) { info!("Cancelling game download {}", game_id); state .lock() @@ -47,12 +43,7 @@ pub fn stop_game_download( .cancel_download(game_id); } #[tauri::command] -pub fn get_current_write_speed( - state: tauri::State<'_, Mutex>, -) { - -} - +pub fn get_current_write_speed(state: tauri::State<'_, Mutex>) {} /* fn use_download_agent( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index beb1aeb..707b787 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,7 +10,7 @@ mod tests; use crate::db::DatabaseImpls; use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; -use db::{add_new_download_dir, DatabaseInterface, DATA_ROOT_DIR}; +use db::{add_new_download_dir, fetch_download_dir_stats, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; use downloads::download_manager::DownloadManagerBuilder; use downloads::download_manager_interface::DownloadManager; @@ -36,12 +36,6 @@ pub enum AppStatus { SignedInNeedsReauth, ServerUnavailable, } -#[derive(Debug, Serialize)] -pub enum AppError { - DoesNotExist, - Signal, - RemoteAccess(String) -} #[derive(Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -65,7 +59,7 @@ pub struct AppState { } #[tauri::command] -fn fetch_state(state: tauri::State<'_, Mutex>) -> Result { +fn fetch_state(state: tauri::State<'_, Mutex>) -> Result { let guard = state.lock().unwrap(); let cloned_state = guard.clone(); drop(guard); @@ -133,12 +127,14 @@ pub fn run() { fetch_library, fetch_game, add_new_download_dir, + fetch_download_dir_stats, // Downloads download_game, get_current_game_download_progress, stop_game_download ]) .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_dialog::init()) .setup(|app| { #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 1127749..50da6af 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -1,5 +1,6 @@ use std::sync::Mutex; +use log::info; use serde::{Deserialize, Serialize}; use serde_json::json; use tauri::{AppHandle, Manager}; @@ -7,7 +8,6 @@ use tauri::{AppHandle, Manager}; use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; use crate::remote::RemoteAccessError; -use crate::AppError; use crate::{auth::generate_authorization_header, AppState, DB}; #[derive(serde::Serialize)] @@ -19,7 +19,7 @@ struct FetchGameStruct { #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Game { - game_id: String, + id: String, m_name: String, m_short_description: String, m_description: String, @@ -55,12 +55,12 @@ fn fetch_library_logic(app: AppHandle) -> Result { let mut db_handle = DB.borrow_data_mut().unwrap(); for game in games.iter() { - handle.games.insert(game.game_id.clone(), game.clone()); - if !db_handle.games.games_statuses.contains_key(&game.game_id) { + handle.games.insert(game.id.clone(), game.clone()); + if !db_handle.games.games_statuses.contains_key(&game.id) { db_handle .games .games_statuses - .insert(game.game_id.clone(), DatabaseGameStatus::Remote); + .insert(game.id.clone(), DatabaseGameStatus::Remote); } } @@ -70,9 +70,8 @@ fn fetch_library_logic(app: AppHandle) -> Result { } #[tauri::command] -pub fn fetch_library(app: AppHandle) -> Result { - fetch_library_logic(app) - .map_err(|e| AppError::RemoteAccess(e.to_string())) +pub fn fetch_library(app: AppHandle) -> Result { + fetch_library_logic(app).map_err(|e| e.to_string()) } fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result { @@ -88,7 +87,7 @@ fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result write!(f, "HTTP {}", error), - RemoteAccessError::ParsingError(parse_error) => todo!(), RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"), RemoteAccessError::HandshakeFailed => write!(f, "Failed to complete handshake"), RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"), diff --git a/yarn.lock b/yarn.lock index da122f8..af475d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1376,10 +1376,10 @@ dependencies: "@tauri-apps/api" "^2.0.0" -"@tauri-apps/plugin-dialog@~2": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0.tgz#f1e2840c7f824572a76b375fd1b538a36f28de14" - integrity sha512-ApNkejXP2jpPBSifznPPcHTXxu9/YaRW+eJ+8+nYwqp0lLUtebFHG4QhxitM43wwReHE81WAV1DQ/b+2VBftOA== +"@tauri-apps/plugin-dialog@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.1.tgz#cca38f2ef361c6d92495f5aa12154492cf3fa779" + integrity sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q== dependencies: "@tauri-apps/api" "^2.0.0" From a580a46e17d91125c12b48ebcb10baed24485af2 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 25 Nov 2024 16:09:29 +1100 Subject: [PATCH 122/164] feat(settings): finish download dir CRUD interface --- pages/settings/downloads.vue | 33 +++++++++++++++++++++++++++------ src-tauri/src/db.rs | 15 ++++++++++++++- src-tauri/src/lib.rs | 5 +++-- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/pages/settings/downloads.vue b/pages/settings/downloads.vue index 0878be7..237ea72 100644 --- a/pages/settings/downloads.vue +++ b/pages/settings/downloads.vue @@ -27,7 +27,7 @@

  • @@ -43,7 +43,16 @@
- @@ -169,7 +178,14 @@ const currentDirectory = ref(undefined); const error = ref(undefined); const createDirectoryLoading = ref(false); -const dirs = ref(await invoke>("fetch_download_dir_stats")); +const dirs = ref>([]); + +async function updateDirs() { + const newDirs = await invoke>("fetch_download_dir_stats"); + dirs.value = newDirs; +} + +await updateDirs(); async function selectDirectoryDialog(): Promise { const res = await invoke("plugin:dialog|open", { @@ -201,12 +217,12 @@ async function submitDirectory() { createDirectoryLoading.value = true; // Add directory - await invoke("add_new_download_dir", { newDir: currentDirectory.value }); + await invoke("add_download_dir", { newDir: currentDirectory.value }); // Update list - const newDirs = await invoke>("fetch_download_dir_stats"); - dirs.value = newDirs; + await updateDirs(); + currentDirectory.value = undefined; createDirectoryLoading.value = false; open.value = false; } catch (e) { @@ -214,4 +230,9 @@ async function submitDirectory() { createDirectoryLoading.value = false; } } + +async function deleteDirectory(index: number) { + await invoke("delete_download_dir", { index }); + await updateDirs(); +} diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index c6d32e7..9ebd141 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -101,7 +101,7 @@ impl DatabaseImpls for DatabaseInterface { } #[tauri::command] -pub fn add_new_download_dir(new_dir: String) -> Result<(), String> { +pub fn add_download_dir(new_dir: String) -> Result<(), String> { // Check the new directory is all good let new_dir_path = Path::new(&new_dir); if new_dir_path.exists() { @@ -124,6 +124,9 @@ pub fn add_new_download_dir(new_dir: String) -> Result<(), String> { // Add it to the dictionary let mut lock = DB.borrow_data_mut().unwrap(); + if lock.games.install_dirs.contains(&new_dir) { + return Err("Download directory already used".to_string()); + } lock.games.install_dirs.push(new_dir); drop(lock); DB.save().unwrap(); @@ -131,6 +134,16 @@ pub fn add_new_download_dir(new_dir: String) -> Result<(), String> { Ok(()) } +#[tauri::command] +pub fn delete_download_dir(index: usize) -> Result<(), String> { + let mut lock = DB.borrow_data_mut().unwrap(); + lock.games.install_dirs.remove(index); + drop(lock); + DB.save().unwrap(); + + Ok(()) +} + // Will, in future, return disk/remaining size // Just returns the directories that have been set up #[tauri::command] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 707b787..06eef38 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,7 +10,7 @@ mod tests; use crate::db::DatabaseImpls; use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; -use db::{add_new_download_dir, fetch_download_dir_stats, DatabaseInterface, DATA_ROOT_DIR}; +use db::{add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; use downloads::download_manager::DownloadManagerBuilder; use downloads::download_manager_interface::DownloadManager; @@ -126,7 +126,8 @@ pub fn run() { // Library fetch_library, fetch_game, - add_new_download_dir, + add_download_dir, + delete_download_dir, fetch_download_dir_stats, // Downloads download_game, From a53d838d0550370d4fef18fab987620ed0a22268 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 26 Nov 2024 18:09:15 +1100 Subject: [PATCH 123/164] feat: retry connnection on server unavailable --- pages/error/serverunavailable.vue | 18 ++++++++++++++++-- src-tauri/src/auth.rs | 18 ++++++++++++++---- src-tauri/src/lib.rs | 3 ++- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/pages/error/serverunavailable.vue b/pages/error/serverunavailable.vue index d0a2515..cfbb178 100644 --- a/pages/error/serverunavailable.vue +++ b/pages/error/serverunavailable.vue @@ -20,9 +20,15 @@ We were unable to contact your Drop instance. See if you can open it in your web browser, or contact your server admin for help.

-
+
+ Connect to different instance @@ -68,7 +74,15 @@ diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs index 78da5b2..6761cec 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/auth.rs @@ -1,7 +1,5 @@ use std::{ - env, - sync::Mutex, - time::{SystemTime, UNIX_EPOCH}, + borrow::BorrowMut, env, sync::Mutex, time::{SystemTime, UNIX_EPOCH} }; use log::{info, warn}; @@ -11,7 +9,7 @@ use tauri::{AppHandle, Emitter, Manager}; use url::Url; use crate::{ - db::{DatabaseAuth, DatabaseImpls}, + db::{self, DatabaseAuth, DatabaseImpls}, remote::RemoteAccessError, AppState, AppStatus, User, DB, }; @@ -185,6 +183,18 @@ pub async fn auth_initiate<'a>() -> Result<(), String> { Ok(()) } +#[tauri::command] +pub fn retry_connect(state: tauri::State<'_, Mutex>) -> Result<(), ()> { + let (app_status, user) = setup()?; + + let mut guard = state.lock().unwrap(); + guard.status = app_status; + guard.user = user; + drop(guard); + + Ok(()) +} + pub fn setup() -> Result<(AppStatus, Option), ()> { let data = DB.borrow_data().unwrap(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 06eef38..ce9462e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,7 +9,7 @@ mod settings; mod tests; use crate::db::DatabaseImpls; -use auth::{auth_initiate, generate_authorization_header, recieve_handshake}; +use auth::{auth_initiate, generate_authorization_header, recieve_handshake, retry_connect}; use db::{add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, DATA_ROOT_DIR}; use downloads::download_commands::*; use downloads::download_manager::DownloadManagerBuilder; @@ -120,6 +120,7 @@ pub fn run() { fetch_state, // Auth auth_initiate, + retry_connect, // Remote use_remote, gen_drop_url, From 99c8b39a1170d989815105d37ec68416d03c66d9 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 26 Nov 2024 19:54:43 +1100 Subject: [PATCH 124/164] refactor(download manager): rename files to what they contain --- src-tauri/src/db.rs | 1 - src-tauri/src/downloads/download_manager.rs | 345 ++++++------------ .../src/downloads/download_manager_builder.rs | 252 +++++++++++++ .../downloads/download_manager_interface.rs | 135 ------- src-tauri/src/downloads/mod.rs | 2 +- src-tauri/src/downloads/queue.rs | 2 +- src-tauri/src/lib.rs | 12 +- src-tauri/src/library.rs | 15 + 8 files changed, 398 insertions(+), 366 deletions(-) create mode 100644 src-tauri/src/downloads/download_manager_builder.rs delete mode 100644 src-tauri/src/downloads/download_manager_interface.rs diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 9ebd141..74536dc 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -28,7 +28,6 @@ pub enum DatabaseGameStatus { Downloading, Installed, Updating, - Uninstalling, } diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index d71d801..f3406aa 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -1,69 +1,20 @@ use std::{ - collections::{HashMap, VecDeque}, + any::Any, + collections::VecDeque, sync::{ - mpsc::{channel, Receiver, Sender}, - Arc, Mutex, + mpsc::{SendError, Sender}, + Arc, Mutex, MutexGuard, }, - thread::spawn, + thread::JoinHandle, }; -use log::{info, warn}; +use log::info; use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, - download_manager_interface::{AgentInterfaceData, DownloadManager}, - download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, queue::Queue, }; -/* - -Welcome to the download manager, the most overengineered, glorious piece of bullshit. - -The download manager takes a queue of game_ids and their associated -GameDownloadAgents, and then, one-by-one, executes them. It provides an interface -to interact with the currently downloading agent, and manage the queue. - -When the DownloadManager is initialised, it is designed to provide a reference -which can be used to provide some instructions (the DownloadManagerInterface), -but other than that, it runs without any sort of interruptions. - -It does this by opening up two data structures. Primarily is the command_receiver, -and mpsc (multi-channel-single-producer) which allows commands to be sent from -the Interface, and queued up for the Manager to process. - -These have been mapped in the DownloadManagerSignal docs. - -The other way to interact with the DownloadManager is via the donwload_queue, -which is just a collection of ids which may be rearranged to suit -whichever download queue order is required. - -+----------------------------------------------------------------------------+ -| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! | -| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE | -| WHICH HAS NOT BEEN ACCOUNTED FOR | -+----------------------------------------------------------------------------+ - -This download queue does not actually own any of the GameDownloadAgents. It is -simply a id-based reference system. The actual Agents are stored in the -download_agent_registry HashMap, as ordering is no issue here. This is why -appending or removing from the download_queue must be done via signals. - -Behold, my madness - quexeky - -*/ - -pub struct DownloadManagerBuilder { - download_agent_registry: HashMap>, - download_queue: Queue, - command_receiver: Receiver, - sender: Sender, - progress: Arc>>, - status: Arc>, - - current_game_interface: Option>, // Should be the only game download agent in the map with the "Go" flag - active_control_flag: Option, -} pub enum DownloadManagerSignal { /// Resumes (or starts) the DownloadManager Go, @@ -86,7 +37,7 @@ pub enum DownloadManagerStatus { Downloading, Paused, Empty, - Error, + Error(GameDownloadError), } pub enum GameDownloadStatus { Downloading, @@ -95,174 +46,120 @@ pub enum GameDownloadStatus { Error, } -impl DownloadManagerBuilder { - pub fn build() -> DownloadManager { - let queue = Queue::new(); - let (command_sender, command_receiver) = channel(); - let active_progress = Arc::new(Mutex::new(None)); - let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty)); - - let manager = Self { - download_agent_registry: HashMap::new(), - download_queue: queue.clone(), - command_receiver, - current_game_interface: None, - active_control_flag: None, - status: status.clone(), - sender: command_sender.clone(), - progress: active_progress.clone(), - }; - - let terminator = spawn(|| manager.manage_queue()); - - DownloadManager::new(terminator, queue, active_progress, command_sender) - } - - fn manage_queue(mut self) -> Result<(), ()> { - loop { - let signal = match self.command_receiver.recv() { - Ok(signal) => signal, - Err(e) => return Err(()), - }; - - match signal { - DownloadManagerSignal::Go => { - self.manage_go_signal(); - } - DownloadManagerSignal::Stop => { - self.manage_stop_signal(); - } - DownloadManagerSignal::Completed(game_id) => { - self.manage_completed_signal(game_id); - } - DownloadManagerSignal::Queue(game_id, version, target_download_dir) => { - self.manage_queue_signal(game_id, version, target_download_dir); - } - DownloadManagerSignal::Finish => { - if let Some(active_control_flag) = self.active_control_flag { - active_control_flag.set(DownloadThreadControlFlag::Stop) - } - return Ok(()); - } - DownloadManagerSignal::Error(e) => { - self.manage_error_signal(e); - }, - DownloadManagerSignal::Cancel(id) => { - self.manage_cancel_signal(id); - }, - }; +/// Accessible front-end for the DownloadManager +/// +/// The system works entirely through signals, both internally and externally, +/// all of which are accessible through the DownloadManagerSignal type, but +/// should not be used directly. Rather, signals are abstracted through this +/// interface. +/// +/// The actual download queue may be accessed through the .edit() function, +/// which provides raw access to the underlying queue. +/// THIS EDITING IS BLOCKING!!! +pub struct DownloadManager { + terminator: JoinHandle>, + download_queue: Queue, + progress: Arc>>, + command_sender: Sender, +} +pub struct AgentInterfaceData { + pub id: String, + pub status: Mutex, +} +impl From> for AgentInterfaceData { + fn from(value: Arc) -> Self { + Self { + id: value.id.clone(), + status: Mutex::from(GameDownloadStatus::Uninitialised), } } - - fn manage_stop_signal(&mut self) { - info!("Got signal 'Stop'"); - if let Some(active_control_flag) = self.active_control_flag.clone() { - active_control_flag.set(DownloadThreadControlFlag::Stop); - } - } - - fn manage_completed_signal(&mut self, game_id: String) { - info!("Got signal 'Completed'"); - if let Some(interface) = &self.current_game_interface { - // When if let chains are stabilised, combine these two statements - if interface.id == game_id { - info!("Popping consumed data"); - self.download_queue.pop_front(); - self.download_agent_registry.remove(&game_id); - self.active_control_flag = None; - *self.progress.lock().unwrap() = None; - } - } - self.sender.send(DownloadManagerSignal::Go).unwrap(); - } - - fn manage_queue_signal(&mut self, id: String, version: String, target_download_dir: usize) { - info!("Got signal Queue"); - let download_agent = Arc::new(GameDownloadAgent::new( - id.clone(), - version, - target_download_dir, - self.sender.clone() - )); - let agent_status = GameDownloadStatus::Uninitialised; - let interface_data = AgentInterfaceData { - id, - status: Mutex::new(agent_status), - }; - self.download_agent_registry - .insert(interface_data.id.clone(), download_agent); - self.download_queue.append(interface_data); - } - - fn manage_go_signal(&mut self) { - info!("Got signal 'Go'"); - if self.active_control_flag.is_none() && !self.download_agent_registry.is_empty() { - info!("Starting download agent"); - let download_agent = { - let front = self.download_queue.read().front().unwrap().clone(); - self.download_agent_registry - .get(&front.id) - .unwrap() - .clone() - }; - let download_agent_interface = - Arc::new(AgentInterfaceData::from(download_agent.clone())); - self.current_game_interface = Some(download_agent_interface); - - let progress_object = download_agent.progress.clone(); - *self.progress.lock().unwrap() = Some(progress_object); - - let active_control_flag = download_agent.control_flag.clone(); - self.active_control_flag = Some(active_control_flag.clone()); - - let sender = self.sender.clone(); - - info!("Spawning download"); - spawn(move || { - match download_agent.download() { - Ok(_) => { - sender.send(DownloadManagerSignal::Completed(download_agent.id.clone())).unwrap(); - }, - Err(e) => { - warn!("Download failed"); - //todo!() // Account for if the setup_download function fails - }, - }; - }); - info!("Finished spawning Download"); - - active_control_flag.set(DownloadThreadControlFlag::Go); - self.set_status(DownloadManagerStatus::Downloading); - } else if let Some(active_control_flag) = self.active_control_flag.clone() { - info!("Restarting current download"); - active_control_flag.set(DownloadThreadControlFlag::Go); - } else { - info!("Nothing was set"); - } - } - fn manage_error_signal(&self, error: GameDownloadError) { - let current_status = self.current_game_interface.clone().unwrap(); - let mut lock = current_status.status.lock().unwrap(); - *lock = GameDownloadStatus::Error; - self.set_status(DownloadManagerStatus::Error); - } - fn manage_cancel_signal(&mut self, game_id: String) { - if let Some(current_flag) = &self.active_control_flag { - current_flag.set(DownloadThreadControlFlag::Stop); - self.active_control_flag = None; - *self.progress.lock().unwrap() = None; - } - self.download_agent_registry.remove(&game_id); - let mut lock = self.download_queue.edit(); - let index = match lock.iter().position(|interface| interface.id == game_id) { - Some(index) => index, - None => return, - }; - lock.remove(index); - self.sender.send(DownloadManagerSignal::Go).unwrap(); - info!("{:?}", self.download_agent_registry.iter().map(|x| x.0.clone()).collect::()); - } - fn set_status(&self, status: DownloadManagerStatus) { - *self.status.lock().unwrap() = status; - } +} + +impl DownloadManager { + pub fn new( + terminator: JoinHandle>, + download_queue: Queue, + progress: Arc>>, + command_sender: Sender, + ) -> Self { + Self { + terminator, + download_queue, + progress, + command_sender, + } + } + + pub fn queue_game( + &self, + id: String, + version: String, + target_download_dir: usize, + ) -> Result<(), SendError> { + info!("Adding game id {}", id); + self.command_sender.send(DownloadManagerSignal::Queue( + id, + version, + target_download_dir, + ))?; + self.command_sender.send(DownloadManagerSignal::Go) + } + pub fn cancel_download( + &self, + game_id: String + ) { + self.command_sender.send(DownloadManagerSignal::Cancel(game_id)).unwrap(); + } + pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { + self.download_queue.edit() + } + pub fn read_queue(&self) -> VecDeque> { + self.download_queue.read() + } + pub fn get_current_game_download_progress(&self) -> Option { + let progress_object = (*self.progress.lock().unwrap()).clone()?; + Some(progress_object.get_progress()) + } + pub fn rearrange_string(&self, id: String, new_index: usize) { + let mut queue = self.edit(); + let current_index = get_index_from_id(&mut queue, id).unwrap(); + let to_move = queue.remove(current_index).unwrap(); + queue.insert(new_index, to_move); + } + pub fn rearrange(&self, current_index: usize, new_index: usize) { + let mut queue = self.edit(); + let to_move = queue.remove(current_index).unwrap(); + queue.insert(new_index, to_move); + } + pub fn remove_from_queue(&self, index: usize) { + self.edit().remove(index); + } + pub fn remove_from_queue_string(&self, id: String) { + let mut queue = self.edit(); + let current_index = get_index_from_id(&mut queue, id).unwrap(); + queue.remove(current_index); + } + pub fn pause_downloads(&self) -> Result<(), SendError> { + self.command_sender.send(DownloadManagerSignal::Stop) + } + pub fn resume_downloads(&self) -> Result<(), SendError> { + self.command_sender.send(DownloadManagerSignal::Go) + } + pub fn ensure_terminated(self) -> Result, Box> { + self.command_sender + .send(DownloadManagerSignal::Finish) + .unwrap(); + self.terminator.join() + } +} + +/// Takes in the locked value from .edit() and attempts to +/// get the index of whatever game_id is passed in +fn get_index_from_id( + queue: &mut MutexGuard<'_, VecDeque>>, + id: String, +) -> Option { + queue + .iter() + .position(|download_agent| download_agent.id == id) } diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs new file mode 100644 index 0000000..e696abf --- /dev/null +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -0,0 +1,252 @@ +use std::{ + collections::HashMap, + sync::{ + mpsc::{channel, Receiver, Sender}, + Arc, Mutex, + }, + thread::spawn, +}; + +use log::{error, info, warn}; + +use super::{ + download_agent::{GameDownloadAgent, GameDownloadError}, + download_manager::{ + AgentInterfaceData, DownloadManager, DownloadManagerSignal, DownloadManagerStatus, + GameDownloadStatus, + }, + download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, + progress_object::ProgressObject, + queue::Queue, +}; + +/* + +Welcome to the download manager, the most overengineered, glorious piece of bullshit. + +The download manager takes a queue of game_ids and their associated +GameDownloadAgents, and then, one-by-one, executes them. It provides an interface +to interact with the currently downloading agent, and manage the queue. + +When the DownloadManager is initialised, it is designed to provide a reference +which can be used to provide some instructions (the DownloadManagerInterface), +but other than that, it runs without any sort of interruptions. + +It does this by opening up two data structures. Primarily is the command_receiver, +and mpsc (multi-channel-single-producer) which allows commands to be sent from +the Interface, and queued up for the Manager to process. + +These have been mapped in the DownloadManagerSignal docs. + +The other way to interact with the DownloadManager is via the donwload_queue, +which is just a collection of ids which may be rearranged to suit +whichever download queue order is required. + ++----------------------------------------------------------------------------+ +| DO NOT ATTEMPT TO ADD OR REMOVE FROM THE QUEUE WITHOUT USING SIGNALS!! | +| THIS WILL CAUSE A DESYNC BETWEEN THE DOWNLOAD AGENT REGISTRY AND THE QUEUE | +| WHICH HAS NOT BEEN ACCOUNTED FOR | ++----------------------------------------------------------------------------+ + +This download queue does not actually own any of the GameDownloadAgents. It is +simply a id-based reference system. The actual Agents are stored in the +download_agent_registry HashMap, as ordering is no issue here. This is why +appending or removing from the download_queue must be done via signals. + +Behold, my madness - quexeky + +*/ + +pub struct DownloadManagerBuilder { + download_agent_registry: HashMap>, + download_queue: Queue, + command_receiver: Receiver, + sender: Sender, + progress: Arc>>, + status: Arc>, + + current_game_interface: Option>, // Should be the only game download agent in the map with the "Go" flag + active_control_flag: Option, +} + +impl DownloadManagerBuilder { + pub fn build() -> DownloadManager { + let queue = Queue::new(); + let (command_sender, command_receiver) = channel(); + let active_progress = Arc::new(Mutex::new(None)); + let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty)); + + let manager = Self { + download_agent_registry: HashMap::new(), + download_queue: queue.clone(), + command_receiver, + current_game_interface: None, + active_control_flag: None, + status: status.clone(), + sender: command_sender.clone(), + progress: active_progress.clone(), + }; + + let terminator = spawn(|| manager.manage_queue()); + + DownloadManager::new(terminator, queue, active_progress, command_sender) + } + + fn manage_queue(mut self) -> Result<(), ()> { + loop { + let signal = match self.command_receiver.recv() { + Ok(signal) => signal, + Err(_) => return Err(()), + }; + + match signal { + DownloadManagerSignal::Go => { + self.manage_go_signal(); + } + DownloadManagerSignal::Stop => { + self.manage_stop_signal(); + } + DownloadManagerSignal::Completed(game_id) => { + self.manage_completed_signal(game_id); + } + DownloadManagerSignal::Queue(game_id, version, target_download_dir) => { + self.manage_queue_signal(game_id, version, target_download_dir); + } + DownloadManagerSignal::Finish => { + if let Some(active_control_flag) = self.active_control_flag { + active_control_flag.set(DownloadThreadControlFlag::Stop) + } + return Ok(()); + } + DownloadManagerSignal::Error(e) => { + self.manage_error_signal(e); + } + DownloadManagerSignal::Cancel(id) => { + self.manage_cancel_signal(id); + } + }; + } + } + + fn manage_stop_signal(&mut self) { + info!("Got signal 'Stop'"); + if let Some(active_control_flag) = self.active_control_flag.clone() { + active_control_flag.set(DownloadThreadControlFlag::Stop); + } + } + + fn manage_completed_signal(&mut self, game_id: String) { + info!("Got signal 'Completed'"); + if let Some(interface) = &self.current_game_interface { + // When if let chains are stabilised, combine these two statements + if interface.id == game_id { + info!("Popping consumed data"); + self.download_queue.pop_front(); + self.download_agent_registry.remove(&game_id); + self.active_control_flag = None; + *self.progress.lock().unwrap() = None; + } + } + self.sender.send(DownloadManagerSignal::Go).unwrap(); + } + + fn manage_queue_signal(&mut self, id: String, version: String, target_download_dir: usize) { + info!("Got signal Queue"); + let download_agent = Arc::new(GameDownloadAgent::new( + id.clone(), + version, + target_download_dir, + self.sender.clone(), + )); + let agent_status = GameDownloadStatus::Uninitialised; + let interface_data = AgentInterfaceData { + id, + status: Mutex::new(agent_status), + }; + self.download_agent_registry + .insert(interface_data.id.clone(), download_agent); + self.download_queue.append(interface_data); + } + + fn manage_go_signal(&mut self) { + info!("Got signal 'Go'"); + if self.active_control_flag.is_none() && !self.download_agent_registry.is_empty() { + info!("Starting download agent"); + let download_agent = { + let front = self.download_queue.read().front().unwrap().clone(); + self.download_agent_registry.get(&front.id).unwrap().clone() + }; + let download_agent_interface = + Arc::new(AgentInterfaceData::from(download_agent.clone())); + self.current_game_interface = Some(download_agent_interface); + + let progress_object = download_agent.progress.clone(); + *self.progress.lock().unwrap() = Some(progress_object); + + let active_control_flag = download_agent.control_flag.clone(); + self.active_control_flag = Some(active_control_flag.clone()); + + let sender = self.sender.clone(); + + info!("Spawning download"); + spawn(move || { + match download_agent.download() { + Ok(_) => { + // TODO wrap this pattern in a macro + let result = sender + .send(DownloadManagerSignal::Completed(download_agent.id.clone())); + if let Err(err) = result { + error!("{}", err); + } + } + Err(err) => { + let result = sender.send(DownloadManagerSignal::Error(err)); + if let Err(err) = result { + error!("{}", err); + } + } + }; + }); + info!("Finished spawning Download"); + + active_control_flag.set(DownloadThreadControlFlag::Go); + self.set_status(DownloadManagerStatus::Downloading); + } else if let Some(active_control_flag) = self.active_control_flag.clone() { + info!("Restarting current download"); + active_control_flag.set(DownloadThreadControlFlag::Go); + } else { + info!("Nothing was set"); + } + } + fn manage_error_signal(&self, error: GameDownloadError) { + let current_status = self.current_game_interface.clone().unwrap(); + let mut lock = current_status.status.lock().unwrap(); + *lock = GameDownloadStatus::Error; + self.set_status(DownloadManagerStatus::Error(error)); + } + fn manage_cancel_signal(&mut self, game_id: String) { + if let Some(current_flag) = &self.active_control_flag { + current_flag.set(DownloadThreadControlFlag::Stop); + self.active_control_flag = None; + *self.progress.lock().unwrap() = None; + } + self.download_agent_registry.remove(&game_id); + let mut lock = self.download_queue.edit(); + let index = match lock.iter().position(|interface| interface.id == game_id) { + Some(index) => index, + None => return, + }; + lock.remove(index); + self.sender.send(DownloadManagerSignal::Go).unwrap(); + info!( + "{:?}", + self.download_agent_registry + .iter() + .map(|x| x.0.clone()) + .collect::() + ); + } + fn set_status(&self, status: DownloadManagerStatus) { + *self.status.lock().unwrap() = status; + } +} diff --git a/src-tauri/src/downloads/download_manager_interface.rs b/src-tauri/src/downloads/download_manager_interface.rs deleted file mode 100644 index 0ecf2bd..0000000 --- a/src-tauri/src/downloads/download_manager_interface.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::{ - any::Any, - collections::VecDeque, - sync::{ - mpsc::{SendError, Sender}, - Arc, Mutex, MutexGuard, - }, - thread::JoinHandle, -}; - -use log::info; - -use super::{ - download_agent::GameDownloadAgent, - download_manager::{DownloadManagerSignal, GameDownloadStatus}, - progress_object::ProgressObject, queue::Queue, -}; - -/// Accessible front-end for the DownloadManager -/// -/// The system works entirely through signals, both internally and externally, -/// all of which are accessible through the DownloadManagerSignal type, but -/// should not be used directly. Rather, signals are abstracted through this -/// interface. -/// -/// The actual download queue may be accessed through the .edit() function, -/// which provides raw access to the underlying queue. -/// THIS EDITING IS BLOCKING!!! -pub struct DownloadManager { - terminator: JoinHandle>, - download_queue: Queue, - progress: Arc>>, - command_sender: Sender, -} -pub struct AgentInterfaceData { - pub id: String, - pub status: Mutex, -} -impl From> for AgentInterfaceData { - fn from(value: Arc) -> Self { - Self { - id: value.id.clone(), - status: Mutex::from(GameDownloadStatus::Uninitialised), - } - } -} - -impl DownloadManager { - pub fn new( - terminator: JoinHandle>, - download_queue: Queue, - progress: Arc>>, - command_sender: Sender, - ) -> Self { - Self { - terminator, - download_queue, - progress, - command_sender, - } - } - - pub fn queue_game( - &self, - id: String, - version: String, - target_download_dir: usize, - ) -> Result<(), SendError> { - info!("Adding game id {}", id); - self.command_sender.send(DownloadManagerSignal::Queue( - id, - version, - target_download_dir, - ))?; - self.command_sender.send(DownloadManagerSignal::Go) - } - pub fn cancel_download( - &self, - game_id: String - ) { - self.command_sender.send(DownloadManagerSignal::Cancel(game_id)).unwrap(); - } - pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { - self.download_queue.edit() - } - pub fn read_queue(&self) -> VecDeque> { - self.download_queue.read() - } - pub fn get_current_game_download_progress(&self) -> Option { - let progress_object = (*self.progress.lock().unwrap()).clone()?; - Some(progress_object.get_progress()) - } - pub fn rearrange_string(&self, id: String, new_index: usize) { - let mut queue = self.edit(); - let current_index = get_index_from_id(&mut queue, id).unwrap(); - let to_move = queue.remove(current_index).unwrap(); - queue.insert(new_index, to_move); - } - pub fn rearrange(&self, current_index: usize, new_index: usize) { - let mut queue = self.edit(); - let to_move = queue.remove(current_index).unwrap(); - queue.insert(new_index, to_move); - } - pub fn remove_from_queue(&self, index: usize) { - self.edit().remove(index); - } - pub fn remove_from_queue_string(&self, id: String) { - let mut queue = self.edit(); - let current_index = get_index_from_id(&mut queue, id).unwrap(); - queue.remove(current_index); - } - pub fn pause_downloads(&self) -> Result<(), SendError> { - self.command_sender.send(DownloadManagerSignal::Stop) - } - pub fn resume_downloads(&self) -> Result<(), SendError> { - self.command_sender.send(DownloadManagerSignal::Go) - } - pub fn ensure_terminated(self) -> Result, Box> { - self.command_sender - .send(DownloadManagerSignal::Finish) - .unwrap(); - self.terminator.join() - } -} - -/// Takes in the locked value from .edit() and attempts to -/// get the index of whatever game_id is passed in -fn get_index_from_id( - queue: &mut MutexGuard<'_, VecDeque>>, - id: String, -) -> Option { - queue - .iter() - .position(|download_agent| download_agent.id == id) -} diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index 3556f81..c7c11ee 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -1,8 +1,8 @@ pub mod download_agent; pub mod download_commands; mod download_logic; +pub mod download_manager_builder; pub mod download_manager; -pub mod download_manager_interface; mod download_thread_control_flag; mod manifest; mod progress_object; diff --git a/src-tauri/src/downloads/queue.rs b/src-tauri/src/downloads/queue.rs index de1be17..1ce695f 100644 --- a/src-tauri/src/downloads/queue.rs +++ b/src-tauri/src/downloads/queue.rs @@ -1,6 +1,6 @@ use std::{collections::VecDeque, sync::{Arc, Mutex, MutexGuard}}; -use super::download_manager_interface::AgentInterfaceData; +use super::download_manager::AgentInterfaceData; #[derive(Clone)] pub struct Queue { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ce9462e..840b803 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,13 +10,16 @@ mod tests; use crate::db::DatabaseImpls; use auth::{auth_initiate, generate_authorization_header, recieve_handshake, retry_connect}; -use db::{add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, DATA_ROOT_DIR}; +use db::{ + add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, + DATA_ROOT_DIR, +}; use downloads::download_commands::*; -use downloads::download_manager::DownloadManagerBuilder; -use downloads::download_manager_interface::DownloadManager; +use downloads::download_manager_builder::DownloadManagerBuilder; +use downloads::download_manager::DownloadManager; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; -use library::{fetch_game, fetch_library, Game}; +use library::{fetch_game, fetch_game_status, fetch_library, Game}; use log::{debug, info}; use remote::{gen_drop_url, use_remote, RemoteAccessError}; use serde::{Deserialize, Serialize}; @@ -130,6 +133,7 @@ pub fn run() { add_download_dir, delete_download_dir, fetch_download_dir_stats, + fetch_game_status, // Downloads download_game, get_current_game_download_progress, diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 50da6af..3fbea71 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use tauri::{AppHandle, Manager}; +use crate::db; use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; use crate::remote::RemoteAccessError; @@ -109,3 +110,17 @@ pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result { Ok(result.unwrap()) } + +#[tauri::command] +pub fn fetch_game_status(id: String) -> Result { + let db_handle = DB.borrow_data().unwrap(); + let status = db_handle + .games + .games_statuses + .get(&id) + .unwrap_or(&DatabaseGameStatus::Remote) + .clone(); + drop(db_handle); + + return Ok(status); +} From 2dedfbbd5c1f4706bbe14c20f7f98067e663245a Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 26 Nov 2024 20:11:03 +1100 Subject: [PATCH 125/164] feat(library): automatically fetch remote data if not available --- pages/library.vue | 2 +- src-tauri/src/db.rs | 1 + src-tauri/src/downloads/download_agent.rs | 2 +- src-tauri/src/library.rs | 51 +++++++++++++++++++++-- 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/pages/library.vue b/pages/library.vue index dcb0b84..c91ea64 100644 --- a/pages/library.vue +++ b/pages/library.vue @@ -13,7 +13,7 @@ >
diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 74536dc..66d08b5 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -35,6 +35,7 @@ pub enum DatabaseGameStatus { #[serde(rename_all = "camelCase")] pub struct DatabaseGames { pub install_dirs: Vec, + // Guaranteed to exist if the game also exists in the app state map pub games_statuses: HashMap, } diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 0bb3b71..9987436 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -212,7 +212,7 @@ impl GameDownloadAgent { } pub fn run(&self) -> Result<(), ()> { - const DOWNLOAD_MAX_THREADS: usize = 4; + const DOWNLOAD_MAX_THREADS: usize = 1; let pool = ThreadPoolBuilder::new() .num_threads(DOWNLOAD_MAX_THREADS) diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 3fbea71..64372b8 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -1,3 +1,4 @@ +use std::fmt::format; use std::sync::Mutex; use log::info; @@ -8,6 +9,7 @@ use tauri::{AppHandle, Manager}; use crate::db; use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; +use crate::downloads::download_manager::GameDownloadStatus; use crate::remote::RemoteAccessError; use crate::{auth::generate_authorization_header, AppState, DB}; @@ -77,9 +79,9 @@ pub fn fetch_library(app: AppHandle) -> Result { fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result { let state = app.state::>(); - let handle = state.lock().unwrap(); + let mut state_handle = state.lock().unwrap(); - let game = handle.games.get(&id); + let game = state_handle.games.get(&id); if let Some(game) = game { let db_handle = DB.borrow_data().unwrap(); @@ -95,9 +97,50 @@ fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result()?; + state_handle.games.insert(id.clone(), game.clone()); + + let mut db_handle = DB.borrow_data_mut().unwrap(); + + if !db_handle.games.games_statuses.contains_key(&id) { + db_handle + .games + .games_statuses + .insert(id, DatabaseGameStatus::Remote); + } + + let data = FetchGameStruct { + game: game.clone(), + status: db_handle + .games + .games_statuses + .get(&game.id) + .unwrap() + .clone(), + }; + + return Ok(json!(data).to_string()); } #[tauri::command] From 64d7f649c6b9eb16afe68e07ea08bbcb8bb1af8b Mon Sep 17 00:00:00 2001 From: DecDuck Date: Thu, 28 Nov 2024 12:39:21 +1100 Subject: [PATCH 126/164] fix(download manager): use of completed signal, and pause/resuming --- pages/store/index.vue | 28 ++++- src-tauri/src/downloads/download_agent.rs | 102 ++++++++++++------ src-tauri/src/downloads/download_commands.rs | 13 ++- src-tauri/src/downloads/download_logic.rs | 1 - src-tauri/src/downloads/download_manager.rs | 25 ++--- .../src/downloads/download_manager_builder.rs | 43 ++++---- src-tauri/src/downloads/progress_object.rs | 13 ++- src-tauri/src/downloads/queue.rs | 21 ++-- src-tauri/src/lib.rs | 6 +- 9 files changed, 162 insertions(+), 90 deletions(-) diff --git a/pages/store/index.vue b/pages/store/index.vue index 45170b5..85d220b 100644 --- a/pages/store/index.vue +++ b/pages/store/index.vue @@ -7,14 +7,30 @@ @click="startGameDownload" > Download game - ({{ Math.floor(progress * 1000) / 10 }}%) + + ({{ Math.floor(progress * 1000) / 10 }}%) + + + + + + diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 9987436..2e960e3 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -29,7 +29,7 @@ pub struct GameDownloadAgent { contexts: Mutex>, pub manifest: Mutex>, pub progress: ProgressObject, - sender: Sender + sender: Sender, } #[derive(Debug)] @@ -39,12 +39,12 @@ pub enum GameDownloadError { Setup(SetupError), Lock, IoError(io::Error), - DownloadError + DownloadError, } #[derive(Debug)] pub enum SetupError { - Context + Context, } impl Display for GameDownloadError { @@ -61,7 +61,12 @@ impl Display for GameDownloadError { } impl GameDownloadAgent { - pub fn new(id: String, version: String, target_download_dir: usize, sender: Sender) -> Self { + pub fn new( + id: String, + version: String, + target_download_dir: usize, + sender: Sender, + ) -> Self { // Don't run by default let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); Self { @@ -72,7 +77,7 @@ impl GameDownloadAgent { target_download_dir, contexts: Mutex::new(Vec::new()), progress: ProgressObject::new(0, 0), - sender + sender, } } @@ -81,8 +86,8 @@ impl GameDownloadAgent { self.ensure_manifest_exists()?; info!("Ensured manifest exists"); - self.generate_contexts()?; - info!("Generated contexts"); + self.ensure_contexts()?; + info!("Ensured contexts exists"); self.control_flag.set(DownloadThreadControlFlag::Go); @@ -129,7 +134,10 @@ impl GameDownloadAgent { if response.status() != 200 { return Err(GameDownloadError::Communication( - RemoteAccessError::ManifestDownloadFailed(response.status(), response.text().unwrap()) + RemoteAccessError::ManifestDownloadFailed( + response.status(), + response.text().unwrap(), + ), )); } @@ -144,12 +152,15 @@ impl GameDownloadAgent { } fn set_progress_object_params(&self) { + // Avoid re-setting it + if self.progress.get_max() != 0 { + return; + } + let lock = self.contexts.lock().unwrap(); let length = lock.len(); - let chunk_count = lock.iter() - .map(|chunk| chunk.length) - .sum(); + let chunk_count = lock.iter().map(|chunk| chunk.length).sum(); debug!("Setting ProgressObject max to {}", chunk_count); self.progress.set_max(chunk_count); @@ -159,6 +170,18 @@ impl GameDownloadAgent { self.progress.set_time_now(); } + pub fn ensure_contexts(&self) -> Result<(), GameDownloadError> { + let context_lock = self.contexts.lock().unwrap(); + info!("{:?} {}", context_lock, context_lock.is_empty()); + if !context_lock.is_empty() { + return Ok(()); + } + drop(context_lock); + + self.generate_contexts()?; + return Ok(()); + } + pub fn generate_contexts(&self) -> Result<(), GameDownloadError> { let db_lock = DB.borrow_data().unwrap(); let data_base_dir = db_lock.games.install_dirs[self.target_download_dir].clone(); @@ -192,14 +215,14 @@ impl GameDownloadAgent { game_id: game_id.to_string(), path: path.clone(), checksum: chunk.checksums[i].clone(), - length: *length + length: *length, }); running_offset += *length as u64; } #[cfg(target_os = "linux")] if running_offset > 0 { - fallocate(file, FallocateFlags::empty(), 0, running_offset).unwrap(); + let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset); } } @@ -212,54 +235,65 @@ impl GameDownloadAgent { } pub fn run(&self) -> Result<(), ()> { - const DOWNLOAD_MAX_THREADS: usize = 1; + info!("downloading game: {}", self.id); + const DOWNLOAD_MAX_THREADS: usize = 4; let pool = ThreadPoolBuilder::new() .num_threads(DOWNLOAD_MAX_THREADS) .build() .unwrap(); - let new_contexts = Arc::new(Mutex::new(Vec::new())); - let new_contexts_ref = new_contexts.clone(); + let completed_indexes = Arc::new(Mutex::new(Vec::new())); + let completed_indexes_loop_arc = completed_indexes.clone(); pool.scope(move |scope| { let contexts = self.contexts.lock().unwrap(); - for (index, context) in contexts.iter().enumerate() { let context = context.clone(); let control_flag = self.control_flag.clone(); // Clone arcs let progress = self.progress.get(index); // Clone arcs - let new_contexts_ref = new_contexts_ref.clone(); + let completed_indexes_ref = completed_indexes_loop_arc.clone(); scope.spawn(move |_| { - info!( - "starting download for file {} {}", - context.file_name, context.index - ); match download_game_chunk(context.clone(), control_flag, progress) { - Ok(res) => { - match res { - true => {}, - false => new_contexts_ref.lock().unwrap().push(context), + Ok(res) => match res { + true => { + let mut lock = completed_indexes_ref.lock().unwrap(); + lock.push(index); } + false => {} }, Err(e) => { error!("GameDownloadError: {}", e); self.sender.send(DownloadManagerSignal::Error(e)).unwrap(); - new_contexts_ref.lock().unwrap().push(context); - }, + } } }); } }); - if !new_contexts.lock().unwrap().is_empty() { - debug!("New contexts not empty"); - *self.contexts.lock().unwrap() = Arc::into_inner(new_contexts).unwrap().into_inner().unwrap(); - debug!("Contexts: {:?}", *self.contexts.lock().unwrap()); - return Err(()) + + let mut context_lock = self.contexts.lock().unwrap(); + let mut completed_lock = completed_indexes.lock().unwrap(); + + // Sort desc so we don't have to modify indexes + completed_lock.sort_by(|a, b| b.cmp(a)); + + for index in completed_lock.iter() { + context_lock.remove(*index); } - info!("Contexts: {:?}", *self.contexts.lock().unwrap()); + + // If we're not out of contexts, we're not done, so we don't fire completed + if !context_lock.is_empty() { + info!("Download agent didn't finish, not sending completed signal"); + return Ok(()); + } + + // We've completed + self.sender + .send(DownloadManagerSignal::Completed(self.id.clone())) + .unwrap(); + Ok(()) } } diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 5644106..c6f3c42 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -34,7 +34,7 @@ pub fn get_current_game_download_progress( } #[tauri::command] -pub fn stop_game_download(state: tauri::State<'_, Mutex>, game_id: String) { +pub fn cancel_game_download(state: tauri::State<'_, Mutex>, game_id: String) { info!("Cancelling game download {}", game_id); state .lock() @@ -42,6 +42,17 @@ pub fn stop_game_download(state: tauri::State<'_, Mutex>, game_id: Str .download_manager .cancel_download(game_id); } + +#[tauri::command] +pub fn pause_game_downloads(state: tauri::State<'_, Mutex>) { + state.lock().unwrap().download_manager.pause_downloads() +} + +#[tauri::command] +pub fn resume_game_downloads(state: tauri::State<'_, Mutex>) { + state.lock().unwrap().download_manager.resume_downloads() +} + #[tauri::command] pub fn get_current_write_speed(state: tauri::State<'_, Mutex>) {} diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 942423a..ed2645f 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -124,7 +124,6 @@ pub fn download_game_chunk( ) -> Result { // If we're paused if control_flag.get() == DownloadThreadControlFlag::Stop { - info!("Control flag is Stop"); progress.store(0, Ordering::Relaxed); return Ok(false); } diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index f3406aa..f57c87c 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -12,7 +12,8 @@ use log::info; use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, - progress_object::ProgressObject, queue::Queue, + progress_object::ProgressObject, + queue::Queue, }; pub enum DownloadManagerSignal { @@ -20,8 +21,7 @@ pub enum DownloadManagerSignal { Go, /// Pauses the DownloadManager Stop, - /// Called when a GameDownloadAgent has finished. - /// Triggers the next download cycle to begin + /// Called when a GameDownloadAgent has fully completed a download. Completed(String), /// Generates and appends a GameDownloadAgent /// to the registry and queue @@ -104,11 +104,10 @@ impl DownloadManager { ))?; self.command_sender.send(DownloadManagerSignal::Go) } - pub fn cancel_download( - &self, - game_id: String - ) { - self.command_sender.send(DownloadManagerSignal::Cancel(game_id)).unwrap(); + pub fn cancel_download(&self, game_id: String) { + self.command_sender + .send(DownloadManagerSignal::Cancel(game_id)) + .unwrap(); } pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { self.download_queue.edit() @@ -139,11 +138,13 @@ impl DownloadManager { let current_index = get_index_from_id(&mut queue, id).unwrap(); queue.remove(current_index); } - pub fn pause_downloads(&self) -> Result<(), SendError> { - self.command_sender.send(DownloadManagerSignal::Stop) + pub fn pause_downloads(&self) { + self.command_sender + .send(DownloadManagerSignal::Stop) + .unwrap(); } - pub fn resume_downloads(&self) -> Result<(), SendError> { - self.command_sender.send(DownloadManagerSignal::Go) + pub fn resume_downloads(&self) { + self.command_sender.send(DownloadManagerSignal::Go).unwrap(); } pub fn ensure_terminated(self) -> Result, Box> { self.command_sender diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index e696abf..656d299 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -170,15 +170,15 @@ impl DownloadManagerBuilder { fn manage_go_signal(&mut self) { info!("Got signal 'Go'"); - if self.active_control_flag.is_none() && !self.download_agent_registry.is_empty() { + if !self.download_agent_registry.is_empty() && !self.download_queue.empty() { info!("Starting download agent"); - let download_agent = { - let front = self.download_queue.read().front().unwrap().clone(); - self.download_agent_registry.get(&front.id).unwrap().clone() - }; - let download_agent_interface = - Arc::new(AgentInterfaceData::from(download_agent.clone())); - self.current_game_interface = Some(download_agent_interface); + let agent_data = self.download_queue.read().front().unwrap().clone(); + let download_agent = self + .download_agent_registry + .get(&agent_data.id) + .unwrap() + .clone(); + self.current_game_interface = Some(agent_data); let progress_object = download_agent.progress.clone(); *self.progress.lock().unwrap() = Some(progress_object); @@ -191,29 +191,20 @@ impl DownloadManagerBuilder { info!("Spawning download"); spawn(move || { match download_agent.download() { - Ok(_) => { - // TODO wrap this pattern in a macro - let result = sender - .send(DownloadManagerSignal::Completed(download_agent.id.clone())); - if let Err(err) = result { - error!("{}", err); - } - } + // Returns once we've exited the download + // (not necessarily completed) + // The download agent will fire the completed event for us + Ok(_) => {} + // If an error occurred while *starting* the download Err(err) => { - let result = sender.send(DownloadManagerSignal::Error(err)); - if let Err(err) = result { - error!("{}", err); - } + error!("error while managing download: {}", err); + sender.send(DownloadManagerSignal::Error(err)).unwrap(); } }; }); - info!("Finished spawning Download"); active_control_flag.set(DownloadThreadControlFlag::Go); self.set_status(DownloadManagerStatus::Downloading); - } else if let Some(active_control_flag) = self.active_control_flag.clone() { - info!("Restarting current download"); - active_control_flag.set(DownloadThreadControlFlag::Go); } else { info!("Nothing was set"); } @@ -230,6 +221,8 @@ impl DownloadManagerBuilder { self.active_control_flag = None; *self.progress.lock().unwrap() = None; } + // TODO wait until current download exits + self.download_agent_registry.remove(&game_id); let mut lock = self.download_queue.edit(); let index = match lock.iter().position(|interface| interface.id == game_id) { @@ -237,6 +230,8 @@ impl DownloadManagerBuilder { None => return, }; lock.remove(index); + + // Start next download self.sender.send(DownloadManagerSignal::Go).unwrap(); info!( "{:?}", diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs index 0848caf..9114003 100644 --- a/src-tauri/src/downloads/progress_object.rs +++ b/src-tauri/src/downloads/progress_object.rs @@ -1,13 +1,16 @@ -use std::{sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, Mutex, -}, time::Instant}; +use std::{ + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, + }, + time::Instant, +}; #[derive(Clone)] pub struct ProgressObject { max: Arc>, progress_instances: Arc>>>, - start: Arc> + start: Arc>, } impl ProgressObject { diff --git a/src-tauri/src/downloads/queue.rs b/src-tauri/src/downloads/queue.rs index 1ce695f..80e3dc0 100644 --- a/src-tauri/src/downloads/queue.rs +++ b/src-tauri/src/downloads/queue.rs @@ -1,19 +1,22 @@ -use std::{collections::VecDeque, sync::{Arc, Mutex, MutexGuard}}; +use std::{ + collections::VecDeque, + sync::{Arc, Mutex, MutexGuard}, +}; use super::download_manager::AgentInterfaceData; #[derive(Clone)] pub struct Queue { - inner: Arc>>> + inner: Arc>>>, } impl Queue { pub fn new() -> Self { Self { - inner: Arc::new(Mutex::new(VecDeque::new())) + inner: Arc::new(Mutex::new(VecDeque::new())), } } - pub fn read(&self) -> VecDeque> { + pub fn read(&self) -> VecDeque> { self.inner.lock().unwrap().clone() } pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { @@ -22,13 +25,15 @@ impl Queue { pub fn pop_front(&self) -> Option> { self.edit().pop_front() } + pub fn empty(&self) -> bool { + self.inner.lock().unwrap().len() == 0 + } /// Either inserts `interface` at the specified index, or appends to /// the back of the deque if index is greater than the length of the deque pub fn insert(&self, interface: AgentInterfaceData, index: usize) { if self.read().len() > index { self.append(interface); - } - else { + } else { self.edit().insert(index, Arc::new(interface)); } } @@ -44,7 +49,7 @@ impl Queue { if front.id == game_id { return queue.pop_front(); } - return None + return None; } pub fn get_by_id(&self, game_id: String) -> Option { self.read().iter().position(|data| data.id == game_id) @@ -61,4 +66,4 @@ impl Queue { self.edit().insert(new_index, existing); Ok(()) } -} \ No newline at end of file +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 840b803..8d1dbd2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,8 +15,8 @@ use db::{ DATA_ROOT_DIR, }; use downloads::download_commands::*; -use downloads::download_manager_builder::DownloadManagerBuilder; use downloads::download_manager::DownloadManager; +use downloads::download_manager_builder::DownloadManagerBuilder; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; use library::{fetch_game, fetch_game_status, fetch_library, Game}; @@ -137,7 +137,9 @@ pub fn run() { // Downloads download_game, get_current_game_download_progress, - stop_game_download + cancel_game_download, + pause_game_downloads, + resume_game_downloads, ]) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) From e4df4eb2d7144a0842955cd356f2d1c078788100 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Thu, 28 Nov 2024 20:31:04 +1100 Subject: [PATCH 127/164] feat(download manager): update db state with ui and emit events --- app.vue | 3 +- components/GameStatusButton.vue | 50 ++++++++ pages/library/[id]/index.vue | 11 +- src-tauri/src/db.rs | 1 + .../src/downloads/download_manager_builder.rs | 107 +++++++++++------- src-tauri/src/lib.rs | 14 ++- src-tauri/src/library.rs | 5 + types.d.ts => types.ts | 1 + 8 files changed, 144 insertions(+), 48 deletions(-) create mode 100644 components/GameStatusButton.vue rename types.d.ts => types.ts (97%) diff --git a/app.vue b/app.vue index 4218fe2..6ae77a5 100644 --- a/app.vue +++ b/app.vue @@ -6,8 +6,7 @@ diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index 94275ba..6ae50ac 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -18,7 +18,7 @@
- +
@@ -27,15 +27,22 @@ diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 66d08b5..b205405 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -25,6 +25,7 @@ pub struct DatabaseAuth { #[derive(Serialize, Clone, Deserialize)] pub enum DatabaseGameStatus { Remote, + Queued, Downloading, Installed, Updating, diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 656d299..5c4a723 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -8,6 +8,9 @@ use std::{ }; use log::{error, info, warn}; +use tauri::{AppHandle, Emitter}; + +use crate::{db::DatabaseGameStatus, library::GameUpdateEvent, DB}; use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, @@ -64,13 +67,14 @@ pub struct DownloadManagerBuilder { sender: Sender, progress: Arc>>, status: Arc>, + app_handle: AppHandle, current_game_interface: Option>, // Should be the only game download agent in the map with the "Go" flag active_control_flag: Option, } impl DownloadManagerBuilder { - pub fn build() -> DownloadManager { + pub fn build(app_handle: AppHandle) -> DownloadManager { let queue = Queue::new(); let (command_sender, command_receiver) = channel(); let active_progress = Arc::new(Mutex::new(None)); @@ -85,6 +89,7 @@ impl DownloadManagerBuilder { status: status.clone(), sender: command_sender.clone(), progress: active_progress.clone(), + app_handle, }; let terminator = spawn(|| manager.manage_queue()); @@ -92,6 +97,23 @@ impl DownloadManagerBuilder { DownloadManager::new(terminator, queue, active_progress, command_sender) } + fn set_game_status(&self, id: String, status: DatabaseGameStatus) { + let mut db_handle = DB.borrow_data_mut().unwrap(); + db_handle + .games + .games_statuses + .insert(id.clone(), status.clone()); + self.app_handle + .emit( + &format!("update_game/{}", id), + GameUpdateEvent { + game_id: id, + status: status, + }, + ) + .unwrap(); + } + fn manage_queue(mut self) -> Result<(), ()> { loop { let signal = match self.command_receiver.recv() { @@ -145,6 +167,8 @@ impl DownloadManagerBuilder { self.download_agent_registry.remove(&game_id); self.active_control_flag = None; *self.progress.lock().unwrap() = None; + + self.set_game_status(game_id, DatabaseGameStatus::Installed); } } self.sender.send(DownloadManagerSignal::Go).unwrap(); @@ -160,54 +184,61 @@ impl DownloadManagerBuilder { )); let agent_status = GameDownloadStatus::Uninitialised; let interface_data = AgentInterfaceData { - id, + id: id.clone(), status: Mutex::new(agent_status), }; self.download_agent_registry .insert(interface_data.id.clone(), download_agent); self.download_queue.append(interface_data); + + self.set_game_status(id, DatabaseGameStatus::Queued); } fn manage_go_signal(&mut self) { info!("Got signal 'Go'"); - if !self.download_agent_registry.is_empty() && !self.download_queue.empty() { - info!("Starting download agent"); - let agent_data = self.download_queue.read().front().unwrap().clone(); - let download_agent = self - .download_agent_registry - .get(&agent_data.id) - .unwrap() - .clone(); - self.current_game_interface = Some(agent_data); - let progress_object = download_agent.progress.clone(); - *self.progress.lock().unwrap() = Some(progress_object); - - let active_control_flag = download_agent.control_flag.clone(); - self.active_control_flag = Some(active_control_flag.clone()); - - let sender = self.sender.clone(); - - info!("Spawning download"); - spawn(move || { - match download_agent.download() { - // Returns once we've exited the download - // (not necessarily completed) - // The download agent will fire the completed event for us - Ok(_) => {} - // If an error occurred while *starting* the download - Err(err) => { - error!("error while managing download: {}", err); - sender.send(DownloadManagerSignal::Error(err)).unwrap(); - } - }; - }); - - active_control_flag.set(DownloadThreadControlFlag::Go); - self.set_status(DownloadManagerStatus::Downloading); - } else { - info!("Nothing was set"); + if !(!self.download_agent_registry.is_empty() && !self.download_queue.empty()) { + return; } + + info!("Starting download agent"); + let agent_data = self.download_queue.read().front().unwrap().clone(); + let download_agent = self + .download_agent_registry + .get(&agent_data.id) + .unwrap() + .clone(); + self.current_game_interface = Some(agent_data); + + let progress_object = download_agent.progress.clone(); + *self.progress.lock().unwrap() = Some(progress_object); + + let active_control_flag = download_agent.control_flag.clone(); + self.active_control_flag = Some(active_control_flag.clone()); + + let sender = self.sender.clone(); + + info!("Spawning download"); + spawn(move || { + match download_agent.download() { + // Returns once we've exited the download + // (not necessarily completed) + // The download agent will fire the completed event for us + Ok(_) => {} + // If an error occurred while *starting* the download + Err(err) => { + error!("error while managing download: {}", err); + sender.send(DownloadManagerSignal::Error(err)).unwrap(); + } + }; + }); + + active_control_flag.set(DownloadThreadControlFlag::Go); + self.set_status(DownloadManagerStatus::Downloading); + self.set_game_status( + self.current_game_interface.as_ref().unwrap().id.clone(), + DatabaseGameStatus::Downloading, + ); } fn manage_error_signal(&self, error: GameDownloadError) { let current_status = self.current_game_interface.clone().unwrap(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8d1dbd2..a245d3d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -28,6 +28,7 @@ use std::{ collections::HashMap, sync::{LazyLock, Mutex}, }; +use tauri::{AppHandle, Manager}; use tauri_plugin_deep_link::DeepLinkExt; #[derive(Clone, Copy, Serialize)] @@ -69,12 +70,12 @@ fn fetch_state(state: tauri::State<'_, Mutex>) -> Result AppState { +fn setup(handle: AppHandle) -> AppState { debug!("Starting env"); env_logger::Builder::from_env(Env::default().default_filter_or("info")).init(); let games = HashMap::new(); - let download_manager = Arc::new(DownloadManagerBuilder::build()); + let download_manager = Arc::new(DownloadManagerBuilder::build(handle)); debug!("Checking if database is set up"); let is_set_up = DB.database_is_set_up(); @@ -102,9 +103,6 @@ pub static DB: LazyLock = LazyLock::new(DatabaseInterface::se #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { - let state = setup(); - info!("initialized drop client"); - let mut builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init()); #[cfg(desktop)] @@ -117,7 +115,6 @@ pub fn run() { builder .plugin(tauri_plugin_deep_link::init()) - .manage(Mutex::new(state)) .invoke_handler(tauri::generate_handler![ // DB fetch_state, @@ -144,6 +141,11 @@ pub fn run() { .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .setup(|app| { + let handle = app.handle().clone(); + let state = setup(handle); + info!("initialized drop client"); + app.manage(Mutex::new(state)); + #[cfg(any(target_os = "linux", all(debug_assertions, windows)))] { use tauri_plugin_deep_link::DeepLinkExt; diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 64372b8..fe792a3 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -33,6 +33,11 @@ pub struct Game { m_cover_id: String, m_image_library: Vec, } +#[derive(serde::Serialize, Clone)] +pub struct GameUpdateEvent { + pub game_id: String, + pub status: DatabaseGameStatus, +} fn fetch_library_logic(app: AppHandle) -> Result { let base_url = DB.fetch_base_url(); diff --git a/types.d.ts b/types.ts similarity index 97% rename from types.d.ts rename to types.ts index b63d904..9b91f48 100644 --- a/types.d.ts +++ b/types.ts @@ -27,6 +27,7 @@ export enum AppStatus { export enum GameStatus { Remote = "Remote", + Queued = "Queued", Downloading = "Downloading", Installed = "Installed", Updating = "Updating", From b5568429f515f716710d183c538d8c6084953e87 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Wed, 4 Dec 2024 17:29:46 +1100 Subject: [PATCH 128/164] feat(download manager): syncs state to disk to persist across reboots --- src-tauri/src/downloads/download_manager_builder.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 5c4a723..822549c 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -103,6 +103,8 @@ impl DownloadManagerBuilder { .games .games_statuses .insert(id.clone(), status.clone()); + drop(db_handle); + DB.save().unwrap(); self.app_handle .emit( &format!("update_game/{}", id), From 8670bca834f491b33233db3a9331435d7a946578 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Fri, 6 Dec 2024 22:16:50 +1100 Subject: [PATCH 129/164] feat(install ui): ui to install games --- components/GameStatusButton.vue | 47 ++- components/LoadingButton.vue | 2 +- pages/library/[id]/index.vue | 332 +++++++++++++++++- pages/store/index.vue | 5 - src-tauri/src/downloads/download_agent.rs | 3 +- src-tauri/src/downloads/download_commands.rs | 3 +- src-tauri/src/downloads/download_logic.rs | 17 +- .../src/downloads/download_manager_builder.rs | 6 + src-tauri/src/downloads/manifest.rs | 1 + src-tauri/src/lib.rs | 3 +- src-tauri/src/library.rs | 42 +++ 11 files changed, 436 insertions(+), 25 deletions(-) diff --git a/components/GameStatusButton.vue b/components/GameStatusButton.vue index e23c314..a721442 100644 --- a/components/GameStatusButton.vue +++ b/components/GameStatusButton.vue @@ -1,29 +1,45 @@ diff --git a/components/LoadingButton.vue b/components/LoadingButton.vue index b61303f..ec0adfa 100644 --- a/components/LoadingButton.vue +++ b/components/LoadingButton.vue @@ -1,7 +1,7 @@ diff --git a/pages/store/index.vue b/pages/store/index.vue index 85d220b..696d67c 100644 --- a/pages/store/index.vue +++ b/pages/store/index.vue @@ -39,11 +39,6 @@ const versionName = ref(""); const progress = ref(0); async function startGameDownload() { - await invoke("download_game", { - gameId: gameId.value, - gameVersion: versionName.value, - }); - setInterval(() => { (async () => { const currentProgress = await invoke( diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 2e960e3..e6c630b 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -172,7 +172,6 @@ impl GameDownloadAgent { pub fn ensure_contexts(&self) -> Result<(), GameDownloadError> { let context_lock = self.contexts.lock().unwrap(); - info!("{:?} {}", context_lock, context_lock.is_empty()); if !context_lock.is_empty() { return Ok(()); } @@ -209,7 +208,7 @@ impl GameDownloadAgent { for (i, length) in chunk.lengths.iter().enumerate() { contexts.push(DropDownloadContext { file_name: raw_path.to_string(), - version: version.to_string(), + version: chunk.versionName.to_string(), offset: running_offset, index: i, game_id: game_id.to_string(), diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index c6f3c42..044a067 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -8,13 +8,14 @@ use crate::AppState; pub fn download_game( game_id: String, game_version: String, + install_dir: usize, state: tauri::State<'_, Mutex>, ) -> Result<(), String> { state .lock() .unwrap() .download_manager - .queue_game(game_id, game_version, 0) + .queue_game(game_id, game_version, install_dir) .map_err(|_| "An error occurred while communicating with the download manager.".to_string()) } diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index ed2645f..a290970 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -3,7 +3,7 @@ use crate::db::DatabaseImpls; use crate::downloads::manifest::DropDownloadContext; use crate::remote::RemoteAccessError; use crate::DB; -use log::info; +use log::{info, warn}; use md5::{Context, Digest}; use reqwest::blocking::Response; @@ -150,6 +150,13 @@ pub fn download_game_chunk( .send() .map_err(|e| GameDownloadError::Communication(e.into()))?; + if response.status() != 200 { + warn!("{}", response.text().unwrap()); + return Err(GameDownloadError::Communication( + RemoteAccessError::InvalidCodeError(400), + )); + } + let mut destination = DropWriter::new(ctx.path); if ctx.offset != 0 { @@ -160,7 +167,9 @@ pub fn download_game_chunk( let content_length = response.content_length(); if content_length.is_none() { - return Err(GameDownloadError::Communication(RemoteAccessError::InvalidResponse)); + return Err(GameDownloadError::Communication( + RemoteAccessError::InvalidResponse, + )); } let mut pipeline = DropDownloadPipeline::new( @@ -176,7 +185,9 @@ pub fn download_game_chunk( return Ok(false); }; - let checksum = pipeline.finish().map_err(|e| GameDownloadError::IoError(e))?; + let checksum = pipeline + .finish() + .map_err(|e| GameDownloadError::IoError(e))?; let res = hex::encode(checksum.0); if res != ctx.checksum { diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 822549c..e9990db 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -8,6 +8,7 @@ use std::{ }; use log::{error, info, warn}; +use rustbreak::Database; use tauri::{AppHandle, Emitter}; use crate::{db::DatabaseGameStatus, library::GameUpdateEvent, DB}; @@ -247,6 +248,11 @@ impl DownloadManagerBuilder { let mut lock = current_status.status.lock().unwrap(); *lock = GameDownloadStatus::Error; self.set_status(DownloadManagerStatus::Error(error)); + + self.set_game_status( + self.current_game_interface.as_ref().unwrap().id.clone(), + DatabaseGameStatus::Remote, + ); } fn manage_cancel_signal(&mut self, game_id: String) { if let Some(current_flag) = &self.active_control_flag { diff --git a/src-tauri/src/downloads/manifest.rs b/src-tauri/src/downloads/manifest.rs index 2c28ea5..d6cf8ec 100644 --- a/src-tauri/src/downloads/manifest.rs +++ b/src-tauri/src/downloads/manifest.rs @@ -9,6 +9,7 @@ pub struct DropChunk { pub ids: Vec, pub checksums: Vec, pub lengths: Vec, + pub versionName: String, } #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a245d3d..0d9a505 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -19,7 +19,7 @@ use downloads::download_manager::DownloadManager; use downloads::download_manager_builder::DownloadManagerBuilder; use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; -use library::{fetch_game, fetch_game_status, fetch_library, Game}; +use library::{fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, Game}; use log::{debug, info}; use remote::{gen_drop_url, use_remote, RemoteAccessError}; use serde::{Deserialize, Serialize}; @@ -131,6 +131,7 @@ pub fn run() { delete_download_dir, fetch_download_dir_stats, fetch_game_status, + fetch_game_verion_options, // Downloads download_game, get_current_game_download_progress, diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index fe792a3..0afb0bc 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -39,6 +39,19 @@ pub struct GameUpdateEvent { pub status: DatabaseGameStatus, } +// 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: String, + setup_command: String, + launch_command: String, + delta: bool, + // total_size: usize, +} + fn fetch_library_logic(app: AppHandle) -> Result { let base_url = DB.fetch_base_url(); let library_url = base_url.join("/api/v1/client/user/library")?; @@ -172,3 +185,32 @@ pub fn fetch_game_status(id: String) -> Result { return Ok(status); } + +fn fetch_game_verion_options_logic(game_id: String) -> Result, RemoteAccessError> { + let base_url = DB.fetch_base_url(); + + let endpoint = + base_url.join(format!("/api/v1/client/metadata/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 { + return Err(RemoteAccessError::InvalidCodeError( + response.status().into(), + )); + } + + let data = response.json::>()?; + + return Ok(data); +} + +#[tauri::command] +pub fn fetch_game_verion_options(game_id: String) -> Result, String> { + fetch_game_verion_options_logic(game_id).map_err(|e| e.to_string()) +} From de52dac0ab42bcd5c6e34fdc1bb66500b707e2b9 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sat, 7 Dec 2024 11:00:35 +1100 Subject: [PATCH 130/164] feat(download & db): combined db and download interface improvements --- components/GameStatusButton.vue | 72 +++++++++-------- pages/library/[id]/index.vue | 6 +- src-tauri/Cargo.lock | 17 ---- src-tauri/Cargo.toml | 12 +-- src-tauri/src/db.rs | 50 +++++++++--- src-tauri/src/downloads/download_agent.rs | 1 - .../src/downloads/download_manager_builder.rs | 19 +++-- src-tauri/src/lib.rs | 2 + src-tauri/src/library.rs | 79 +++++++++++++++++-- types.ts | 8 +- 10 files changed, 186 insertions(+), 80 deletions(-) diff --git a/components/GameStatusButton.vue b/components/GameStatusButton.vue index a721442..972de00 100644 --- a/components/GameStatusButton.vue +++ b/components/GameStatusButton.vue @@ -1,18 +1,18 @@ @@ -22,9 +22,10 @@ import { PlayIcon, QueueListIcon, TrashIcon, + WrenchIcon, } from "@heroicons/vue/20/solid"; import type { Component } from "vue"; -import { GameStatus } from "~/types.js"; +import { GameStatusEnum, type GameStatus } from "~/types.js"; const props = defineProps<{ status: GameStatus }>(); const emit = defineEmits<{ @@ -33,43 +34,48 @@ const emit = defineEmits<{ (e: "play"): void; }>(); -const styles: { [key in GameStatus]: string } = { - [GameStatus.Remote]: +const styles: { [key in GameStatusEnum]: string } = { + [GameStatusEnum.Remote]: "bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600", - [GameStatus.Queued]: + [GameStatusEnum.Queued]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700", - [GameStatus.Downloading]: + [GameStatusEnum.Downloading]: "bg-zinc-800 text-white hover:bg-zinc-700 focus-visible:outline-zinc-700", - [GameStatus.Installed]: + [GameStatusEnum.SetupRequired]: + "bg-yellow-600 text-white hover:bg-yellow-500 focus-visible:outline-yellow-600", + [GameStatusEnum.Installed]: "bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600", - [GameStatus.Updating]: "", - [GameStatus.Uninstalling]: "", + [GameStatusEnum.Updating]: "", + [GameStatusEnum.Uninstalling]: "", }; -const buttonNames: { [key in GameStatus]: string } = { - [GameStatus.Remote]: "Install", - [GameStatus.Queued]: "Queued", - [GameStatus.Downloading]: "Downloading", - [GameStatus.Installed]: "Play", - [GameStatus.Updating]: "Updating", - [GameStatus.Uninstalling]: "Uninstalling", +const buttonNames: { [key in GameStatusEnum]: string } = { + [GameStatusEnum.Remote]: "Install", + [GameStatusEnum.Queued]: "Queued", + [GameStatusEnum.Downloading]: "Downloading", + [GameStatusEnum.SetupRequired]: "Setup", + [GameStatusEnum.Installed]: "Play", + [GameStatusEnum.Updating]: "Updating", + [GameStatusEnum.Uninstalling]: "Uninstalling", }; -const buttonIcons: { [key in GameStatus]: Component } = { - [GameStatus.Remote]: ArrowDownTrayIcon, - [GameStatus.Queued]: QueueListIcon, - [GameStatus.Downloading]: ArrowDownTrayIcon, - [GameStatus.Installed]: PlayIcon, - [GameStatus.Updating]: ArrowDownTrayIcon, - [GameStatus.Uninstalling]: TrashIcon, +const buttonIcons: { [key in GameStatusEnum]: Component } = { + [GameStatusEnum.Remote]: ArrowDownTrayIcon, + [GameStatusEnum.Queued]: QueueListIcon, + [GameStatusEnum.Downloading]: ArrowDownTrayIcon, + [GameStatusEnum.SetupRequired]: WrenchIcon, + [GameStatusEnum.Installed]: PlayIcon, + [GameStatusEnum.Updating]: ArrowDownTrayIcon, + [GameStatusEnum.Uninstalling]: TrashIcon, }; -const buttonActions: { [key in GameStatus]: () => void } = { - [GameStatus.Remote]: () => emit("install"), - [GameStatus.Queued]: () => emit("cancel"), - [GameStatus.Downloading]: () => emit("cancel"), - [GameStatus.Installed]: () => emit("play"), - [GameStatus.Updating]: () => emit("cancel"), - [GameStatus.Uninstalling]: () => {}, +const buttonActions: { [key in GameStatusEnum]: () => void } = { + [GameStatusEnum.Remote]: () => emit("install"), + [GameStatusEnum.Queued]: () => emit("cancel"), + [GameStatusEnum.Downloading]: () => emit("cancel"), + [GameStatusEnum.SetupRequired]: () => {}, + [GameStatusEnum.Installed]: () => emit("play"), + [GameStatusEnum.Updating]: () => emit("cancel"), + [GameStatusEnum.Uninstalling]: () => {}, }; diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index b930d76..9d914ab 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -275,7 +275,11 @@ > , // Guaranteed to exist if the game also exists in the app state map pub games_statuses: HashMap, + pub game_versions: HashMap>, } #[derive(Serialize, Clone, Deserialize)] @@ -50,8 +65,22 @@ pub struct Database { pub static DATA_ROOT_DIR: LazyLock> = LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop"))); +// Custom JSON serializer to support everything we need +#[derive(Debug, Default, Clone)] +pub struct DropDatabaseSerializer; + +impl DeSerializer for DropDatabaseSerializer { + fn serialize(&self, val: &T) -> rustbreak::error::DeSerResult> { + serde_json::to_vec(val).map_err(|e| DeSerError::Internal(e.to_string())) + } + + fn deserialize(&self, s: R) -> rustbreak::error::DeSerResult { + serde_json::from_reader(s).map_err(|e| DeSerError::Internal(e.to_string())) + } +} + pub type DatabaseInterface = - rustbreak::Database; + rustbreak::Database; pub trait DatabaseImpls { fn set_up_database() -> DatabaseInterface; @@ -80,6 +109,7 @@ impl DatabaseImpls for DatabaseInterface { games: DatabaseGames { install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], games_statuses: HashMap::new(), + game_versions: HashMap::new(), }, }; debug!("Creating database at path {}", db_path.as_str().unwrap()); diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index e6c630b..2b3c26a 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -187,7 +187,6 @@ impl GameDownloadAgent { drop(db_lock); let manifest = self.manifest.lock().unwrap().clone().unwrap(); - let version = self.version.clone(); let game_id = self.id.clone(); let data_base_dir_path = Path::new(&data_base_dir); diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index e9990db..a1578a5 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -11,7 +11,11 @@ use log::{error, info, warn}; use rustbreak::Database; use tauri::{AppHandle, Emitter}; -use crate::{db::DatabaseGameStatus, library::GameUpdateEvent, DB}; +use crate::{ + db::DatabaseGameStatus, + library::{on_game_complete, GameUpdateEvent}, + DB, +}; use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, @@ -167,11 +171,11 @@ impl DownloadManagerBuilder { if interface.id == game_id { info!("Popping consumed data"); self.download_queue.pop_front(); - self.download_agent_registry.remove(&game_id); + let download_agent = self.download_agent_registry.remove(&game_id).unwrap(); self.active_control_flag = None; *self.progress.lock().unwrap() = None; - self.set_game_status(game_id, DatabaseGameStatus::Installed); + on_game_complete(game_id, download_agent.version.clone(), &self.app_handle); } } self.sender.send(DownloadManagerSignal::Go).unwrap(); @@ -190,11 +194,12 @@ impl DownloadManagerBuilder { id: id.clone(), status: Mutex::new(agent_status), }; + let version_name = download_agent.version.clone(); self.download_agent_registry .insert(interface_data.id.clone(), download_agent); self.download_queue.append(interface_data); - self.set_game_status(id, DatabaseGameStatus::Queued); + self.set_game_status(id, DatabaseGameStatus::Queued { version_name }); } fn manage_go_signal(&mut self) { @@ -213,6 +218,8 @@ impl DownloadManagerBuilder { .clone(); self.current_game_interface = Some(agent_data); + let version_name = download_agent.version.clone(); + let progress_object = download_agent.progress.clone(); *self.progress.lock().unwrap() = Some(progress_object); @@ -240,7 +247,7 @@ impl DownloadManagerBuilder { self.set_status(DownloadManagerStatus::Downloading); self.set_game_status( self.current_game_interface.as_ref().unwrap().id.clone(), - DatabaseGameStatus::Downloading, + DatabaseGameStatus::Downloading { version_name }, ); } fn manage_error_signal(&self, error: GameDownloadError) { @@ -251,7 +258,7 @@ impl DownloadManagerBuilder { self.set_game_status( self.current_game_interface.as_ref().unwrap().id.clone(), - DatabaseGameStatus::Remote, + DatabaseGameStatus::Remote {}, ); } fn manage_cancel_signal(&mut self, game_id: String) { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0d9a505..ef30788 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(map_try_insert)] + mod auth; mod db; mod downloads; diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 0afb0bc..041cf2e 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -1,14 +1,17 @@ +use std::collections::HashMap; use std::fmt::format; use std::sync::Mutex; use log::info; use serde::{Deserialize, Serialize}; use serde_json::json; +use tauri::Emitter; use tauri::{AppHandle, Manager}; +use urlencoding::encode; -use crate::db; use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; +use crate::db::{self, GameVersion}; use crate::downloads::download_manager::GameDownloadStatus; use crate::remote::RemoteAccessError; use crate::{auth::generate_authorization_header, AppState, DB}; @@ -68,7 +71,7 @@ fn fetch_library_logic(app: AppHandle) -> Result { return Err(response.status().as_u16().into()); } - let games = response.json::>()?; + let games: Vec = response.json::>()?; let state = app.state::>(); let mut handle = state.lock().unwrap(); @@ -81,7 +84,7 @@ fn fetch_library_logic(app: AppHandle) -> Result { db_handle .games .games_statuses - .insert(game.id.clone(), DatabaseGameStatus::Remote); + .insert(game.id.clone(), DatabaseGameStatus::Remote {}); } } @@ -145,7 +148,7 @@ fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result Result { .games .games_statuses .get(&id) - .unwrap_or(&DatabaseGameStatus::Remote) + .unwrap_or(&DatabaseGameStatus::Remote {}) .clone(); drop(db_handle); return Ok(status); } -fn fetch_game_verion_options_logic(game_id: String) -> Result, RemoteAccessError> { +fn fetch_game_verion_options_logic( + game_id: String, +) -> Result, RemoteAccessError> { let base_url = DB.fetch_base_url(); let endpoint = @@ -214,3 +219,65 @@ fn fetch_game_verion_options_logic(game_id: String) -> Result Result, String> { fetch_game_verion_options_logic(game_id).map_err(|e| e.to_string()) } + +pub fn on_game_complete( + game_id: String, + version_name: String, + app_handle: &AppHandle, +) -> Result<(), RemoteAccessError> { + // Fetch game version information from remote + let base_url = DB.fetch_base_url(); + + let endpoint = base_url.join( + format!( + "/api/v1/client/metadata/version?id={}&version={}", + game_id, + encode(&version_name) + ) + .as_str(), + )?; + let header = generate_authorization_header(); + + let client = reqwest::blocking::Client::new(); + let response = client + .get(endpoint.to_string()) + .header("Authorization", header) + .send()?; + + let data = response.json::()?; + + let mut handle = DB.borrow_data_mut().unwrap(); + handle + .games + .game_versions + .entry(game_id.clone()) + .or_insert(HashMap::new()) + .insert(version_name.clone(), data.clone()); + drop(handle); + DB.save().unwrap(); + + let status = if data.setup_command.is_empty() { + DatabaseGameStatus::Installed { version_name } + } else { + DatabaseGameStatus::SetupRequired { version_name } + }; + + let mut db_handle = DB.borrow_data_mut().unwrap(); + db_handle + .games + .games_statuses + .insert(game_id.clone(), status.clone()); + drop(db_handle); + DB.save().unwrap(); + app_handle + .emit( + &format!("update_game/{}", game_id), + GameUpdateEvent { + game_id: game_id, + status: status, + }, + ) + .unwrap(); + + Ok(()) +} diff --git a/types.ts b/types.ts index 9b91f48..3a6e3f4 100644 --- a/types.ts +++ b/types.ts @@ -25,11 +25,17 @@ export enum AppStatus { ServerUnavailable = "ServerUnavailable", } -export enum GameStatus { +export enum GameStatusEnum { Remote = "Remote", Queued = "Queued", Downloading = "Downloading", Installed = "Installed", Updating = "Updating", Uninstalling = "Uninstalling", + SetupRequired = "SetupRequired", } + +export type GameStatus = { + type: GameStatusEnum; + version_name?: string; +}; From 5cbeb3bdb6da2118e0194c995fd8625bc7ba705a Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sat, 7 Dec 2024 20:21:22 +1100 Subject: [PATCH 131/164] feat: temporary queue ui and flamegraph instructions --- .gitignore | 5 +- DEBUG.md | 15 +++++ app.vue | 2 + composables/queue.ts | 13 ++++ layouts/default.vue | 6 ++ src-tauri/src/downloads/download_agent.rs | 10 +-- src-tauri/src/downloads/download_logic.rs | 12 ++-- src-tauri/src/downloads/download_manager.rs | 26 +++++--- .../src/downloads/download_manager_builder.rs | 60 ++++++++++++++--- src-tauri/src/downloads/progress_object.rs | 64 ++++++++++++++++++- src-tauri/src/downloads/queue.rs | 16 ++--- src-tauri/src/library.rs | 14 +++- 12 files changed, 201 insertions(+), 42 deletions(-) create mode 100644 DEBUG.md create mode 100644 composables/queue.ts diff --git a/.gitignore b/.gitignore index 2f4d822..8e8efd6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ dist-ssr *.sln *.sw? .nuxt -.output \ No newline at end of file +.output + +src-tauri/flamegraph.svg +src-tuair/perf* \ No newline at end of file diff --git a/DEBUG.md b/DEBUG.md new file mode 100644 index 0000000..2a4478f --- /dev/null +++ b/DEBUG.md @@ -0,0 +1,15 @@ +# How to create Flamegraph + +Run this in `src-tauri`: +``` +WEBKIT_DISABLE_DMABUF_RENDERER=1 CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --release +``` + +You can leave out `WEBKIT_DISABLE_DMABUF_RENDERER=1` if you're not on NVIDIA/Linux + +And then run this in the root dir: +``` +yarn dev --port 1432 +``` + +And then do what you want, and it'll create the flamegraph for you diff --git a/app.vue b/app.vue index 6ae77a5..a5c6584 100644 --- a/app.vue +++ b/app.vue @@ -5,6 +5,8 @@ diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 2b3c26a..0b429d1 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -1,6 +1,7 @@ use crate::auth::generate_authorization_header; use crate::db::DatabaseImpls; use crate::downloads::manifest::{DropDownloadContext, DropManifest}; +use crate::downloads::progress_object::ProgressHandle; use crate::remote::RemoteAccessError; use crate::DB; use log::{debug, error, info}; @@ -28,7 +29,7 @@ pub struct GameDownloadAgent { pub target_download_dir: usize, contexts: Mutex>, pub manifest: Mutex>, - pub progress: ProgressObject, + pub progress: Arc, sender: Sender, } @@ -76,7 +77,7 @@ impl GameDownloadAgent { manifest: Mutex::new(None), target_download_dir, contexts: Mutex::new(Vec::new()), - progress: ProgressObject::new(0, 0), + progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), sender, } } @@ -234,7 +235,7 @@ impl GameDownloadAgent { pub fn run(&self) -> Result<(), ()> { info!("downloading game: {}", self.id); - const DOWNLOAD_MAX_THREADS: usize = 4; + const DOWNLOAD_MAX_THREADS: usize = 1; let pool = ThreadPoolBuilder::new() .num_threads(DOWNLOAD_MAX_THREADS) @@ -251,10 +252,11 @@ impl GameDownloadAgent { let context = context.clone(); let control_flag = self.control_flag.clone(); // Clone arcs let progress = self.progress.get(index); // Clone arcs + let progress_handle = ProgressHandle::new(progress, self.progress.clone()); let completed_indexes_ref = completed_indexes_loop_arc.clone(); scope.spawn(move |_| { - match download_game_chunk(context.clone(), control_flag, progress) { + match download_game_chunk(context.clone(), control_flag, progress_handle) { Ok(res) => match res { true => { let mut lock = completed_indexes_ref.lock().unwrap(); diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index a290970..ff0bba6 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -19,6 +19,7 @@ use urlencoding::encode; use super::download_agent::GameDownloadError; use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; +use super::progress_object::{ProgressHandle, ProgressObject}; pub struct DropWriter { hasher: Context, @@ -65,7 +66,7 @@ pub struct DropDownloadPipeline { pub source: R, pub destination: DropWriter, pub control_flag: DownloadThreadControl, - pub progress: Arc, + pub progress: ProgressHandle, pub size: usize, } impl DropDownloadPipeline { @@ -73,7 +74,7 @@ impl DropDownloadPipeline { source: Response, destination: DropWriter, control_flag: DownloadThreadControl, - progress: Arc, + progress: ProgressHandle, size: usize, ) -> Self { Self { @@ -100,8 +101,7 @@ impl DropDownloadPipeline { current_size += bytes_read; buf_writer.write_all(©_buf[0..bytes_read])?; - self.progress - .fetch_add(bytes_read, std::sync::atomic::Ordering::Relaxed); + self.progress.add(bytes_read); if current_size == self.size { break; @@ -120,11 +120,11 @@ impl DropDownloadPipeline { pub fn download_game_chunk( ctx: DropDownloadContext, control_flag: DownloadThreadControl, - progress: Arc, + progress: ProgressHandle, ) -> Result { // If we're paused if control_flag.get() == DownloadThreadControlFlag::Stop { - progress.store(0, Ordering::Relaxed); + progress.set(0); return Ok(false); } diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index f57c87c..e70d53d 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -9,9 +9,11 @@ use std::{ }; use log::info; +use serde::Serialize; use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, + download_manager_builder::CurrentProgressObject, progress_object::ProgressObject, queue::Queue, }; @@ -32,6 +34,8 @@ pub enum DownloadManagerSignal { Cancel(String), /// Any error which occurs in the agent Error(GameDownloadError), + /// Pushes UI update + Update, } pub enum DownloadManagerStatus { Downloading, @@ -39,10 +43,12 @@ pub enum DownloadManagerStatus { Empty, Error(GameDownloadError), } + +#[derive(Serialize, Clone)] pub enum GameDownloadStatus { + Queued, Downloading, Paused, - Uninitialised, Error, } @@ -59,18 +65,20 @@ pub enum GameDownloadStatus { pub struct DownloadManager { terminator: JoinHandle>, download_queue: Queue, - progress: Arc>>, + progress: CurrentProgressObject, command_sender: Sender, } -pub struct AgentInterfaceData { +pub struct GameDownloadAgentQueueStandin { pub id: String, pub status: Mutex, + pub progress: Arc, } -impl From> for AgentInterfaceData { +impl From> for GameDownloadAgentQueueStandin { fn from(value: Arc) -> Self { Self { id: value.id.clone(), - status: Mutex::from(GameDownloadStatus::Uninitialised), + status: Mutex::from(GameDownloadStatus::Queued), + progress: value.progress.clone(), } } } @@ -79,7 +87,7 @@ impl DownloadManager { pub fn new( terminator: JoinHandle>, download_queue: Queue, - progress: Arc>>, + progress: CurrentProgressObject, command_sender: Sender, ) -> Self { Self { @@ -109,10 +117,10 @@ impl DownloadManager { .send(DownloadManagerSignal::Cancel(game_id)) .unwrap(); } - pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { + pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { self.download_queue.edit() } - pub fn read_queue(&self) -> VecDeque> { + pub fn read_queue(&self) -> VecDeque> { self.download_queue.read() } pub fn get_current_game_download_progress(&self) -> Option { @@ -157,7 +165,7 @@ impl DownloadManager { /// Takes in the locked value from .edit() and attempts to /// get the index of whatever game_id is passed in fn get_index_from_id( - queue: &mut MutexGuard<'_, VecDeque>>, + queue: &mut MutexGuard<'_, VecDeque>>, id: String, ) -> Option { queue diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index a1578a5..4809707 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -7,21 +7,20 @@ use std::{ thread::spawn, }; -use log::{error, info, warn}; -use rustbreak::Database; +use log::{error, info}; use tauri::{AppHandle, Emitter}; use crate::{ db::DatabaseGameStatus, - library::{on_game_complete, GameUpdateEvent}, + library::{on_game_complete, GameUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData}, DB, }; use super::{ download_agent::{GameDownloadAgent, GameDownloadError}, download_manager::{ - AgentInterfaceData, DownloadManager, DownloadManagerSignal, DownloadManagerStatus, - GameDownloadStatus, + DownloadManager, DownloadManagerSignal, DownloadManagerStatus, + GameDownloadAgentQueueStandin, GameDownloadStatus, }, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, progress_object::ProgressObject, @@ -65,16 +64,19 @@ Behold, my madness - quexeky */ +// Refactored to consolidate this type. It's a monster. +pub type CurrentProgressObject = Arc>>>; + pub struct DownloadManagerBuilder { download_agent_registry: HashMap>, download_queue: Queue, command_receiver: Receiver, sender: Sender, - progress: Arc>>, + progress: CurrentProgressObject, status: Arc>, app_handle: AppHandle, - current_game_interface: Option>, // Should be the only game download agent in the map with the "Go" flag + current_game_interface: Option>, // Should be the only game download agent in the map with the "Go" flag active_control_flag: Option, } @@ -121,6 +123,21 @@ impl DownloadManagerBuilder { .unwrap(); } + fn push_manager_update(&self) { + let queue = self.download_queue.read(); + let queue_objs: Vec = queue + .iter() + .map(|interface| QueueUpdateEventQueueData { + id: interface.id.clone(), + status: interface.status.lock().unwrap().clone(), + progress: interface.progress.get_progress(), + }) + .collect(); + + let event_data = QueueUpdateEvent { queue: queue_objs }; + self.app_handle.emit("update_queue", event_data).unwrap(); + } + fn manage_queue(mut self) -> Result<(), ()> { loop { let signal = match self.command_receiver.recv() { @@ -153,6 +170,9 @@ impl DownloadManagerBuilder { DownloadManagerSignal::Cancel(id) => { self.manage_cancel_signal(id); } + DownloadManagerSignal::Update => { + self.push_manager_update(); + } }; } } @@ -175,9 +195,18 @@ impl DownloadManagerBuilder { self.active_control_flag = None; *self.progress.lock().unwrap() = None; - on_game_complete(game_id, download_agent.version.clone(), &self.app_handle); + if let Err(error) = + on_game_complete(game_id, download_agent.version.clone(), &self.app_handle) + { + self.sender + .send(DownloadManagerSignal::Error( + GameDownloadError::Communication(error), + )) + .unwrap(); + } } } + self.sender.send(DownloadManagerSignal::Update).unwrap(); self.sender.send(DownloadManagerSignal::Go).unwrap(); } @@ -189,10 +218,11 @@ impl DownloadManagerBuilder { target_download_dir, self.sender.clone(), )); - let agent_status = GameDownloadStatus::Uninitialised; - let interface_data = AgentInterfaceData { + let agent_status = GameDownloadStatus::Queued; + let interface_data = GameDownloadAgentQueueStandin { id: id.clone(), status: Mutex::new(agent_status), + progress: download_agent.progress.clone(), }; let version_name = download_agent.version.clone(); self.download_agent_registry @@ -200,6 +230,7 @@ impl DownloadManagerBuilder { self.download_queue.append(interface_data); self.set_game_status(id, DatabaseGameStatus::Queued { version_name }); + self.sender.send(DownloadManagerSignal::Update).unwrap(); } fn manage_go_signal(&mut self) { @@ -217,6 +248,8 @@ impl DownloadManagerBuilder { .unwrap() .clone(); self.current_game_interface = Some(agent_data); + // Cloning option should be okay because it only clones the Arc inside, not the AgentInterfaceData + let agent_data = self.current_game_interface.clone().unwrap(); let version_name = download_agent.version.clone(); @@ -243,6 +276,11 @@ impl DownloadManagerBuilder { }; }); + // Set status for game + let mut status_handle = agent_data.status.lock().unwrap(); + *status_handle = GameDownloadStatus::Downloading; + + // Set flags for download manager active_control_flag.set(DownloadThreadControlFlag::Go); self.set_status(DownloadManagerStatus::Downloading); self.set_game_status( @@ -260,6 +298,8 @@ impl DownloadManagerBuilder { self.current_game_interface.as_ref().unwrap().id.clone(), DatabaseGameStatus::Remote {}, ); + + self.sender.send(DownloadManagerSignal::Update).unwrap(); } fn manage_cancel_signal(&mut self, game_id: String) { if let Some(current_flag) = &self.active_control_flag { diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs index 9114003..13b6592 100644 --- a/src-tauri/src/downloads/progress_object.rs +++ b/src-tauri/src/downloads/progress_object.rs @@ -1,27 +1,84 @@ use std::{ sync::{ atomic::{AtomicUsize, Ordering}, + mpsc::Sender, Arc, Mutex, }, time::Instant, }; +use log::info; + +use super::download_manager::DownloadManagerSignal; + #[derive(Clone)] pub struct ProgressObject { max: Arc>, progress_instances: Arc>>>, start: Arc>, + sender: Sender, + + points_towards_update: Arc, + points_to_push_update: Arc>, } +pub struct ProgressHandle { + progress: Arc, + progress_object: Arc, +} + +impl ProgressHandle { + pub fn new(progress: Arc, progress_object: Arc) -> Self { + Self { + progress, + progress_object, + } + } + pub fn set(&self, amount: usize) { + self.progress.store(amount, Ordering::Relaxed); + } + pub fn add(&self, amount: usize) { + self.progress + .fetch_add(amount, std::sync::atomic::Ordering::Relaxed); + self.progress_object.check_push_update(amount); + } +} + +static PROGRESS_UPDATES: usize = 100; + impl ProgressObject { - pub fn new(max: usize, length: usize) -> Self { + 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 calculate 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, + + points_towards_update: Arc::new(AtomicUsize::new(0)), + points_to_push_update: Arc::new(Mutex::new(points_to_push_update)), } } + + 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_handle = self.points_to_push_update.lock().unwrap(); + let to_update = to_update_handle.clone(); + drop(to_update_handle); + + if current_amount < to_update { + return; + } + self.points_towards_update + .fetch_sub(to_update, Ordering::Relaxed); + self.sender.send(DownloadManagerSignal::Update).unwrap(); + } + pub fn set_time_now(&self) { *self.start.lock().unwrap() = Instant::now(); } @@ -37,13 +94,14 @@ impl ProgressObject { *self.max.lock().unwrap() } pub fn set_max(&self, new_max: usize) { - *self.max.lock().unwrap() = new_max + *self.max.lock().unwrap() = new_max; + *self.points_to_push_update.lock().unwrap() = new_max / PROGRESS_UPDATES; + info!("points to push update: {}", new_max / PROGRESS_UPDATES); } pub fn set_size(&self, length: usize) { *self.progress_instances.lock().unwrap() = (0..length).map(|_| Arc::new(AtomicUsize::new(0))).collect(); } - pub fn get_progress(&self) -> f64 { self.sum() as f64 / self.get_max() as f64 } diff --git a/src-tauri/src/downloads/queue.rs b/src-tauri/src/downloads/queue.rs index 80e3dc0..f4139ac 100644 --- a/src-tauri/src/downloads/queue.rs +++ b/src-tauri/src/downloads/queue.rs @@ -3,11 +3,11 @@ use std::{ sync::{Arc, Mutex, MutexGuard}, }; -use super::download_manager::AgentInterfaceData; +use super::download_manager::GameDownloadAgentQueueStandin; #[derive(Clone)] pub struct Queue { - inner: Arc>>>, + inner: Arc>>>, } impl Queue { @@ -16,13 +16,13 @@ impl Queue { inner: Arc::new(Mutex::new(VecDeque::new())), } } - pub fn read(&self) -> VecDeque> { + pub fn read(&self) -> VecDeque> { self.inner.lock().unwrap().clone() } - pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { + pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { self.inner.lock().unwrap() } - pub fn pop_front(&self) -> Option> { + pub fn pop_front(&self) -> Option> { self.edit().pop_front() } pub fn empty(&self) -> bool { @@ -30,17 +30,17 @@ impl Queue { } /// Either inserts `interface` at the specified index, or appends to /// the back of the deque if index is greater than the length of the deque - pub fn insert(&self, interface: AgentInterfaceData, index: usize) { + pub fn insert(&self, interface: GameDownloadAgentQueueStandin, index: usize) { if self.read().len() > index { self.append(interface); } else { self.edit().insert(index, Arc::new(interface)); } } - pub fn append(&self, interface: AgentInterfaceData) { + pub fn append(&self, interface: GameDownloadAgentQueueStandin) { self.edit().push_back(Arc::new(interface)); } - pub fn pop_front_if_equal(&self, game_id: String) -> Option> { + pub fn pop_front_if_equal(&self, game_id: String) -> Option> { let mut queue = self.edit(); let front = match queue.front() { Some(front) => front, diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 041cf2e..9b38c5d 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use std::fmt::format; use std::sync::Mutex; @@ -42,6 +42,18 @@ pub struct GameUpdateEvent { pub status: DatabaseGameStatus, } +#[derive(Serialize, Clone)] +pub struct QueueUpdateEventQueueData { + pub id: String, + pub status: GameDownloadStatus, + pub progress: f64, +} + +#[derive(serde::Serialize, Clone)] +pub struct QueueUpdateEvent { + pub queue: Vec, +} + // Game version with some fields missing and size information #[derive(serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] From 532d13e96f8de967453e691e8a343d62967faf8b Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sun, 8 Dec 2024 12:33:45 +1100 Subject: [PATCH 132/164] feat(download widget): download widget and queue fix --- .gitignore | 2 +- components/Header.vue | 46 +++++++++-------- components/HeaderQueueWidget.vue | 17 +++++++ composables/queue.ts | 2 +- layouts/default.vue | 2 - pages/library/[id]/index.vue | 2 +- src-tauri/src/downloads/download_logic.rs | 6 ++- .../src/downloads/download_manager_builder.rs | 51 ++++++++++++------- src-tauri/src/downloads/progress_object.rs | 2 +- 9 files changed, 85 insertions(+), 45 deletions(-) create mode 100644 components/HeaderQueueWidget.vue diff --git a/.gitignore b/.gitignore index 8e8efd6..5767a3f 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,4 @@ dist-ssr .output src-tauri/flamegraph.svg -src-tuair/perf* \ No newline at end of file +src-tauri/perf* \ No newline at end of file diff --git a/components/Header.vue b/components/Header.vue index 33ee506..e91edf5 100644 --- a/components/Header.vue +++ b/components/Header.vue @@ -1,53 +1,58 @@ diff --git a/components/HeaderQueueWidget.vue b/components/HeaderQueueWidget.vue new file mode 100644 index 0000000..105d14e --- /dev/null +++ b/components/HeaderQueueWidget.vue @@ -0,0 +1,17 @@ + + + diff --git a/composables/queue.ts b/composables/queue.ts index 931d330..f684852 100644 --- a/composables/queue.ts +++ b/composables/queue.ts @@ -1,7 +1,7 @@ import { listen } from "@tauri-apps/api/event"; export type QueueState = { - queue: Array<{ id: string; status: string }>; + queue: Array<{ id: string; status: string, progress: number }>; }; export const useQueueState = () => diff --git a/layouts/default.vue b/layouts/default.vue index af9826c..89dd49b 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -2,8 +2,6 @@
- {{ queueState }} -
diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index 9d914ab..1ede123 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -227,7 +227,7 @@ : 'font-normal', 'block truncate', ]" - >{{ dir }}}{{ dir }} { // 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() } } @@ -185,6 +187,7 @@ pub fn download_game_chunk( return Ok(false); }; + /* let checksum = pipeline .finish() .map_err(|e| GameDownloadError::IoError(e))?; @@ -193,6 +196,7 @@ pub fn download_game_chunk( if res != ctx.checksum { return Err(GameDownloadError::Checksum); } + */ Ok(true) } diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 4809707..fa3e13f 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -76,7 +76,7 @@ pub struct DownloadManagerBuilder { status: Arc>, app_handle: AppHandle, - current_game_interface: Option>, // Should be the only game download agent in the map with the "Go" flag + current_download_agent: Option>, // Should be the only game download agent in the map with the "Go" flag active_control_flag: Option, } @@ -91,7 +91,7 @@ impl DownloadManagerBuilder { download_agent_registry: HashMap::new(), download_queue: queue.clone(), command_receiver, - current_game_interface: None, + current_download_agent: None, active_control_flag: None, status: status.clone(), sender: command_sender.clone(), @@ -138,6 +138,16 @@ impl DownloadManagerBuilder { self.app_handle.emit("update_queue", event_data).unwrap(); } + fn cleanup_current_game(&mut self, game_id: &String) -> Arc { + self.download_queue.pop_front(); + let download_agent = self.download_agent_registry.remove(game_id).unwrap(); + self.active_control_flag = None; + *self.progress.lock().unwrap() = None; + self.current_download_agent = None; + + return download_agent; + } + fn manage_queue(mut self) -> Result<(), ()> { loop { let signal = match self.command_receiver.recv() { @@ -186,14 +196,11 @@ impl DownloadManagerBuilder { fn manage_completed_signal(&mut self, game_id: String) { info!("Got signal 'Completed'"); - if let Some(interface) = &self.current_game_interface { + if let Some(interface) = &self.current_download_agent { // When if let chains are stabilised, combine these two statements if interface.id == game_id { info!("Popping consumed data"); - self.download_queue.pop_front(); - let download_agent = self.download_agent_registry.remove(&game_id).unwrap(); - self.active_control_flag = None; - *self.progress.lock().unwrap() = None; + let download_agent = self.cleanup_current_game(&game_id); if let Err(error) = on_game_complete(game_id, download_agent.version.clone(), &self.app_handle) @@ -240,6 +247,11 @@ impl DownloadManagerBuilder { return; } + if self.current_download_agent.is_some() { + info!("skipping go signal due to existing download job"); + return; + } + info!("Starting download agent"); let agent_data = self.download_queue.read().front().unwrap().clone(); let download_agent = self @@ -247,9 +259,9 @@ impl DownloadManagerBuilder { .get(&agent_data.id) .unwrap() .clone(); - self.current_game_interface = Some(agent_data); + self.current_download_agent = Some(agent_data); // Cloning option should be okay because it only clones the Arc inside, not the AgentInterfaceData - let agent_data = self.current_game_interface.clone().unwrap(); + let agent_data = self.current_download_agent.clone().unwrap(); let version_name = download_agent.version.clone(); @@ -284,31 +296,34 @@ impl DownloadManagerBuilder { active_control_flag.set(DownloadThreadControlFlag::Go); self.set_status(DownloadManagerStatus::Downloading); self.set_game_status( - self.current_game_interface.as_ref().unwrap().id.clone(), + self.current_download_agent.as_ref().unwrap().id.clone(), DatabaseGameStatus::Downloading { version_name }, ); } - fn manage_error_signal(&self, error: GameDownloadError) { - let current_status = self.current_game_interface.clone().unwrap(); + fn manage_error_signal(&mut self, error: GameDownloadError) { + let current_status = self.current_download_agent.clone().unwrap(); + + self.cleanup_current_game(¤t_status.id); // Remove all the locks and shit + let mut lock = current_status.status.lock().unwrap(); *lock = GameDownloadStatus::Error; self.set_status(DownloadManagerStatus::Error(error)); - self.set_game_status( - self.current_game_interface.as_ref().unwrap().id.clone(), - DatabaseGameStatus::Remote {}, - ); + let game_id = self.current_download_agent.as_ref().unwrap().id.clone(); + self.set_game_status(game_id, DatabaseGameStatus::Remote {}); self.sender.send(DownloadManagerSignal::Update).unwrap(); } fn manage_cancel_signal(&mut self, game_id: String) { if let Some(current_flag) = &self.active_control_flag { current_flag.set(DownloadThreadControlFlag::Stop); - self.active_control_flag = None; - *self.progress.lock().unwrap() = None; } // TODO wait until current download exits + // This cleanup function might break things because it + // unsets the control flag + self.cleanup_current_game(&game_id); + self.download_agent_registry.remove(&game_id); let mut lock = self.download_queue.edit(); let index = match lock.iter().position(|interface| interface.id == game_id) { diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs index 13b6592..fac9b9b 100644 --- a/src-tauri/src/downloads/progress_object.rs +++ b/src-tauri/src/downloads/progress_object.rs @@ -49,7 +49,7 @@ 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 calculate with the set_max function below + // 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)), From d5ac1b0a0e54d7707d2a3a5a3dd1303e9a9e0744 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sun, 8 Dec 2024 12:55:41 +1100 Subject: [PATCH 133/164] fix: remove unnecessary unstable feature --- src-tauri/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ef30788..0d9a505 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,3 @@ -#![feature(map_try_insert)] - mod auth; mod db; mod downloads; From 671d45fbe4e20a34b10de3fa41eb97cf97ef0de4 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 9 Dec 2024 17:03:48 +1100 Subject: [PATCH 134/164] feat(download ui): debug queue interface --- components/Header.vue | 1 - components/HeaderQueueWidget.vue | 25 +++- composables/game.ts | 31 ++++ composables/queue.ts | 2 +- package.json | 5 +- pages/library.vue | 6 +- pages/library/[id]/index.vue | 18 +-- pages/queue.vue | 24 +++ plugins/vuedraggable.ts | 5 + prisma/schema.prisma | 150 ------------------- src-tauri/src/downloads/download_commands.rs | 28 ++-- src-tauri/src/downloads/download_manager.rs | 4 + src-tauri/src/lib.rs | 2 +- src-tauri/src/library.rs | 19 ++- types.ts | 21 ++- yarn.lock | 64 ++------ 16 files changed, 148 insertions(+), 257 deletions(-) create mode 100644 composables/game.ts create mode 100644 pages/queue.vue create mode 100644 plugins/vuedraggable.ts delete mode 100644 prisma/schema.prisma diff --git a/components/Header.vue b/components/Header.vue index e91edf5..75537eb 100644 --- a/components/Header.vue +++ b/components/Header.vue @@ -29,7 +29,6 @@
  1. diff --git a/components/HeaderQueueWidget.vue b/components/HeaderQueueWidget.vue index 105d14e..23c5ad5 100644 --- a/components/HeaderQueueWidget.vue +++ b/components/HeaderQueueWidget.vue @@ -1,17 +1,26 @@ diff --git a/composables/game.ts b/composables/game.ts new file mode 100644 index 0000000..5eba1d9 --- /dev/null +++ b/composables/game.ts @@ -0,0 +1,31 @@ +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import type { Game, GameStatus } from "~/types"; + +const gameRegistry: { [key: string]: Game } = {}; + +const gameStatusRegistry: { [key: string]: Ref } = {}; + +export const useGame = async (id: string) => { + if (!gameRegistry[id]) { + const data: { game: Game; status: GameStatus } = await invoke( + "fetch_game", + { + id, + } + ); + gameRegistry[id] = data.game; + if (!gameStatusRegistry[id]) { + gameStatusRegistry[id] = ref(data.status); + + listen(`update_game/${id}`, (event) => { + const payload: { status: GameStatus } = event.payload as any; + gameStatusRegistry[id].value = payload.status; + }); + } + } + + const game = gameRegistry[id]; + const status = gameStatusRegistry[id]; + return { game, status }; +}; diff --git a/composables/queue.ts b/composables/queue.ts index f684852..0487260 100644 --- a/composables/queue.ts +++ b/composables/queue.ts @@ -1,7 +1,7 @@ import { listen } from "@tauri-apps/api/event"; export type QueueState = { - queue: Array<{ id: string; status: string, progress: number }>; + queue: Array<{ id: string; status: string, progress: number | null }>; }; export const useQueueState = () => diff --git a/package.json b/package.json index 49c2009..c93e9a4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "dependencies": { "@headlessui/vue": "^1.7.23", "@heroicons/vue": "^2.1.5", - "@prisma/client": "5.20.0", "@tauri-apps/api": ">=2.0.0", "@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-dialog": "^2.0.1", @@ -22,14 +21,14 @@ "nuxt": "^3.13.0", "scss": "^0.2.4", "vue": "latest", - "vue-router": "latest" + "vue-router": "latest", + "vuedraggable": "^4.1.0" }, "devDependencies": { "@tailwindcss/forms": "^0.5.9", "@tauri-apps/cli": ">=2.0.0", "autoprefixer": "^10.4.20", "postcss": "^8.4.47", - "prisma": "^5.20.0", "sass-embedded": "^1.79.4", "tailwindcss": "^3.4.13" }, diff --git a/pages/library.vue b/pages/library.vue index c91ea64..ac7e24f 100644 --- a/pages/library.vue +++ b/pages/library.vue @@ -40,12 +40,10 @@ diff --git a/plugins/vuedraggable.ts b/plugins/vuedraggable.ts new file mode 100644 index 0000000..58d50a0 --- /dev/null +++ b/plugins/vuedraggable.ts @@ -0,0 +1,5 @@ +import draggable from "vuedraggable"; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.component("draggable", draggable); +}); diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 1ca51a1..0000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,150 +0,0 @@ -// This should be copied from the main Drop repo -// TODO: do this automatically - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -model User { - id String @id @default(uuid()) - username String @unique - admin Boolean @default(false) - - email String - displayName String - profilePicture String // Object - - authMecs LinkedAuthMec[] - clients Client[] -} - -enum AuthMec { - Simple -} - -model LinkedAuthMec { - userId String - mec AuthMec - - credentials Json - - user User @relation(fields: [userId], references: [id]) - - @@id([userId, mec]) -} - -enum ClientCapabilities { - DownloadAggregation -} - -enum Platform { - Windows @map("windows") - Linux @map("linux") -} - -// References a device -model Client { - id String @id @default(uuid()) - userId String - user User @relation(fields: [userId], references: [id]) - - endpoint String - capabilities ClientCapabilities[] - - name String - platform Platform - lastConnected DateTime -} - -enum MetadataSource { - Custom - GiantBomb -} - -model Game { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - - // Any field prefixed with m is filled in from metadata - // Acts as a cache so we can search and filter it - mName String // Name of game - mShortDescription String // Short description - mDescription String // Supports markdown - mDevelopers Developer[] - mPublishers Publisher[] - - mReviewCount Int - mReviewRating Float - - mIconId String // linked to objects in s3 - mBannerId String // linked to objects in s3 - mCoverId String - mImageLibrary String[] // linked to objects in s3 - - versions GameVersion[] - libraryBasePath String @unique // Base dir for all the game versions - - @@unique([metadataSource, metadataId], name: "metadataKey") -} - -// A particular set of files that relate to the version -model GameVersion { - gameId String - game Game @relation(fields: [gameId], references: [id]) - versionName String // Sub directory for the game files - - platform Platform - launchCommand String // Command to run to start. Platform-specific. Windows games on Linux will wrap this command in Proton/Wine - setupCommand String // Command to setup game (dependencies and such) - dropletManifest Json // Results from droplet - - versionIndex Int - delta Boolean @default(false) - - @@id([gameId, versionName]) -} - -model Developer { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - metadataOriginalQuery String - - mName String - mShortDescription String - mDescription String - mLogo String - mBanner String - mWebsite String - - games Game[] - - @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") -} - -model Publisher { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - metadataOriginalQuery String - - mName String - mShortDescription String - mDescription String - mLogo String - mBanner String - mWebsite String - - games Game[] - - @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") -} diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 044a067..d5ef686 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -19,21 +19,6 @@ pub fn download_game( .map_err(|_| "An error occurred while communicating with the download manager.".to_string()) } -#[tauri::command] -pub fn get_current_game_download_progress( - state: tauri::State<'_, Mutex>, -) -> Result { - match state - .lock() - .unwrap() - .download_manager - .get_current_game_download_progress() - { - Some(progress) => Ok(progress), - None => Err("Game does not exist".to_string()), - } -} - #[tauri::command] pub fn cancel_game_download(state: tauri::State<'_, Mutex>, game_id: String) { info!("Cancelling game download {}", game_id); @@ -54,6 +39,19 @@ pub fn resume_game_downloads(state: tauri::State<'_, Mutex>) { state.lock().unwrap().download_manager.resume_downloads() } +#[tauri::command] +pub fn move_game_in_queue( + state: tauri::State<'_, Mutex>, + old_index: usize, + new_index: usize, +) { + state + .lock() + .unwrap() + .download_manager + .rearrange(old_index, new_index) +} + #[tauri::command] pub fn get_current_write_speed(state: tauri::State<'_, Mutex>) {} diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index e70d53d..79c1006 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -132,19 +132,23 @@ impl DownloadManager { let current_index = get_index_from_id(&mut queue, id).unwrap(); let to_move = queue.remove(current_index).unwrap(); queue.insert(new_index, to_move); + self.command_sender.send(DownloadManagerSignal::Update); } pub fn rearrange(&self, current_index: usize, new_index: usize) { let mut queue = self.edit(); let to_move = queue.remove(current_index).unwrap(); queue.insert(new_index, to_move); + self.command_sender.send(DownloadManagerSignal::Update); } pub fn remove_from_queue(&self, index: usize) { self.edit().remove(index); + self.command_sender.send(DownloadManagerSignal::Update); } pub fn remove_from_queue_string(&self, id: String) { let mut queue = self.edit(); let current_index = get_index_from_id(&mut queue, id).unwrap(); queue.remove(current_index); + self.command_sender.send(DownloadManagerSignal::Update); } pub fn pause_downloads(&self) { self.command_sender diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0d9a505..8785007 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -134,7 +134,7 @@ pub fn run() { fetch_game_verion_options, // Downloads download_game, - get_current_game_download_progress, + move_game_in_queue, cancel_game_download, pause_game_downloads, resume_game_downloads, diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 9b38c5d..139b04c 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -17,7 +17,7 @@ use crate::remote::RemoteAccessError; use crate::{auth::generate_authorization_header, AppState, DB}; #[derive(serde::Serialize)] -struct FetchGameStruct { +pub struct FetchGameStruct { game: Game, status: DatabaseGameStatus, } @@ -67,7 +67,7 @@ pub struct GameVersionOption { // total_size: usize, } -fn fetch_library_logic(app: AppHandle) -> Result { +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")?; @@ -102,15 +102,18 @@ fn fetch_library_logic(app: AppHandle) -> Result { drop(handle); - Ok(json!(games.clone()).to_string()) + Ok(games) } #[tauri::command] -pub fn fetch_library(app: AppHandle) -> Result { +pub fn fetch_library(app: AppHandle) -> Result, String> { fetch_library_logic(app).map_err(|e| e.to_string()) } -fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result { +fn fetch_game_logic( + id: String, + app: tauri::AppHandle, +) -> Result { let state = app.state::>(); let mut state_handle = state.lock().unwrap(); @@ -128,7 +131,7 @@ fn fetch_game_logic(id: String, app: tauri::AppHandle) -> Result Result Result { +pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result { let result = fetch_game_logic(id, app); if result.is_err() { diff --git a/types.ts b/types.ts index 3a6e3f4..5702594 100644 --- a/types.ts +++ b/types.ts @@ -1,4 +1,3 @@ -import type { User } from "@prisma/client"; import type { Component } from "vue"; export type NavigationItem = { @@ -12,11 +11,31 @@ export type QuickActionNav = { notifications?: number; action: () => Promise; }; + +export type User = { + id: string; + username: string; + admin: boolean; + displayName: string; + profilePicture: string; +}; + export type AppState = { status: AppStatus; user?: User; }; +export type Game = { + id: string; + mName: string; + mShortDescription: string; + mDescription: string; + mIconId: string; + mBannerId: string; + mCoverId: string; + mImageLibrary: string[]; +}; + export enum AppStatus { NotConfigured = "NotConfigured", SignedOut = "SignedOut", diff --git a/yarn.lock b/yarn.lock index af475d7..a4fa8b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1075,47 +1075,6 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73" integrity sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw== -"@prisma/client@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.20.0.tgz#4fc9f2b2341c9c997c139df4445688dd6b39663b" - integrity sha512-CLv55ZuMuUawMsxoqxGtLT3bEZoa2W8L3Qnp6rDIFWy+ZBrUcOFKdoeGPSnbBqxc3SkdxJrF+D1veN/WNynZYA== - -"@prisma/debug@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@prisma/debug/-/debug-5.20.0.tgz#c6d1cf6e3c6e9dba150347f13ca200b1d66cc9fc" - integrity sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ== - -"@prisma/engines-version@5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284": - version "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284.tgz#9a53b13cdcfd706ae54198111000f33c63655c39" - integrity sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA== - -"@prisma/engines@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-5.20.0.tgz#86fe407e55219d33d03ebc26dc829a422faed545" - integrity sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ== - dependencies: - "@prisma/debug" "5.20.0" - "@prisma/engines-version" "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284" - "@prisma/fetch-engine" "5.20.0" - "@prisma/get-platform" "5.20.0" - -"@prisma/fetch-engine@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@prisma/fetch-engine/-/fetch-engine-5.20.0.tgz#b917880fb08f654981f14ca49923031b39683586" - integrity sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw== - dependencies: - "@prisma/debug" "5.20.0" - "@prisma/engines-version" "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284" - "@prisma/get-platform" "5.20.0" - -"@prisma/get-platform@5.20.0": - version "5.20.0" - resolved "https://registry.yarnpkg.com/@prisma/get-platform/-/get-platform-5.20.0.tgz#c1a53a8d8af67f2b4a6b97dd4d25b1c603236804" - integrity sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA== - dependencies: - "@prisma/debug" "5.20.0" - "@rollup/plugin-alias@^5.1.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz#53601d88cda8b1577aa130b4a6e452283605bf26" @@ -2816,7 +2775,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@2.3.3, fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -4345,15 +4304,6 @@ pretty-bytes@^6.1.1: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b" integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ== -prisma@^5.20.0: - version "5.20.0" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-5.20.0.tgz#f2ab266a0d59383506886e7acbff0dbf322f4c7e" - integrity sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig== - dependencies: - "@prisma/engines" "5.20.0" - optionalDependencies: - fsevents "2.3.3" - process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -4847,6 +4797,11 @@ smob@^1.0.0: resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab" integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== +sortablejs@1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.14.0.tgz#6d2e17ccbdb25f464734df621d4f35d4ab35b3d8" + integrity sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w== + source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" @@ -5553,6 +5508,13 @@ vue@^3.5.5, vue@latest: "@vue/server-renderer" "3.5.11" "@vue/shared" "3.5.11" +vuedraggable@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/vuedraggable/-/vuedraggable-4.1.0.tgz#edece68adb8a4d9e06accff9dfc9040e66852270" + integrity sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww== + dependencies: + sortablejs "1.14.0" + webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" From 01260f0732d72d484436496d10fe04d02e144c0d Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 9 Dec 2024 18:07:41 +1100 Subject: [PATCH 135/164] fix(download manager): fixed queue manipulation and waiting for downloads --- src-tauri/src/downloads/download_agent.rs | 2 +- src-tauri/src/downloads/download_commands.rs | 10 --- src-tauri/src/downloads/download_manager.rs | 48 ++++++++----- .../src/downloads/download_manager_builder.rs | 69 +++++++++---------- src-tauri/src/lib.rs | 1 - 5 files changed, 65 insertions(+), 65 deletions(-) diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 0b429d1..a5b2d7d 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -285,7 +285,7 @@ impl GameDownloadAgent { // If we're not out of contexts, we're not done, so we don't fire completed if !context_lock.is_empty() { - info!("Download agent didn't finish, not sending completed signal"); + info!("da for {} exited without completing", self.id.clone()); return Ok(()); } diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index d5ef686..1bf1dee 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -19,16 +19,6 @@ pub fn download_game( .map_err(|_| "An error occurred while communicating with the download manager.".to_string()) } -#[tauri::command] -pub fn cancel_game_download(state: tauri::State<'_, Mutex>, game_id: String) { - info!("Cancelling game download {}", game_id); - state - .lock() - .unwrap() - .download_manager - .cancel_download(game_id); -} - #[tauri::command] pub fn pause_game_downloads(state: tauri::State<'_, Mutex>) { state.lock().unwrap().download_manager.pause_downloads() diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 79c1006..9f8799b 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -1,6 +1,7 @@ use std::{ any::Any, collections::VecDeque, + fmt::Debug, sync::{ mpsc::{SendError, Sender}, Arc, Mutex, MutexGuard, @@ -31,7 +32,7 @@ pub enum DownloadManagerSignal { /// Tells the Manager to stop the current /// download and return Finish, - Cancel(String), + Cancel, /// Any error which occurs in the agent Error(GameDownloadError), /// Pushes UI update @@ -82,6 +83,13 @@ impl From> for GameDownloadAgentQueueStandin { } } } +impl Debug for GameDownloadAgentQueueStandin { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GameDownloadAgentQueueStandin") + .field("id", &self.id) + .finish() + } +} impl DownloadManager { pub fn new( @@ -112,11 +120,6 @@ impl DownloadManager { ))?; self.command_sender.send(DownloadManagerSignal::Go) } - pub fn cancel_download(&self, game_id: String) { - self.command_sender - .send(DownloadManagerSignal::Cancel(game_id)) - .unwrap(); - } pub fn edit(&self) -> MutexGuard<'_, VecDeque>> { self.download_queue.edit() } @@ -132,23 +135,32 @@ impl DownloadManager { let current_index = get_index_from_id(&mut queue, id).unwrap(); let to_move = queue.remove(current_index).unwrap(); queue.insert(new_index, to_move); - self.command_sender.send(DownloadManagerSignal::Update); + self.command_sender + .send(DownloadManagerSignal::Update) + .unwrap(); } pub fn rearrange(&self, current_index: usize, new_index: usize) { + let needs_pause = current_index == 0 || new_index == 0; + if needs_pause { + self.command_sender + .send(DownloadManagerSignal::Cancel) + .unwrap(); + } + + 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); - self.command_sender.send(DownloadManagerSignal::Update); - } - pub fn remove_from_queue(&self, index: usize) { - self.edit().remove(index); - self.command_sender.send(DownloadManagerSignal::Update); - } - pub fn remove_from_queue_string(&self, id: String) { - let mut queue = self.edit(); - let current_index = get_index_from_id(&mut queue, id).unwrap(); - queue.remove(current_index); - self.command_sender.send(DownloadManagerSignal::Update); + + info!("new queue: {:?}", queue); + + if needs_pause { + self.command_sender.send(DownloadManagerSignal::Go).unwrap(); + } + self.command_sender + .send(DownloadManagerSignal::Update) + .unwrap(); } pub fn pause_downloads(&self) { self.command_sender diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index fa3e13f..1c4d53f 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -4,7 +4,7 @@ use std::{ mpsc::{channel, Receiver, Sender}, Arc, Mutex, }, - thread::spawn, + thread::{spawn, JoinHandle}, }; use log::{error, info}; @@ -77,6 +77,7 @@ pub struct DownloadManagerBuilder { app_handle: AppHandle, current_download_agent: Option>, // Should be the only game download agent in the map with the "Go" flag + current_download_thread: Mutex>>, active_control_flag: Option, } @@ -91,12 +92,14 @@ impl DownloadManagerBuilder { download_agent_registry: HashMap::new(), download_queue: queue.clone(), command_receiver, - current_download_agent: None, - active_control_flag: None, status: status.clone(), sender: command_sender.clone(), progress: active_progress.clone(), app_handle, + + current_download_agent: None, + current_download_thread: Mutex::new(None), + active_control_flag: None, }; let terminator = spawn(|| manager.manage_queue()); @@ -138,14 +141,23 @@ impl DownloadManagerBuilder { self.app_handle.emit("update_queue", event_data).unwrap(); } - fn cleanup_current_game(&mut self, game_id: &String) -> Arc { + fn remove_and_cleanup_game(&mut self, game_id: &String) -> Arc { self.download_queue.pop_front(); let download_agent = self.download_agent_registry.remove(game_id).unwrap(); + self.cleanup_current_download(); + return download_agent; + } + + // CAREFUL WITH THIS FUNCTION + // Make sure the download thread is terminated + fn cleanup_current_download(&mut self) { self.active_control_flag = None; *self.progress.lock().unwrap() = None; self.current_download_agent = None; - return download_agent; + let mut download_thread_lock = self.current_download_thread.lock().unwrap(); + *download_thread_lock = None; + drop(download_thread_lock); } fn manage_queue(mut self) -> Result<(), ()> { @@ -177,8 +189,8 @@ impl DownloadManagerBuilder { DownloadManagerSignal::Error(e) => { self.manage_error_signal(e); } - DownloadManagerSignal::Cancel(id) => { - self.manage_cancel_signal(id); + DownloadManagerSignal::Cancel => { + self.manage_cancel_signal(); } DownloadManagerSignal::Update => { self.push_manager_update(); @@ -200,7 +212,7 @@ impl DownloadManagerBuilder { // When if let chains are stabilised, combine these two statements if interface.id == game_id { info!("Popping consumed data"); - let download_agent = self.cleanup_current_game(&game_id); + let download_agent = self.remove_and_cleanup_game(&game_id); if let Err(error) = on_game_complete(game_id, download_agent.version.clone(), &self.app_handle) @@ -241,8 +253,6 @@ impl DownloadManagerBuilder { } fn manage_go_signal(&mut self) { - info!("Got signal 'Go'"); - if !(!self.download_agent_registry.is_empty() && !self.download_queue.empty()) { return; } @@ -252,8 +262,9 @@ impl DownloadManagerBuilder { return; } - info!("Starting download agent"); + info!("current download queue: {:?}", self.download_queue.read()); let agent_data = self.download_queue.read().front().unwrap().clone(); + info!("starting download for {}", agent_data.id.clone()); let download_agent = self .download_agent_registry .get(&agent_data.id) @@ -274,7 +285,8 @@ impl DownloadManagerBuilder { let sender = self.sender.clone(); info!("Spawning download"); - spawn(move || { + let mut download_thread_lock = self.current_download_thread.lock().unwrap(); + *download_thread_lock = Some(spawn(move || { match download_agent.download() { // Returns once we've exited the download // (not necessarily completed) @@ -286,7 +298,7 @@ impl DownloadManagerBuilder { sender.send(DownloadManagerSignal::Error(err)).unwrap(); } }; - }); + })); // Set status for game let mut status_handle = agent_data.status.lock().unwrap(); @@ -303,7 +315,7 @@ impl DownloadManagerBuilder { fn manage_error_signal(&mut self, error: GameDownloadError) { let current_status = self.current_download_agent.clone().unwrap(); - self.cleanup_current_game(¤t_status.id); // Remove all the locks and shit + self.remove_and_cleanup_game(¤t_status.id); // Remove all the locks and shit let mut lock = current_status.status.lock().unwrap(); *lock = GameDownloadStatus::Error; @@ -314,33 +326,20 @@ impl DownloadManagerBuilder { self.sender.send(DownloadManagerSignal::Update).unwrap(); } - fn manage_cancel_signal(&mut self, game_id: String) { + fn manage_cancel_signal(&mut self) { if let Some(current_flag) = &self.active_control_flag { current_flag.set(DownloadThreadControlFlag::Stop); } - // TODO wait until current download exits - // This cleanup function might break things because it - // unsets the control flag - self.cleanup_current_game(&game_id); + let mut download_thread_lock = self.current_download_thread.lock().unwrap(); + if let Some(current_download_thread) = download_thread_lock.take() { + current_download_thread.join().unwrap(); + } + drop(download_thread_lock); - self.download_agent_registry.remove(&game_id); - let mut lock = self.download_queue.edit(); - let index = match lock.iter().position(|interface| interface.id == game_id) { - Some(index) => index, - None => return, - }; - lock.remove(index); + info!("cancel waited for download to finish"); - // Start next download - self.sender.send(DownloadManagerSignal::Go).unwrap(); - info!( - "{:?}", - self.download_agent_registry - .iter() - .map(|x| x.0.clone()) - .collect::() - ); + self.cleanup_current_download(); } fn set_status(&self, status: DownloadManagerStatus) { *self.status.lock().unwrap() = status; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8785007..31c51f6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -135,7 +135,6 @@ pub fn run() { // Downloads download_game, move_game_in_queue, - cancel_game_download, pause_game_downloads, resume_game_downloads, ]) From 653717ebcf9b7d76b2876942c4180c464a43c632 Mon Sep 17 00:00:00 2001 From: Louis van Liefland <116044207+quexeky@users.noreply.github.com> Date: Mon, 9 Dec 2024 20:32:42 +1100 Subject: [PATCH 136/164] refactor: Ran cargo clippy & cargo fmt --- src-tauri/src/auth.rs | 6 ++-- src-tauri/src/db.rs | 8 ++--- src-tauri/src/downloads/download_agent.rs | 11 ++++--- src-tauri/src/downloads/download_commands.rs | 4 +-- src-tauri/src/downloads/download_logic.rs | 10 +++---- src-tauri/src/downloads/download_manager.rs | 1 + .../src/downloads/download_manager_builder.rs | 4 +-- src-tauri/src/downloads/manifest.rs | 5 ++-- src-tauri/src/downloads/mod.rs | 4 +-- src-tauri/src/downloads/progress_object.rs | 2 +- src-tauri/src/downloads/queue.rs | 8 +++-- src-tauri/src/lib.rs | 4 +-- src-tauri/src/library.rs | 30 +++++++------------ src-tauri/src/remote.rs | 12 ++++---- 14 files changed, 52 insertions(+), 57 deletions(-) diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs index 6761cec..5cb6aaf 100644 --- a/src-tauri/src/auth.rs +++ b/src-tauri/src/auth.rs @@ -1,5 +1,7 @@ use std::{ - borrow::BorrowMut, env, sync::Mutex, time::{SystemTime, UNIX_EPOCH} + env, + sync::Mutex, + time::{SystemTime, UNIX_EPOCH}, }; use log::{info, warn}; @@ -9,7 +11,7 @@ use tauri::{AppHandle, Emitter, Manager}; use url::Url; use crate::{ - db::{self, DatabaseAuth, DatabaseImpls}, + db::{DatabaseAuth, DatabaseImpls}, remote::RemoteAccessError, AppState, AppStatus, User, DB, }; diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index d4ba64d..d9122e6 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -10,7 +10,6 @@ use log::debug; use rustbreak::{DeSerError, DeSerializer, PathDatabase}; use rustix::path::Arg; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_json::json; use url::Url; use crate::DB; @@ -100,7 +99,8 @@ impl DatabaseImpls for DatabaseInterface { #[allow(clippy::let_and_return)] let exists = fs::exists(db_path.clone()).unwrap(); - let db = match exists { + + match exists { true => PathDatabase::load_from_path(db_path).expect("Database loading failed"), false => { let default = Database { @@ -116,9 +116,7 @@ impl DatabaseImpls for DatabaseInterface { PathDatabase::create_at_path(db_path, default) .expect("Database could not be created") } - }; - - db + } } fn database_is_set_up(&self) -> bool { diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index a5b2d7d..380797f 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -179,7 +179,7 @@ impl GameDownloadAgent { drop(context_lock); self.generate_contexts()?; - return Ok(()); + Ok(()) } pub fn generate_contexts(&self) -> Result<(), GameDownloadError> { @@ -208,7 +208,7 @@ impl GameDownloadAgent { for (i, length) in chunk.lengths.iter().enumerate() { contexts.push(DropDownloadContext { file_name: raw_path.to_string(), - version: chunk.versionName.to_string(), + version: chunk.version_name.to_string(), offset: running_offset, index: i, game_id: game_id.to_string(), @@ -257,13 +257,12 @@ impl GameDownloadAgent { scope.spawn(move |_| { match download_game_chunk(context.clone(), control_flag, progress_handle) { - Ok(res) => match res { - true => { + Ok(res) => { + if res { let mut lock = completed_indexes_ref.lock().unwrap(); lock.push(index); } - false => {} - }, + } Err(e) => { error!("GameDownloadError: {}", e); self.sender.send(DownloadManagerSignal::Error(e)).unwrap(); diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 1bf1dee..23b8bab 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -1,7 +1,5 @@ use std::sync::Mutex; -use log::info; - use crate::AppState; #[tauri::command] @@ -42,8 +40,10 @@ pub fn move_game_in_queue( .rearrange(old_index, new_index) } +/* #[tauri::command] pub fn get_current_write_speed(state: tauri::State<'_, Mutex>) {} +*/ /* fn use_download_agent( diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index bdb3c64..6735c56 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -3,23 +3,21 @@ use crate::db::DatabaseImpls; use crate::downloads::manifest::DropDownloadContext; use crate::remote::RemoteAccessError; use crate::DB; -use log::{info, warn}; +use log::warn; use md5::{Context, Digest}; use reqwest::blocking::Response; use std::io::Read; -use std::sync::atomic::{AtomicUsize, Ordering}; use std::{ fs::{File, OpenOptions}, - io::{self, BufWriter, ErrorKind, Seek, SeekFrom, Write}, + io::{self, BufWriter, Seek, SeekFrom, Write}, path::PathBuf, - sync::Arc, }; use urlencoding::encode; use super::download_agent::GameDownloadError; use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}; -use super::progress_object::{ProgressHandle, ProgressObject}; +use super::progress_object::ProgressHandle; pub struct DropWriter { hasher: Context, @@ -182,7 +180,7 @@ pub fn download_game_chunk( content_length.unwrap().try_into().unwrap(), ); - let completed = pipeline.copy().map_err(|e| GameDownloadError::IoError(e))?; + let completed = pipeline.copy().map_err(GameDownloadError::IoError)?; if !completed { return Ok(false); }; diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 9f8799b..f04135f 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -91,6 +91,7 @@ impl Debug for GameDownloadAgentQueueStandin { } } +#[allow(dead_code)] impl DownloadManager { pub fn new( terminator: JoinHandle>, diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 1c4d53f..1b33853 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -120,7 +120,7 @@ impl DownloadManagerBuilder { &format!("update_game/{}", id), GameUpdateEvent { game_id: id, - status: status, + status, }, ) .unwrap(); @@ -145,7 +145,7 @@ impl DownloadManagerBuilder { self.download_queue.pop_front(); let download_agent = self.download_agent_registry.remove(game_id).unwrap(); self.cleanup_current_download(); - return download_agent; + download_agent } // CAREFUL WITH THIS FUNCTION diff --git a/src-tauri/src/downloads/manifest.rs b/src-tauri/src/downloads/manifest.rs index d6cf8ec..d815861 100644 --- a/src-tauri/src/downloads/manifest.rs +++ b/src-tauri/src/downloads/manifest.rs @@ -4,12 +4,13 @@ use std::path::PathBuf; pub type DropManifest = HashMap; #[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct DropChunk { pub permissions: usize, pub ids: Vec, pub checksums: Vec, pub lengths: Vec, - pub versionName: String, + pub version_name: String, } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -21,5 +22,5 @@ pub struct DropDownloadContext { pub game_id: String, pub path: PathBuf, pub checksum: String, - pub length: usize + pub length: usize, } diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs index c7c11ee..0102c33 100644 --- a/src-tauri/src/downloads/mod.rs +++ b/src-tauri/src/downloads/mod.rs @@ -1,9 +1,9 @@ pub mod download_agent; pub mod download_commands; mod download_logic; -pub mod download_manager_builder; pub mod download_manager; +pub mod download_manager_builder; mod download_thread_control_flag; mod manifest; mod progress_object; -pub mod queue; \ No newline at end of file +pub mod queue; diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/downloads/progress_object.rs index fac9b9b..143656d 100644 --- a/src-tauri/src/downloads/progress_object.rs +++ b/src-tauri/src/downloads/progress_object.rs @@ -68,7 +68,7 @@ impl ProgressObject { .fetch_add(amount_added, Ordering::Relaxed); let to_update_handle = self.points_to_push_update.lock().unwrap(); - let to_update = to_update_handle.clone(); + let to_update = *to_update_handle; drop(to_update_handle); if current_amount < to_update { diff --git a/src-tauri/src/downloads/queue.rs b/src-tauri/src/downloads/queue.rs index f4139ac..0ea65ca 100644 --- a/src-tauri/src/downloads/queue.rs +++ b/src-tauri/src/downloads/queue.rs @@ -10,6 +10,7 @@ pub struct Queue { inner: Arc>>>, } +#[allow(dead_code)] impl Queue { pub fn new() -> Self { Self { @@ -40,7 +41,10 @@ impl Queue { pub fn append(&self, interface: GameDownloadAgentQueueStandin) { self.edit().push_back(Arc::new(interface)); } - pub fn pop_front_if_equal(&self, game_id: String) -> Option> { + pub fn pop_front_if_equal( + &self, + game_id: String, + ) -> Option> { let mut queue = self.edit(); let front = match queue.front() { Some(front) => front, @@ -49,7 +53,7 @@ impl Queue { if front.id == game_id { return queue.pop_front(); } - return None; + None } pub fn get_by_id(&self, game_id: String) -> Option { self.read().iter().position(|data| data.id == game_id) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 31c51f6..db0d3b3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,7 +2,7 @@ mod auth; mod db; mod downloads; mod library; -mod p2p; +// mod p2p; mod remote; mod settings; #[cfg(test)] @@ -21,7 +21,7 @@ use env_logger::Env; use http::{header::*, response::Builder as ResponseBuilder}; use library::{fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, Game}; use log::{debug, info}; -use remote::{gen_drop_url, use_remote, RemoteAccessError}; +use remote::{gen_drop_url, use_remote}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use std::{ diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 139b04c..0e25c52 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -1,17 +1,13 @@ -use std::collections::{HashMap, VecDeque}; -use std::fmt::format; use std::sync::Mutex; -use log::info; use serde::{Deserialize, Serialize}; -use serde_json::json; use tauri::Emitter; use tauri::{AppHandle, Manager}; use urlencoding::encode; use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; -use crate::db::{self, GameVersion}; +use crate::db::GameVersion; use crate::downloads::download_manager::GameDownloadStatus; use crate::remote::RemoteAccessError; use crate::{auth::generate_authorization_header, AppState, DB}; @@ -159,12 +155,11 @@ fn fetch_game_logic( let mut db_handle = DB.borrow_data_mut().unwrap(); - if !db_handle.games.games_statuses.contains_key(&id) { - db_handle - .games - .games_statuses - .insert(id, DatabaseGameStatus::Remote {}); - } + db_handle + .games + .games_statuses + .entry(id) + .or_insert(DatabaseGameStatus::Remote {}); let data = FetchGameStruct { game: game.clone(), @@ -176,7 +171,7 @@ fn fetch_game_logic( .clone(), }; - return Ok(data); + Ok(data) } #[tauri::command] @@ -201,7 +196,7 @@ pub fn fetch_game_status(id: String) -> Result { .clone(); drop(db_handle); - return Ok(status); + Ok(status) } fn fetch_game_verion_options_logic( @@ -227,7 +222,7 @@ fn fetch_game_verion_options_logic( let data = response.json::>()?; - return Ok(data); + Ok(data) } #[tauri::command] @@ -266,7 +261,7 @@ pub fn on_game_complete( .games .game_versions .entry(game_id.clone()) - .or_insert(HashMap::new()) + .or_default() .insert(version_name.clone(), data.clone()); drop(handle); DB.save().unwrap(); @@ -287,10 +282,7 @@ pub fn on_game_complete( app_handle .emit( &format!("update_game/{}", game_id), - GameUpdateEvent { - game_id: game_id, - status: status, - }, + GameUpdateEvent { game_id, status }, ) .unwrap(); diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs index 8e05d4c..0a53c2b 100644 --- a/src-tauri/src/remote.rs +++ b/src-tauri/src/remote.rs @@ -5,7 +5,7 @@ use std::{ use http::StatusCode; use log::{info, warn}; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use url::{ParseError, Url}; use crate::{AppState, AppStatus, DB}; @@ -20,7 +20,7 @@ pub enum RemoteAccessError { GameNotFound, InvalidResponse, InvalidRedirect, - ManifestDownloadFailed(StatusCode, String) + ManifestDownloadFailed(StatusCode, String), } impl Display for RemoteAccessError { @@ -36,10 +36,10 @@ impl Display for RemoteAccessError { RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"), RemoteAccessError::InvalidResponse => write!(f, "Server returned an invalid response"), RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"), - RemoteAccessError::ManifestDownloadFailed(status, response) => - write!(f, "Failed to download game manifest: {} {}", - status, - response + RemoteAccessError::ManifestDownloadFailed(status, response) => write!( + f, + "Failed to download game manifest: {} {}", + status, response ), } } From 8d9234f82a0217736de694c8673b24741c40839a Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 9 Dec 2024 20:41:36 +1100 Subject: [PATCH 137/164] fix: windows build --- src-tauri/src/db.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index d9122e6..38b11fc 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -8,7 +8,6 @@ use std::{ use directories::BaseDirs; use log::debug; use rustbreak::{DeSerError, DeSerializer, PathDatabase}; -use rustix::path::Arg; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use url::Url; @@ -112,7 +111,7 @@ impl DatabaseImpls for DatabaseInterface { game_versions: HashMap::new(), }, }; - debug!("Creating database at path {}", db_path.as_str().unwrap()); + 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") } From 52436942eb5a4bd7600fec4a3c1f0b6469fc9c80 Mon Sep 17 00:00:00 2001 From: Louis van Liefland <116044207+quexeky@users.noreply.github.com> Date: Sat, 14 Dec 2024 22:38:11 +1100 Subject: [PATCH 138/164] chore(downloads): Added time debugging and fixed logging formatting --- src-tauri/src/downloads/download_agent.rs | 4 ++++ src-tauri/src/downloads/download_manager_builder.rs | 2 ++ src-tauri/src/lib.rs | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 52855cd..5aed80a 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -6,12 +6,14 @@ use crate::remote::RemoteAccessError; use crate::DB; use log::{debug, error, info}; use rayon::ThreadPoolBuilder; +use core::time; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::io; use std::path::Path; use std::sync::mpsc::Sender; use std::sync::{Arc, Mutex}; +use std::time::Instant; use urlencoding::encode; #[cfg(target_os = "linux")] @@ -99,8 +101,10 @@ impl GameDownloadAgent { pub fn download(&self) -> Result<(), GameDownloadError> { self.setup_download()?; self.set_progress_object_params(); + let timer = Instant::now(); self.run().map_err(|_| GameDownloadError::DownloadError)?; + info!("{} took {}ms to download", self.id, timer.elapsed().as_millis()); Ok(()) } diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 1b33853..9f569ac 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -201,6 +201,7 @@ impl DownloadManagerBuilder { fn manage_stop_signal(&mut self) { info!("Got signal 'Stop'"); + self.set_status(DownloadManagerStatus::Paused); if let Some(active_control_flag) = self.active_control_flag.clone() { active_control_flag.set(DownloadThreadControlFlag::Stop); } @@ -327,6 +328,7 @@ impl DownloadManagerBuilder { self.sender.send(DownloadManagerSignal::Update).unwrap(); } fn manage_cancel_signal(&mut self) { + self.set_status(DownloadManagerStatus::Paused); if let Some(current_flag) = &self.active_control_flag { current_flag.set(DownloadThreadControlFlag::Stop); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f9b5ab2..34d7d0a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -76,12 +76,12 @@ fn fetch_state(state: tauri::State<'_, Mutex>) -> Result AppState { let logfile = FileAppender::builder() - .encoder(Box::new(PatternEncoder::new("{t}|{l}|{f} - {m}{n}"))) + .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}"))) .build(DATA_ROOT_DIR.lock().unwrap().join("./drop.log")) .unwrap(); let console = ConsoleAppender::builder() - .encoder(Box::new(PatternEncoder::new("{t}|{l}|{f} - {m}{n}\n"))) + .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}\n"))) .build(); let config = Config::builder() From 269dcbb6f34c313a66aafeae999e358191ed7a7e Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sun, 15 Dec 2024 16:15:11 +1100 Subject: [PATCH 139/164] feat(download manager): only allow downloads for supported platforms --- pages/library/[id]/index.vue | 2 +- src-tauri/src/db.rs | 29 ++++++++---- src-tauri/src/downloads/download_manager.rs | 2 + .../src/downloads/download_manager_builder.rs | 5 +++ src-tauri/src/lib.rs | 13 +++++- src-tauri/src/library.rs | 25 +++++++++-- src-tauri/src/p2p/discovery.rs | 26 ----------- src-tauri/src/p2p/mod.rs | 1 - src-tauri/src/p2p/registration.rs | 17 ------- src-tauri/src/process/mod.rs | 1 + src-tauri/src/process/process_manager.rs | 45 +++++++++++++++++++ 11 files changed, 107 insertions(+), 59 deletions(-) delete mode 100644 src-tauri/src/p2p/discovery.rs delete mode 100644 src-tauri/src/p2p/mod.rs delete mode 100644 src-tauri/src/p2p/registration.rs create mode 100644 src-tauri/src/process/mod.rs create mode 100644 src-tauri/src/process/process_manager.rs diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index 40d9596..5f3c119 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -168,7 +168,7 @@

- There are no versions to install. Please contact your + There are no supported versions to install. Please contact your server admin or try again later.

diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 38b11fc..e5d60b2 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -11,7 +11,7 @@ use rustbreak::{DeSerError, DeSerializer, PathDatabase}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use url::Url; -use crate::DB; +use crate::{process::process_manager::Platform, DB}; #[derive(serde::Serialize, Clone, Deserialize)] #[serde(rename_all = "camelCase")] @@ -26,11 +26,21 @@ pub struct DatabaseAuth { #[serde(tag = "type")] pub enum DatabaseGameStatus { Remote {}, - Queued { version_name: String }, - Downloading { version_name: String }, - SetupRequired { version_name: String }, - Installed { version_name: String }, - Updating { version_name: String }, + Queued { + version_name: String, + }, + Downloading { + version_name: String, + }, + SetupRequired { + version_name: String, + }, + Installed { + version_name: String, + }, + Updating { + version_name: String, + }, Uninstalling {}, } @@ -41,7 +51,7 @@ pub struct GameVersion { pub version_name: String, pub launch_command: String, pub setup_command: String, - pub platform: String, + pub platform: Platform, } #[derive(Serialize, Clone, Deserialize)] @@ -111,7 +121,10 @@ impl DatabaseImpls for DatabaseInterface { game_versions: HashMap::new(), }, }; - debug!("Creating database at path {}", db_path.as_os_str().to_str().unwrap()); + 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") } diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index f04135f..2e1291c 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -37,6 +37,8 @@ pub enum DownloadManagerSignal { Error(GameDownloadError), /// Pushes UI update Update, + /// Causes the Download Agent status to be synced to disk + Sync(usize), } pub enum DownloadManagerStatus { Downloading, diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 9f569ac..ca18df5 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -141,6 +141,8 @@ impl DownloadManagerBuilder { self.app_handle.emit("update_queue", event_data).unwrap(); } + fn sync_download_agent(&self) {} + fn remove_and_cleanup_game(&mut self, game_id: &String) -> Arc { self.download_queue.pop_front(); let download_agent = self.download_agent_registry.remove(game_id).unwrap(); @@ -195,6 +197,9 @@ impl DownloadManagerBuilder { DownloadManagerSignal::Update => { self.push_manager_update(); } + DownloadManagerSignal::Sync(index) => { + self.sync_download_agent(); + } }; } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 34d7d0a..8e4e2fc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2,7 +2,8 @@ mod auth; mod db; mod downloads; mod library; -// mod p2p; + +mod process; mod remote; mod settings; #[cfg(test)] @@ -25,6 +26,7 @@ use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; use log4rs::Config; +use process::process_manager::ProcessManager; use remote::{gen_drop_url, use_remote}; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -64,6 +66,8 @@ pub struct AppState { #[serde(skip_serializing)] download_manager: Arc, + #[serde(skip_serializing)] + process_manager: Arc, } #[tauri::command] @@ -81,7 +85,7 @@ fn setup(handle: AppHandle) -> AppState { .unwrap(); let console = ConsoleAppender::builder() - .encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}\n"))) + .encoder(Box::new(PatternEncoder::new("{t}|{l}|{f} - {m}{n}"))) .build(); let config = Config::builder() @@ -100,6 +104,7 @@ fn setup(handle: AppHandle) -> AppState { let games = HashMap::new(); let download_manager = Arc::new(DownloadManagerBuilder::build(handle)); + let process_manager = Arc::new(ProcessManager::new()); debug!("Checking if database is set up"); let is_set_up = DB.database_is_set_up(); @@ -109,6 +114,7 @@ fn setup(handle: AppHandle) -> AppState { user: None, games, download_manager, + process_manager, }; } @@ -120,6 +126,7 @@ fn setup(handle: AppHandle) -> AppState { user, games, download_manager, + process_manager, } } @@ -234,4 +241,6 @@ pub fn run() { }) .run(tauri::generate_context!()) .expect("error while running tauri application"); + + info!("exiting drop application..."); } diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 0e25c52..15fa7b6 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -9,6 +9,7 @@ use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; use crate::db::GameVersion; use crate::downloads::download_manager::GameDownloadStatus; +use crate::process::process_manager::Platform; use crate::remote::RemoteAccessError; use crate::{auth::generate_authorization_header, AppState, DB}; @@ -56,7 +57,7 @@ pub struct QueueUpdateEvent { pub struct GameVersionOption { version_index: usize, version_name: String, - platform: String, + platform: Platform, setup_command: String, launch_command: String, delta: bool, @@ -199,8 +200,9 @@ pub fn fetch_game_status(id: String) -> Result { Ok(status) } -fn fetch_game_verion_options_logic( +fn fetch_game_verion_options_logic<'a>( game_id: String, + state: tauri::State<'_, Mutex>, ) -> Result, RemoteAccessError> { let base_url = DB.fetch_base_url(); @@ -222,12 +224,27 @@ fn fetch_game_verion_options_logic( let data = response.json::>()?; + let state_lock = state.lock().unwrap(); + let data = data + .into_iter() + .filter(|v| { + state_lock + .process_manager + .valid_platform(&v.platform) + .unwrap() + }) + .collect::>(); + drop(state_lock); + Ok(data) } #[tauri::command] -pub fn fetch_game_verion_options(game_id: String) -> Result, String> { - fetch_game_verion_options_logic(game_id).map_err(|e| e.to_string()) +pub fn fetch_game_verion_options<'a>( + game_id: String, + state: tauri::State<'_, Mutex>, +) -> Result, String> { + fetch_game_verion_options_logic(game_id, state).map_err(|e| e.to_string()) } pub fn on_game_complete( diff --git a/src-tauri/src/p2p/discovery.rs b/src-tauri/src/p2p/discovery.rs deleted file mode 100644 index 0b2ec70..0000000 --- a/src-tauri/src/p2p/discovery.rs +++ /dev/null @@ -1,26 +0,0 @@ -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Serialize, Deserialize, Debug)] -pub struct P2PManager { - peers: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Peer { - endpoints: Vec, - current_endpoint: usize, - // TODO: Implement Wireguard tunnels -} - -impl Peer { - pub fn get_current_endpoint(&self) -> Url { - self.endpoints[self.current_endpoint].clone() - } - pub fn connect(&mut self) { - todo!() - } - pub fn disconnect(&mut self) { - todo!() - } -} diff --git a/src-tauri/src/p2p/mod.rs b/src-tauri/src/p2p/mod.rs deleted file mode 100644 index fc4b5cb..0000000 --- a/src-tauri/src/p2p/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod discovery; diff --git a/src-tauri/src/p2p/registration.rs b/src-tauri/src/p2p/registration.rs deleted file mode 100644 index 0926515..0000000 --- a/src-tauri/src/p2p/registration.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::{auth::generate_authorization_header, db::DatabaseImpls, remote::RemoteAccessError, DB}; - - -pub async fn register() -> Result { - let base_url = DB.fetch_base_url(); - let registration_url = base_url.join("/api/v1/client/capability").unwrap(); - let header = generate_authorization_header(); - - - let client = reqwest::blocking::Client::new(); - client - .post(registration_url) - .header("Authorization", header) - .send()?; - - return Ok(String::new()) -} \ No newline at end of file diff --git a/src-tauri/src/process/mod.rs b/src-tauri/src/process/mod.rs new file mode 100644 index 0000000..30cc14c --- /dev/null +++ b/src-tauri/src/process/mod.rs @@ -0,0 +1 @@ +pub mod process_manager; diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs new file mode 100644 index 0000000..e0bda56 --- /dev/null +++ b/src-tauri/src/process/process_manager.rs @@ -0,0 +1,45 @@ +use std::{collections::HashMap, sync::LazyLock}; + +use serde::{Deserialize, Serialize}; + +pub struct ProcessManager { + current_platform: Platform, +} + +impl ProcessManager { + pub fn new() -> Self { + ProcessManager { + current_platform: if cfg!(windows) { + Platform::Windows + } else { + Platform::Linux + }, + } + } + + pub fn valid_platform(&self, platform: &Platform) -> Result { + let current = &self.current_platform; + let valid_platforms = PROCESS_COMPATABILITY_MATRIX + .get(current) + .ok_or("Incomplete platform compatability matrix.")?; + + Ok(valid_platforms.contains(platform)) + } +} + +#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone)] +pub enum Platform { + Windows, + Linux, +} + +pub type ProcessCompatabilityMatrix = HashMap>; +pub static PROCESS_COMPATABILITY_MATRIX: LazyLock = + LazyLock::new(|| { + let mut matrix: ProcessCompatabilityMatrix = HashMap::new(); + + matrix.insert(Platform::Windows, vec![Platform::Windows]); + matrix.insert(Platform::Linux, vec![Platform::Linux]); // TODO: add Proton support + + return matrix; + }); From 3f71149289e5d988abb560dcd5ee6870b7fe84c8 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sun, 15 Dec 2024 17:29:21 +1100 Subject: [PATCH 140/164] feat(process manager): launch games with log files --- pages/library/[id]/index.vue | 19 +++- src-tauri/src/db.rs | 5 + src-tauri/src/downloads/download_agent.rs | 31 +++--- src-tauri/src/downloads/download_logic.rs | 12 ++- .../src/downloads/download_manager_builder.rs | 9 +- src-tauri/src/downloads/manifest.rs | 3 +- src-tauri/src/lib.rs | 7 +- src-tauri/src/library.rs | 20 ++-- src-tauri/src/process/mod.rs | 1 + src-tauri/src/process/process_commands.rs | 16 +++ src-tauri/src/process/process_manager.rs | 99 ++++++++++++++++++- 11 files changed, 191 insertions(+), 31 deletions(-) create mode 100644 src-tauri/src/process/process_commands.rs diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index 5f3c119..9ffd0ce 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -18,7 +18,11 @@
- +
@@ -168,8 +172,8 @@

- There are no supported versions to install. Please contact your - server admin or try again later. + There are no supported versions to install. Please + contact your server admin or try again later.

@@ -369,4 +373,13 @@ async function install() { installError.value = (error as string).toString(); } } + +async function play() { + try { + await invoke("launch_game", { gameId: game.value.id }); + } catch (e) { + game.value.mName = e as string; + console.error(e); + } +} diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index e5d60b2..603db7b 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -34,9 +34,11 @@ pub enum DatabaseGameStatus { }, SetupRequired { version_name: String, + install_dir: String, }, Installed { version_name: String, + install_dir: String, }, Updating { version_name: String, @@ -100,11 +102,14 @@ impl DatabaseImpls for DatabaseInterface { let data_root_dir = DATA_ROOT_DIR.lock().unwrap(); let db_path = data_root_dir.join("drop.db"); 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); create_dir_all(data_root_dir.clone()).unwrap(); debug!("Creating games directory"); create_dir_all(games_base_dir.clone()).unwrap(); + debug!("Creating logs directory"); + create_dir_all(logs_root_dir.clone()).unwrap(); #[allow(clippy::let_and_return)] let exists = fs::exists(db_path.clone()).unwrap(); diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 5aed80a..3068089 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -4,9 +4,9 @@ use crate::downloads::manifest::{DropDownloadContext, DropManifest}; use crate::downloads::progress_object::ProgressHandle; use crate::remote::RemoteAccessError; use crate::DB; +use core::time; use log::{debug, error, info}; use rayon::ThreadPoolBuilder; -use core::time; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::io; @@ -28,7 +28,7 @@ pub struct GameDownloadAgent { pub id: String, pub version: String, pub control_flag: DownloadThreadControl, - pub target_download_dir: usize, + pub base_dir: String, contexts: Mutex>, pub manifest: Mutex>, pub progress: Arc, @@ -72,12 +72,20 @@ impl GameDownloadAgent { ) -> Self { // Don't run by default let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); + + let db_lock = DB.borrow_data().unwrap(); + let base_dir = db_lock.games.install_dirs[target_download_dir].clone(); + drop(db_lock); + + let base_dir_path = Path::new(&base_dir); + let data_base_dir_path = base_dir_path.join(id.clone()); + Self { id, version, control_flag, manifest: Mutex::new(None), - target_download_dir, + base_dir: data_base_dir_path.to_str().unwrap().to_owned(), contexts: Mutex::new(Vec::new()), progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), sender, @@ -104,7 +112,11 @@ impl GameDownloadAgent { let timer = Instant::now(); self.run().map_err(|_| GameDownloadError::DownloadError)?; - info!("{} took {}ms to download", self.id, timer.elapsed().as_millis()); + info!( + "{} took {}ms to download", + self.id, + timer.elapsed().as_millis() + ); Ok(()) } @@ -187,18 +199,12 @@ impl GameDownloadAgent { } pub fn generate_contexts(&self) -> Result<(), GameDownloadError> { - let db_lock = DB.borrow_data().unwrap(); - let data_base_dir = db_lock.games.install_dirs[self.target_download_dir].clone(); - drop(db_lock); - let manifest = self.manifest.lock().unwrap().clone().unwrap(); let game_id = self.id.clone(); - let data_base_dir_path = Path::new(&data_base_dir); - let mut contexts = Vec::new(); - let base_path = data_base_dir_path.join(game_id.clone()).clone(); - create_dir_all(base_path.clone()).unwrap(); + let base_path = Path::new(&self.base_dir); + create_dir_all(base_path).unwrap(); for (raw_path, chunk) in manifest { let path = base_path.join(Path::new(&raw_path)); @@ -219,6 +225,7 @@ impl GameDownloadAgent { path: path.clone(), checksum: chunk.checksums[index].clone(), length: *length, + permissions: chunk.permissions, }); running_offset += *length as u64; } diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/downloads/download_logic.rs index 6735c56..8f82962 100644 --- a/src-tauri/src/downloads/download_logic.rs +++ b/src-tauri/src/downloads/download_logic.rs @@ -6,8 +6,11 @@ use crate::DB; use log::warn; use md5::{Context, Digest}; use reqwest::blocking::Response; +use tauri::utils::acl::Permission; +use std::fs::{set_permissions, Permissions}; use std::io::Read; +use std::os::unix::fs::PermissionsExt; use std::{ fs::{File, OpenOptions}, io::{self, BufWriter, Seek, SeekFrom, Write}, @@ -157,7 +160,7 @@ pub fn download_game_chunk( )); } - let mut destination = DropWriter::new(ctx.path); + let mut destination = DropWriter::new(ctx.path.clone()); if ctx.offset != 0 { destination @@ -185,6 +188,13 @@ pub fn download_game_chunk( return Ok(false); }; + // If we complete the file, set the permissions (if on Linux) + #[cfg(unix)] + { + let permissions = Permissions::from_mode(ctx.permissions); + set_permissions(ctx.path, permissions).unwrap(); + } + /* let checksum = pipeline .finish() diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index ca18df5..dfe429d 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -220,9 +220,12 @@ impl DownloadManagerBuilder { info!("Popping consumed data"); let download_agent = self.remove_and_cleanup_game(&game_id); - if let Err(error) = - on_game_complete(game_id, download_agent.version.clone(), &self.app_handle) - { + if let Err(error) = on_game_complete( + game_id, + download_agent.version.clone(), + download_agent.base_dir.clone(), + &self.app_handle, + ) { self.sender .send(DownloadManagerSignal::Error( GameDownloadError::Communication(error), diff --git a/src-tauri/src/downloads/manifest.rs b/src-tauri/src/downloads/manifest.rs index d815861..7c5b3a9 100644 --- a/src-tauri/src/downloads/manifest.rs +++ b/src-tauri/src/downloads/manifest.rs @@ -6,7 +6,7 @@ pub type DropManifest = HashMap; #[derive(Serialize, Deserialize, Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct DropChunk { - pub permissions: usize, + pub permissions: u32, pub ids: Vec, pub checksums: Vec, pub lengths: Vec, @@ -23,4 +23,5 @@ pub struct DropDownloadContext { pub path: PathBuf, pub checksum: String, pub length: usize, + pub permissions: u32, } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8e4e2fc..b74eb98 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,6 +26,7 @@ use log4rs::append::file::FileAppender; use log4rs::config::{Appender, Root}; use log4rs::encode::pattern::PatternEncoder; use log4rs::Config; +use process::process_commands::launch_game; use process::process_manager::ProcessManager; use remote::{gen_drop_url, use_remote}; use serde::{Deserialize, Serialize}; @@ -67,7 +68,7 @@ pub struct AppState { #[serde(skip_serializing)] download_manager: Arc, #[serde(skip_serializing)] - process_manager: Arc, + process_manager: Arc>, } #[tauri::command] @@ -104,7 +105,7 @@ fn setup(handle: AppHandle) -> AppState { let games = HashMap::new(); let download_manager = Arc::new(DownloadManagerBuilder::build(handle)); - let process_manager = Arc::new(ProcessManager::new()); + let process_manager = Arc::new(Mutex::new(ProcessManager::new())); debug!("Checking if database is set up"); let is_set_up = DB.database_is_set_up(); @@ -168,6 +169,8 @@ pub fn run() { move_game_in_queue, pause_game_downloads, resume_game_downloads, + // Processes + launch_game, ]) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index 15fa7b6..860729a 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -225,15 +225,12 @@ fn fetch_game_verion_options_logic<'a>( let data = response.json::>()?; let state_lock = state.lock().unwrap(); + let process_manager_lock = state_lock.process_manager.lock().unwrap(); let data = data .into_iter() - .filter(|v| { - state_lock - .process_manager - .valid_platform(&v.platform) - .unwrap() - }) + .filter(|v| process_manager_lock.valid_platform(&v.platform).unwrap()) .collect::>(); + drop(process_manager_lock); drop(state_lock); Ok(data) @@ -250,6 +247,7 @@ pub fn fetch_game_verion_options<'a>( pub fn on_game_complete( game_id: String, version_name: String, + install_dir: String, app_handle: &AppHandle, ) -> Result<(), RemoteAccessError> { // Fetch game version information from remote @@ -284,9 +282,15 @@ pub fn on_game_complete( DB.save().unwrap(); let status = if data.setup_command.is_empty() { - DatabaseGameStatus::Installed { version_name } + DatabaseGameStatus::Installed { + version_name, + install_dir, + } } else { - DatabaseGameStatus::SetupRequired { version_name } + DatabaseGameStatus::SetupRequired { + version_name, + install_dir, + } }; let mut db_handle = DB.borrow_data_mut().unwrap(); diff --git a/src-tauri/src/process/mod.rs b/src-tauri/src/process/mod.rs index 30cc14c..a4ceb45 100644 --- a/src-tauri/src/process/mod.rs +++ b/src-tauri/src/process/mod.rs @@ -1 +1,2 @@ pub mod process_manager; +pub mod process_commands; \ No newline at end of file diff --git a/src-tauri/src/process/process_commands.rs b/src-tauri/src/process/process_commands.rs new file mode 100644 index 0000000..327728f --- /dev/null +++ b/src-tauri/src/process/process_commands.rs @@ -0,0 +1,16 @@ +use std::sync::Mutex; + +use crate::AppState; + +#[tauri::command] +pub fn launch_game(game_id: String, state: tauri::State<'_, Mutex>) -> Result<(), String> { + let state_lock = state.lock().unwrap(); + let mut process_manager_lock = state_lock.process_manager.lock().unwrap(); + + process_manager_lock.launch_game(game_id)?; + + drop(process_manager_lock); + drop(state_lock); + + Ok(()) +} diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs index e0bda56..8eed7ef 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -1,22 +1,62 @@ -use std::{collections::HashMap, sync::LazyLock}; +use std::{ + collections::HashMap, + fs::{File, OpenOptions}, + path::PathBuf, + process::{Child, Command}, + sync::LazyLock, +}; +use log::info; use serde::{Deserialize, Serialize}; +use crate::{ + db::{DatabaseGameStatus, DATA_ROOT_DIR}, + DB, +}; + pub struct ProcessManager { current_platform: Platform, + log_output_dir: PathBuf, + processes: HashMap, } impl ProcessManager { pub fn new() -> Self { + let root_dir_lock = DATA_ROOT_DIR.lock().unwrap(); + let log_output_dir = root_dir_lock.join("logs"); + drop(root_dir_lock); + ProcessManager { current_platform: if cfg!(windows) { Platform::Windows } else { Platform::Linux }, + + processes: HashMap::new(), + log_output_dir, } } + fn process_command(&self, raw_command: String) -> (String, Vec) { + let command_components = raw_command.split(" ").collect::>(); + let root = match self.current_platform { + Platform::Windows => command_components[0].to_string(), + Platform::Linux => { + let mut root = command_components[0].to_string(); + if !root.starts_with("./") { + root = format!("{}{}", "./", root); + } + root + } + }; + let args = command_components[1..] + .into_iter() + .map(|v| v.to_string()) + .collect(); + (root, args) + } + pub fn valid_platform(&self, platform: &Platform) -> Result { let current = &self.current_platform; let valid_platforms = PROCESS_COMPATABILITY_MATRIX @@ -25,6 +65,63 @@ impl ProcessManager { Ok(valid_platforms.contains(platform)) } + + pub fn launch_game(&mut self, game_id: String) -> Result<(), String> { + if self.processes.contains_key(&game_id) { + return Err("Game or setup is already running.".to_owned()); + } + + let db_lock = DB.borrow_data().unwrap(); + let game_status = db_lock + .games + .games_statuses + .get(&game_id) + .ok_or("Game not installed")?; + + let DatabaseGameStatus::Installed { + version_name, + install_dir, + } = game_status + else { + return Err("Game not installed.".to_owned()); + }; + + let game_version = db_lock + .games + .game_versions + .get(&game_id) + .ok_or("Invalid game ID".to_owned())? + .get(version_name) + .ok_or("Invalid version name".to_owned())?; + + let (command, args) = self.process_command(game_version.launch_command.clone()); + + info!("launching process {} in {}", command, install_dir); + + let current_time = chrono::offset::Local::now(); + let log_file = OpenOptions::new() + .write(true) + .append(true) + .read(true) + .create(true) + .open(self.log_output_dir.join(format!( + "{}-{}.log", + game_id, + current_time.to_rfc3339() + ))) + .map_err(|v| v.to_string())?; + + let launch_process = Command::new(command) + .current_dir(install_dir) + .stdout(log_file) + .args(args) + .spawn() + .map_err(|v| v.to_string())?; + + self.processes.insert(game_id, launch_process); + + Ok(()) + } } #[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone)] From 0a20139a7ce6c53dbd30474250038c45395adae0 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Tue, 17 Dec 2024 20:29:54 +1100 Subject: [PATCH 141/164] feat(queue & game): queue and library UIs --- package.json | 4 + pages/library.vue | 2 +- pages/library/[id]/index.vue | 60 +++++++++++- pages/queue.vue | 87 ++++++++++++++++-- src-tauri/src/db.rs | 3 - src-tauri/src/downloads/download_logic.rs | 2 + src-tauri/src/downloads/download_manager.rs | 5 +- .../src/downloads/download_manager_builder.rs | 17 +++- src-tauri/src/lib.rs | 2 +- src-tauri/src/library.rs | 5 +- tailwind.config.js | 2 +- yarn.lock | 92 ++++++++++++++++++- 12 files changed, 255 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index c93e9a4..2ab82a4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "@tauri-apps/plugin-deep-link": "~2", "@tauri-apps/plugin-dialog": "^2.0.1", "@tauri-apps/plugin-shell": ">=2.0.0", + "markdown-it": "^14.1.0", + "moment": "^2.30.1", "nuxt": "^3.13.0", "scss": "^0.2.4", "vue": "latest", @@ -26,7 +28,9 @@ }, "devDependencies": { "@tailwindcss/forms": "^0.5.9", + "@tailwindcss/typography": "^0.5.15", "@tauri-apps/cli": ">=2.0.0", + "@types/markdown-it": "^14.1.2", "autoprefixer": "^10.4.20", "postcss": "^8.4.47", "sass-embedded": "^1.79.4", diff --git a/pages/library.vue b/pages/library.vue index ac7e24f..ca01cdb 100644 --- a/pages/library.vue +++ b/pages/library.vue @@ -1,6 +1,6 @@ +
+ No items in the queue +
diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 3068089..9930a83 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -7,6 +7,8 @@ use crate::DB; use core::time; use log::{debug, error, info}; use rayon::ThreadPoolBuilder; +use serde::ser::{Error, SerializeMap}; +use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::fs::{create_dir_all, File}; use std::io; @@ -92,6 +94,30 @@ impl GameDownloadAgent { } } + pub fn from_contexts( + id: String, + version: String, + base_dir: String, + manifest: DropManifest, + contexts: Vec, + sender: Sender, + ) -> Self { + let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); + + let me = Self { + id, + version, + control_flag, + manifest: Mutex::new(Some(manifest)), + base_dir, + contexts: Mutex::new(contexts), + progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), + sender, + }; + me.set_progress_object_params(); + me + } + // Blocking pub fn setup_download(&self) -> Result<(), GameDownloadError> { self.ensure_manifest_exists()?; @@ -307,3 +333,25 @@ impl GameDownloadAgent { Ok(()) } } + +#[derive(Serialize, Deserialize)] +pub struct GameDownloadAgentOfflineState { + id: String, + version: String, + base_dir: String, + manifest: DropManifest, + contexts: Vec, +} + +impl GameDownloadAgentOfflineState { + fn to_download_agent(self, sender: Sender) -> GameDownloadAgent { + GameDownloadAgent::from_contexts( + self.id, + self.version, + self.base_dir, + self.manifest, + self.contexts, + sender, + ) + } +} diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index ffc0b00..36313a9 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -30,21 +30,22 @@ pub enum DownloadManagerSignal { /// to the registry and queue Queue(String, String, usize), /// Tells the Manager to stop the current - /// download and return + /// download, sync everything to disk, and + /// then exit Finish, Cancel, /// Any error which occurs in the agent Error(GameDownloadError), /// Pushes UI update Update, - /// Causes the Download Agent status to be synced to disk - Sync(usize), } + pub enum DownloadManagerStatus { Downloading, Paused, Empty, Error(GameDownloadError), + Finished, } #[derive(Serialize, Clone)] diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index e89a588..6ccf8c8 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -141,6 +141,19 @@ impl DownloadManagerBuilder { self.app_handle.emit("update_queue", event_data).unwrap(); } + fn stop_and_wait_current_download(&self) { + self.set_status(DownloadManagerStatus::Paused); + if let Some(current_flag) = &self.active_control_flag { + current_flag.set(DownloadThreadControlFlag::Stop); + } + + let mut download_thread_lock = self.current_download_thread.lock().unwrap(); + if let Some(current_download_thread) = download_thread_lock.take() { + current_download_thread.join().unwrap(); + } + drop(download_thread_lock); + } + fn sync_download_agent(&self) {} fn remove_and_cleanup_game(&mut self, game_id: &String) -> Arc { @@ -182,12 +195,6 @@ impl DownloadManagerBuilder { DownloadManagerSignal::Queue(game_id, version, target_download_dir) => { self.manage_queue_signal(game_id, version, target_download_dir); } - DownloadManagerSignal::Finish => { - if let Some(active_control_flag) = self.active_control_flag { - active_control_flag.set(DownloadThreadControlFlag::Stop) - } - return Ok(()); - } DownloadManagerSignal::Error(e) => { self.manage_error_signal(e); } @@ -197,8 +204,9 @@ impl DownloadManagerBuilder { DownloadManagerSignal::Update => { self.push_manager_update(); } - DownloadManagerSignal::Sync(index) => { - self.sync_download_agent(); + DownloadManagerSignal::Finish => { + self.stop_and_wait_current_download(); + return Ok(()); } }; } @@ -345,16 +353,7 @@ impl DownloadManagerBuilder { self.sender.send(DownloadManagerSignal::Update).unwrap(); } fn manage_cancel_signal(&mut self) { - self.set_status(DownloadManagerStatus::Paused); - if let Some(current_flag) = &self.active_control_flag { - current_flag.set(DownloadThreadControlFlag::Stop); - } - - let mut download_thread_lock = self.current_download_thread.lock().unwrap(); - if let Some(current_download_thread) = download_thread_lock.take() { - current_download_thread.join().unwrap(); - } - drop(download_thread_lock); + self.stop_and_wait_current_download(); info!("cancel waited for download to finish"); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b3ab431..acecd5f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -35,10 +35,11 @@ use std::{ collections::HashMap, sync::{LazyLock, Mutex}, }; -use tauri::menu::{Menu, MenuItem, MenuItemBuilder}; +use tauri::menu::{Menu, MenuItem, MenuItemBuilder, PredefinedMenuItem}; use tauri::tray::TrayIconBuilder; -use tauri::{AppHandle, Manager}; +use tauri::{AppHandle, Manager, RunEvent, WindowEvent}; use tauri_plugin_deep_link::DeepLinkExt; +use url::Url; #[derive(Clone, Copy, Serialize)] pub enum AppStatus { @@ -133,6 +134,12 @@ fn setup(handle: AppHandle) -> AppState { } } +pub fn cleanup_and_exit(app: &AppHandle) { + info!("exiting drop application..."); + + app.exit(0); +} + pub static DB: LazyLock = LazyLock::new(DatabaseInterface::set_up_database); #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -216,9 +223,13 @@ pub fn run() { let menu = Menu::with_items( app, &[ + &MenuItem::with_id(app, "open", "Open", true, None::<&str>)?, + &PredefinedMenuItem::separator(app)?, + /* &MenuItem::with_id(app, "show_library", "Library", true, None::<&str>)?, &MenuItem::with_id(app, "show_settings", "Settings", true, None::<&str>)?, - &MenuItem::with_id(app, "open", "Open", true, None::<&str>)?, + &PredefinedMenuItem::separator(app)?, + */ &MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?, ], )?; @@ -226,6 +237,18 @@ pub fn run() { let tray = TrayIconBuilder::new() .icon(app.default_window_icon().unwrap().clone()) .menu(&menu) + .on_menu_event(|app, event| match event.id.as_ref() { + "open" => { + app.webview_windows().get("main").unwrap().show().unwrap(); + } + "quit" => { + cleanup_and_exit(app); + } + + _ => { + println!("Menu event not handled: {:?}", event.id); + } + }) .build(app) .expect("error while setting up tray menu"); @@ -260,12 +283,22 @@ pub fn run() { responder.respond(resp); }) + .on_window_event(|window, event| match event { + WindowEvent::CloseRequested { api, .. } => { + window.hide().unwrap(); + api.prevent_close(); + } + _ => (), + }) .build(tauri::generate_context!()) .expect("error while running tauri application"); - app.run(|app_handle, e| match e { + app.run(|app_handle, event| match event { + RunEvent::ExitRequested { code, api, .. } => { + if code.is_none() { + api.prevent_exit(); + } + } _ => {} }); - - info!("exiting drop application..."); } From 64ebc191bf50a81b998e3a54cb447fb91cb0f71f Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sat, 21 Dec 2024 19:21:15 +1100 Subject: [PATCH 144/164] chore(download agent): moved to completed index arr to help serialization --- src-tauri/src/downloads/download_agent.rs | 109 ++++++------------ .../src/downloads/download_manager_builder.rs | 47 +++++--- src-tauri/src/lib.rs | 2 +- 3 files changed, 64 insertions(+), 94 deletions(-) diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs index 9930a83..5a6fad7 100644 --- a/src-tauri/src/downloads/download_agent.rs +++ b/src-tauri/src/downloads/download_agent.rs @@ -31,7 +31,8 @@ pub struct GameDownloadAgent { pub version: String, pub control_flag: DownloadThreadControl, pub base_dir: String, - contexts: Mutex>, + contexts: Vec, + completed_contexts: Mutex>, pub manifest: Mutex>, pub progress: Arc, sender: Sender, @@ -56,7 +57,7 @@ impl Display for GameDownloadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { GameDownloadError::Communication(error) => write!(f, "{}", error), - GameDownloadError::Setup(error) => write!(f, "{:?}", error), + GameDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", error), GameDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"), GameDownloadError::Checksum => write!(f, "Checksum failed to validate for download"), GameDownloadError::IoError(error) => write!(f, "{}", error), @@ -65,6 +66,14 @@ impl Display for GameDownloadError { } } +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"), + } + } +} + impl GameDownloadAgent { pub fn new( id: String, @@ -88,38 +97,15 @@ impl GameDownloadAgent { control_flag, manifest: Mutex::new(None), base_dir: data_base_dir_path.to_str().unwrap().to_owned(), - contexts: Mutex::new(Vec::new()), + contexts: Vec::new(), + completed_contexts: Mutex::new(Vec::new()), progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), sender, } } - pub fn from_contexts( - id: String, - version: String, - base_dir: String, - manifest: DropManifest, - contexts: Vec, - sender: Sender, - ) -> Self { - let control_flag = DownloadThreadControl::new(DownloadThreadControlFlag::Stop); - - let me = Self { - id, - version, - control_flag, - manifest: Mutex::new(Some(manifest)), - base_dir, - contexts: Mutex::new(contexts), - progress: Arc::new(ProgressObject::new(0, 0, sender.clone())), - sender, - }; - me.set_progress_object_params(); - me - } - // Blocking - pub fn setup_download(&self) -> Result<(), GameDownloadError> { + pub fn setup_download(&mut self) -> Result<(), GameDownloadError> { self.ensure_manifest_exists()?; info!("Ensured manifest exists"); @@ -132,7 +118,7 @@ impl GameDownloadAgent { } // Blocking - pub fn download(&self) -> Result<(), GameDownloadError> { + pub fn download(&mut self) -> Result<(), GameDownloadError> { self.setup_download()?; self.set_progress_object_params(); let timer = Instant::now(); @@ -200,10 +186,9 @@ impl GameDownloadAgent { return; } - let lock = self.contexts.lock().unwrap(); - let length = lock.len(); + let length = self.contexts.len(); - let chunk_count = lock.iter().map(|chunk| chunk.length).sum(); + let chunk_count = self.contexts.iter().map(|chunk| chunk.length).sum(); debug!("Setting ProgressObject max to {}", chunk_count); self.progress.set_max(chunk_count); @@ -213,18 +198,16 @@ impl GameDownloadAgent { self.progress.set_time_now(); } - pub fn ensure_contexts(&self) -> Result<(), GameDownloadError> { - let context_lock = self.contexts.lock().unwrap(); - if !context_lock.is_empty() { + pub fn ensure_contexts(&mut self) -> Result<(), GameDownloadError> { + if !self.contexts.is_empty() { return Ok(()); } - drop(context_lock); self.generate_contexts()?; Ok(()) } - pub fn generate_contexts(&self) -> Result<(), GameDownloadError> { + pub fn generate_contexts(&mut self) -> Result<(), GameDownloadError> { let manifest = self.manifest.lock().unwrap().clone().unwrap(); let game_id = self.id.clone(); @@ -261,13 +244,9 @@ impl GameDownloadAgent { let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset); } } + self.contexts = contexts; - if let Ok(mut context_lock) = self.contexts.lock() { - *context_lock = contexts; - return Ok(()); - } - - Err(GameDownloadError::Setup(SetupError::Context)) + Ok(()) } pub fn run(&self) -> Result<(), ()> { @@ -283,9 +262,14 @@ impl GameDownloadAgent { let completed_indexes_loop_arc = completed_indexes.clone(); pool.scope(move |scope| { - let contexts = self.contexts.lock().unwrap(); + let completed_lock = self.completed_contexts.lock().unwrap(); + + for (index, context) in self.contexts.iter().enumerate() { + // If we've done this one already, skip it + if completed_lock.contains(&index) { + continue; + } - for (index, context) in contexts.iter().enumerate() { let context = context.clone(); let control_flag = self.control_flag.clone(); // Clone arcs let progress = self.progress.get(index); // Clone arcs @@ -309,18 +293,13 @@ impl GameDownloadAgent { } }); - let mut context_lock = self.contexts.lock().unwrap(); - let mut completed_lock = completed_indexes.lock().unwrap(); + let mut completed_lock = self.completed_contexts.lock().unwrap(); + let newly_completed_lock = completed_indexes.lock().unwrap(); - // Sort desc so we don't have to modify indexes - completed_lock.sort_by(|a, b| b.cmp(a)); - - for index in completed_lock.iter() { - context_lock.remove(*index); - } + completed_lock.extend(newly_completed_lock.iter()); // If we're not out of contexts, we're not done, so we don't fire completed - if !context_lock.is_empty() { + if completed_lock.len() != self.contexts.len() { info!("da for {} exited without completing", self.id.clone()); return Ok(()); } @@ -333,25 +312,3 @@ impl GameDownloadAgent { Ok(()) } } - -#[derive(Serialize, Deserialize)] -pub struct GameDownloadAgentOfflineState { - id: String, - version: String, - base_dir: String, - manifest: DropManifest, - contexts: Vec, -} - -impl GameDownloadAgentOfflineState { - fn to_download_agent(self, sender: Sender) -> GameDownloadAgent { - GameDownloadAgent::from_contexts( - self.id, - self.version, - self.base_dir, - self.manifest, - self.contexts, - sender, - ) - } -} diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 6ccf8c8..9e42598 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -68,7 +68,7 @@ Behold, my madness - quexeky pub type CurrentProgressObject = Arc>>>; pub struct DownloadManagerBuilder { - download_agent_registry: HashMap>, + download_agent_registry: HashMap>>, download_queue: Queue, command_receiver: Receiver, sender: Sender, @@ -156,7 +156,7 @@ impl DownloadManagerBuilder { fn sync_download_agent(&self) {} - fn remove_and_cleanup_game(&mut self, game_id: &String) -> Arc { + fn remove_and_cleanup_game(&mut self, game_id: &String) -> Arc> { self.download_queue.pop_front(); let download_agent = self.download_agent_registry.remove(game_id).unwrap(); self.cleanup_current_download(); @@ -227,13 +227,16 @@ impl DownloadManagerBuilder { if interface.id == game_id { info!("Popping consumed data"); let download_agent = self.remove_and_cleanup_game(&game_id); + let download_agent_lock = download_agent.lock().unwrap(); - if let Err(error) = on_game_complete( - game_id, - download_agent.version.clone(), - download_agent.base_dir.clone(), - &self.app_handle, - ) { + let version = download_agent_lock.version.clone(); + let install_dir = download_agent_lock.base_dir.clone(); + + drop(download_agent_lock); + + if let Err(error) = + on_game_complete(game_id, version, install_dir, &self.app_handle) + { self.sender .send(DownloadManagerSignal::Error( GameDownloadError::Communication(error), @@ -248,19 +251,24 @@ impl DownloadManagerBuilder { fn manage_queue_signal(&mut self, id: String, version: String, target_download_dir: usize) { info!("Got signal Queue"); - let download_agent = Arc::new(GameDownloadAgent::new( + let download_agent = Arc::new(Mutex::new(GameDownloadAgent::new( id.clone(), version, target_download_dir, self.sender.clone(), - )); + ))); + let download_agent_lock = download_agent.lock().unwrap(); + let agent_status = GameDownloadStatus::Queued; let interface_data = GameDownloadAgentQueueStandin { id: id.clone(), status: Mutex::new(agent_status), - progress: download_agent.progress.clone(), + progress: download_agent_lock.progress.clone(), }; - let version_name = download_agent.version.clone(); + let version_name = download_agent_lock.version.clone(); + + drop(download_agent_lock); + self.download_agent_registry .insert(interface_data.id.clone(), download_agent); self.download_queue.append(interface_data); @@ -287,24 +295,28 @@ impl DownloadManagerBuilder { .get(&agent_data.id) .unwrap() .clone(); + let download_agent_lock = download_agent.lock().unwrap(); self.current_download_agent = Some(agent_data); // Cloning option should be okay because it only clones the Arc inside, not the AgentInterfaceData let agent_data = self.current_download_agent.clone().unwrap(); - let version_name = download_agent.version.clone(); + let version_name = download_agent_lock.version.clone(); - let progress_object = download_agent.progress.clone(); + let progress_object = download_agent_lock.progress.clone(); *self.progress.lock().unwrap() = Some(progress_object); - let active_control_flag = download_agent.control_flag.clone(); + let active_control_flag = download_agent_lock.control_flag.clone(); self.active_control_flag = Some(active_control_flag.clone()); let sender = self.sender.clone(); + drop(download_agent_lock); + info!("Spawning download"); let mut download_thread_lock = self.current_download_thread.lock().unwrap(); *download_thread_lock = Some(spawn(move || { - match download_agent.download() { + let mut download_agent_lock = download_agent.lock().unwrap(); + match download_agent_lock.download() { // Returns once we've exited the download // (not necessarily completed) // The download agent will fire the completed event for us @@ -315,6 +327,7 @@ impl DownloadManagerBuilder { sender.send(DownloadManagerSignal::Error(err)).unwrap(); } }; + drop(download_agent_lock); })); // Set status for games @@ -347,7 +360,7 @@ impl DownloadManagerBuilder { *lock = GameDownloadStatus::Error; self.set_status(DownloadManagerStatus::Error(error)); - let game_id = self.current_download_agent.as_ref().unwrap().id.clone(); + let game_id = current_status.id.clone(); self.set_game_status(game_id, DatabaseGameStatus::Remote {}); self.sender.send(DownloadManagerSignal::Update).unwrap(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index acecd5f..6e25768 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -234,7 +234,7 @@ pub fn run() { ], )?; - let tray = TrayIconBuilder::new() + TrayIconBuilder::new() .icon(app.default_window_icon().unwrap().clone()) .menu(&menu) .on_menu_event(|app, event| match event.id.as_ref() { From 42c0198f1d95a2e95ec53489f572c3e038c33393 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 23 Dec 2024 20:44:02 +1100 Subject: [PATCH 145/164] refactor(game status): transient vs synced state now defined --- components/GameStatusButton.vue | 8 +- composables/game.ts | 34 ++++++-- composables/state-navigation.ts | 4 + pages/library/[id]/index.vue | 80 +++++-------------- pages/queue.vue | 21 +++-- src-tauri/src/db.rs | 27 ++++--- src-tauri/src/downloads/download_commands.rs | 5 ++ src-tauri/src/downloads/download_manager.rs | 10 ++- .../src/downloads/download_manager_builder.rs | 65 ++++++++++++--- src-tauri/src/lib.rs | 3 +- src-tauri/src/library.rs | 68 +++++++--------- src-tauri/src/process/process_manager.rs | 8 +- src-tauri/src/settings.rs | 1 - src-tauri/src/state.rs | 31 +++++++ 14 files changed, 220 insertions(+), 145 deletions(-) delete mode 100644 src-tauri/src/settings.rs create mode 100644 src-tauri/src/state.rs diff --git a/components/GameStatusButton.vue b/components/GameStatusButton.vue index 972de00..a71fca4 100644 --- a/components/GameStatusButton.vue +++ b/components/GameStatusButton.vue @@ -30,8 +30,8 @@ import { GameStatusEnum, type GameStatus } from "~/types.js"; const props = defineProps<{ status: GameStatus }>(); const emit = defineEmits<{ (e: "install"): void; - (e: "cancel"): void; (e: "play"): void; + (e: "queue"): void; }>(); const styles: { [key in GameStatusEnum]: string } = { @@ -71,11 +71,11 @@ const buttonIcons: { [key in GameStatusEnum]: Component } = { const buttonActions: { [key in GameStatusEnum]: () => void } = { [GameStatusEnum.Remote]: () => emit("install"), - [GameStatusEnum.Queued]: () => emit("cancel"), - [GameStatusEnum.Downloading]: () => emit("cancel"), + [GameStatusEnum.Queued]: () => emit("queue"), + [GameStatusEnum.Downloading]: () => emit("queue"), [GameStatusEnum.SetupRequired]: () => {}, [GameStatusEnum.Installed]: () => emit("play"), - [GameStatusEnum.Updating]: () => emit("cancel"), + [GameStatusEnum.Updating]: () => emit("queue"), [GameStatusEnum.Uninstalling]: () => {}, }; diff --git a/composables/game.ts b/composables/game.ts index 5eba1d9..7f28435 100644 --- a/composables/game.ts +++ b/composables/game.ts @@ -1,14 +1,36 @@ import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import type { Game, GameStatus } from "~/types"; +import type { Game, GameStatus, GameStatusEnum } from "~/types"; const gameRegistry: { [key: string]: Game } = {}; const gameStatusRegistry: { [key: string]: Ref } = {}; +type OptionGameStatus = { [key in GameStatusEnum]: { version_name?: string } }; +export type SerializedGameStatus = [ + { type: GameStatusEnum }, + OptionGameStatus | null +]; + +const parseStatus = (status: SerializedGameStatus): GameStatus => { + if (status[0]) { + return { + type: status[0].type, + }; + } else if (status[1]) { + const [[gameStatus, options]] = Object.entries(status[1]); + return { + type: gameStatus as GameStatusEnum, + ...options, + }; + } else { + throw new Error("No game status"); + } +}; + export const useGame = async (id: string) => { if (!gameRegistry[id]) { - const data: { game: Game; status: GameStatus } = await invoke( + const data: { game: Game; status: SerializedGameStatus } = await invoke( "fetch_game", { id, @@ -16,11 +38,13 @@ export const useGame = async (id: string) => { ); gameRegistry[id] = data.game; if (!gameStatusRegistry[id]) { - gameStatusRegistry[id] = ref(data.status); + gameStatusRegistry[id] = ref(parseStatus(data.status)); listen(`update_game/${id}`, (event) => { - const payload: { status: GameStatus } = event.payload as any; - gameStatusRegistry[id].value = payload.status; + const payload: { + status: SerializedGameStatus; + } = event.payload as any; + gameStatusRegistry[id].value = parseStatus(payload.status); }); } } diff --git a/composables/state-navigation.ts b/composables/state-navigation.ts index 4658eb4..790d258 100644 --- a/composables/state-navigation.ts +++ b/composables/state-navigation.ts @@ -18,10 +18,14 @@ export function setupHooks() { router.push("/store"); }); + /* + document.addEventListener("contextmenu", (event) => { event.target?.dispatchEvent(new Event("contextmenu")); event.preventDefault(); }); + + */ } export function initialNavigation(state: Ref) { diff --git a/pages/library/[id]/index.vue b/pages/library/[id]/index.vue index c5678ec..4456b11 100644 --- a/pages/library/[id]/index.vue +++ b/pages/library/[id]/index.vue @@ -6,7 +6,7 @@

{{ game.mName }}

@@ -17,40 +17,23 @@
-
+
-
+ + @@ -349,45 +332,23 @@ import { ListboxOptions, } from "@headlessui/vue"; import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid"; +import { BuildingStorefrontIcon } from "@heroicons/vue/24/outline"; import { XCircleIcon } from "@heroicons/vue/24/solid"; import { invoke } from "@tauri-apps/api/core"; -import MarkdownIt from "markdown-it"; -import moment from "moment"; const route = useRoute(); +const router = useRouter(); const id = route.params.id.toString(); const { game: rawGame, status } = await useGame(id); const game = ref(rawGame); +const remoteUrl: string = await invoke("gen_drop_url", { + path: `/store/${game.value.id}`, +}); + const bannerUrl = await useObject(game.value.mBannerId); -const md = MarkdownIt(); - -const showPreview = ref(true); -const gameDescriptionCharacters = game.value.mDescription.split(""); - -// First new line after x characters -const descriptionSplitIndex = gameDescriptionCharacters.findIndex( - (v, i, arr) => { - // If we're at the last element, we return true. - // So we don't have to handle a -1 from this findIndex - if (i + 1 == arr.length) return true; - if (i < 500) return false; - if (v != "\n") return false; - return true; - } -); - -const previewDescription = gameDescriptionCharacters - .slice(0, descriptionSplitIndex + 1) // Slice a character after - .join(""); -const previewHTML = md.render(previewDescription); - -const descriptionHTML = md.render(game.value.mDescription); - -const showReadMore = previewHTML != descriptionHTML; - const installFlowOpen = ref(false); const versionOptions = ref< undefined | Array<{ versionName: string; platform: string }> @@ -432,8 +393,11 @@ async function play() { try { await invoke("launch_game", { gameId: game.value.id }); } catch (e) { - game.value.mName = e as string; console.error(e); } } + +async function queue() { + router.push("/queue"); +} diff --git a/pages/queue.vue b/pages/queue.vue index d00466c..6572203 100644 --- a/pages/queue.vue +++ b/pages/queue.vue @@ -5,9 +5,9 @@
  • -
    +

    - + {{ games[element.id].game.mName }} @@ -40,10 +40,12 @@ />

    -
  • Loading...

    @@ -59,6 +61,7 @@ diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index b89d2ce..00e93ab 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -24,11 +24,8 @@ pub struct DatabaseAuth { // Strings are version names for a particular game #[derive(Serialize, Clone, Deserialize)] #[serde(tag = "type")] -pub enum DatabaseGameStatus { +pub enum GameStatus { Remote {}, - Downloading { - version_name: String, - }, SetupRequired { version_name: String, install_dir: String, @@ -37,10 +34,14 @@ pub enum DatabaseGameStatus { version_name: String, install_dir: String, }, - Updating { - version_name: String, - }, +} + +// Stuff that shouldn't be synced to disk +#[derive(Clone, Serialize)] +pub enum GameTransientStatus { + Downloading { version_name: String }, Uninstalling {}, + Updating { version_name: String }, } #[derive(Serialize, Deserialize, Clone)] @@ -58,8 +59,11 @@ pub struct GameVersion { pub struct DatabaseGames { pub install_dirs: Vec, // Guaranteed to exist if the game also exists in the app state map - pub games_statuses: HashMap, - pub game_versions: HashMap>, + pub statuses: HashMap, + pub versions: HashMap>, + + #[serde(skip)] + pub transient_statuses: HashMap, } #[derive(Serialize, Clone, Deserialize)] @@ -119,8 +123,9 @@ impl DatabaseImpls for DatabaseInterface { base_url: "".to_string(), games: DatabaseGames { install_dirs: vec![games_base_dir.to_str().unwrap().to_string()], - games_statuses: HashMap::new(), - game_versions: HashMap::new(), + statuses: HashMap::new(), + transient_statuses: HashMap::new(), + versions: HashMap::new(), }, }; debug!( diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/downloads/download_commands.rs index 23b8bab..d3ca2e7 100644 --- a/src-tauri/src/downloads/download_commands.rs +++ b/src-tauri/src/downloads/download_commands.rs @@ -40,6 +40,11 @@ pub fn move_game_in_queue( .rearrange(old_index, new_index) } +#[tauri::command] +pub fn cancel_game(state: tauri::State<'_, Mutex>, game_id: String) { + state.lock().unwrap().download_manager.cancel(game_id) +} + /* #[tauri::command] pub fn get_current_write_speed(state: tauri::State<'_, Mutex>) {} diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/downloads/download_manager.rs index 36313a9..8676087 100644 --- a/src-tauri/src/downloads/download_manager.rs +++ b/src-tauri/src/downloads/download_manager.rs @@ -33,7 +33,10 @@ pub enum DownloadManagerSignal { /// download, sync everything to disk, and /// then exit Finish, + /// Stops (but doesn't remove) current download Cancel, + /// Removes a given game + Remove(String), /// Any error which occurs in the agent Error(GameDownloadError), /// Pushes UI update @@ -142,6 +145,11 @@ impl DownloadManager { .send(DownloadManagerSignal::Update) .unwrap(); } + pub fn cancel(&self, game_id: String) { + self.command_sender + .send(DownloadManagerSignal::Remove(game_id)) + .unwrap(); + } pub fn rearrange(&self, current_index: usize, new_index: usize) { if current_index == new_index { return; @@ -159,8 +167,8 @@ impl DownloadManager { 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 { self.command_sender.send(DownloadManagerSignal::Go).unwrap(); diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs index 9e42598..c24bbe3 100644 --- a/src-tauri/src/downloads/download_manager_builder.rs +++ b/src-tauri/src/downloads/download_manager_builder.rs @@ -2,7 +2,7 @@ use std::{ collections::HashMap, sync::{ mpsc::{channel, Receiver, Sender}, - Arc, Mutex, + Arc, Mutex, RwLockWriteGuard, }, thread::{spawn, JoinHandle}, }; @@ -11,8 +11,9 @@ use log::{error, info}; use tauri::{AppHandle, Emitter}; use crate::{ - db::DatabaseGameStatus, + db::{Database, GameStatus, GameTransientStatus}, library::{on_game_complete, GameUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData}, + state::GameStatusManager, DB, }; @@ -107,14 +108,18 @@ impl DownloadManagerBuilder { DownloadManager::new(terminator, queue, active_progress, command_sender) } - fn set_game_status(&self, id: String, status: DatabaseGameStatus) { + fn set_game_status, &String) -> ()>( + &self, + id: String, + setter: F, + ) { let mut db_handle = DB.borrow_data_mut().unwrap(); - db_handle - .games - .games_statuses - .insert(id.clone(), status.clone()); + setter(&mut db_handle, &id); drop(db_handle); DB.save().unwrap(); + + let status = GameStatusManager::fetch_state(&id); + self.app_handle .emit( &format!("update_game/{}", id), @@ -208,10 +213,35 @@ impl DownloadManagerBuilder { self.stop_and_wait_current_download(); return Ok(()); } + DownloadManagerSignal::Remove(game_id) => { + self.manage_remove_game(game_id); + } }; } } + fn manage_remove_game(&mut self, game_id: String) { + if let Some(current_download) = &self.current_download_agent { + if current_download.id == game_id { + self.manage_cancel_signal(); + } + } + + let index = self.download_queue.get_by_id(game_id.clone()).unwrap(); + let mut queue_handle = self.download_queue.edit(); + queue_handle.remove(index); + self.set_game_status(game_id, |db_handle, id| { + db_handle.games.transient_statuses.remove(id); + }); + drop(queue_handle); + + if self.current_download_agent.is_none() { + self.manage_go_signal(); + } + + self.push_manager_update(); + } + fn manage_stop_signal(&mut self) { info!("Got signal 'Stop'"); self.set_status(DownloadManagerStatus::Paused); @@ -273,7 +303,12 @@ impl DownloadManagerBuilder { .insert(interface_data.id.clone(), download_agent); self.download_queue.append(interface_data); - self.set_game_status(id, DatabaseGameStatus::Downloading { version_name }); + self.set_game_status(id, |db, id| { + db.games.transient_statuses.insert( + id.to_string(), + GameTransientStatus::Downloading { version_name }, + ); + }); self.sender.send(DownloadManagerSignal::Update).unwrap(); } @@ -344,10 +379,12 @@ impl DownloadManagerBuilder { // Set flags for download manager active_control_flag.set(DownloadThreadControlFlag::Go); self.set_status(DownloadManagerStatus::Downloading); - self.set_game_status( - self.current_download_agent.as_ref().unwrap().id.clone(), - DatabaseGameStatus::Downloading { version_name }, - ); + self.set_game_status(agent_data.id.clone(), |db, id| { + db.games.transient_statuses.insert( + id.to_string(), + GameTransientStatus::Downloading { version_name }, + ); + }); self.sender.send(DownloadManagerSignal::Update).unwrap(); } @@ -361,7 +398,9 @@ impl DownloadManagerBuilder { self.set_status(DownloadManagerStatus::Error(error)); let game_id = current_status.id.clone(); - self.set_game_status(game_id, DatabaseGameStatus::Remote {}); + self.set_game_status(game_id, |db_handle, id| { + db_handle.games.transient_statuses.remove(id); + }); self.sender.send(DownloadManagerSignal::Update).unwrap(); } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6e25768..0d19ea6 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -5,7 +5,7 @@ mod library; mod process; mod remote; -mod settings; +mod state; #[cfg(test)] mod tests; @@ -178,6 +178,7 @@ pub fn run() { move_game_in_queue, pause_game_downloads, resume_game_downloads, + cancel_game, // Processes launch_game, ]) diff --git a/src-tauri/src/library.rs b/src-tauri/src/library.rs index d4d7f63..6e8536e 100644 --- a/src-tauri/src/library.rs +++ b/src-tauri/src/library.rs @@ -5,18 +5,19 @@ use tauri::Emitter; use tauri::{AppHandle, Manager}; use urlencoding::encode; -use crate::db::DatabaseGameStatus; use crate::db::DatabaseImpls; use crate::db::GameVersion; +use crate::db::{GameStatus, GameTransientStatus}; use crate::downloads::download_manager::GameDownloadStatus; use crate::process::process_manager::Platform; use crate::remote::RemoteAccessError; +use crate::state::{GameStatusManager, GameStatusWithTransient}; use crate::{auth::generate_authorization_header, AppState, DB}; #[derive(serde::Serialize)] pub struct FetchGameStruct { game: Game, - status: DatabaseGameStatus, + status: GameStatusWithTransient, } #[derive(Serialize, Deserialize, Clone)] @@ -36,7 +37,7 @@ pub struct Game { #[derive(serde::Serialize, Clone)] pub struct GameUpdateEvent { pub game_id: String, - pub status: DatabaseGameStatus, + pub status: (Option, Option), } #[derive(Serialize, Clone)] @@ -61,6 +62,7 @@ pub struct GameVersionOption { setup_command: String, launch_command: String, delta: bool, + umu_id_override: Option, // total_size: usize, } @@ -89,11 +91,11 @@ fn fetch_library_logic(app: AppHandle) -> Result, RemoteAccessError> { for game in games.iter() { handle.games.insert(game.id.clone(), game.clone()); - if !db_handle.games.games_statuses.contains_key(&game.id) { + if !db_handle.games.statuses.contains_key(&game.id) { db_handle .games - .games_statuses - .insert(game.id.clone(), DatabaseGameStatus::Remote {}); + .statuses + .insert(game.id.clone(), GameStatus::Remote {}); } } @@ -116,16 +118,11 @@ fn fetch_game_logic( let game = state_handle.games.get(&id); if let Some(game) = game { - let db_handle = DB.borrow_data().unwrap(); + let status = GameStatusManager::fetch_state(&id); let data = FetchGameStruct { game: game.clone(), - status: db_handle - .games - .games_statuses - .get(&game.id) - .unwrap() - .clone(), + status, }; return Ok(data); @@ -158,28 +155,23 @@ fn fetch_game_logic( db_handle .games - .games_statuses - .entry(id) - .or_insert(DatabaseGameStatus::Remote {}); + .statuses + .entry(id.clone()) + .or_insert(GameStatus::Remote {}); + drop(db_handle); + + let status = GameStatusManager::fetch_state(&id); let data = FetchGameStruct { game: game.clone(), - status: db_handle - .games - .games_statuses - .get(&game.id) - .unwrap() - .clone(), + status, }; Ok(data) } #[tauri::command] -pub fn fetch_game( - id: String, - app: tauri::AppHandle, -) -> Result { +pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result { let result = fetch_game_logic(id, app); if result.is_err() { @@ -190,15 +182,8 @@ pub fn fetch_game( } #[tauri::command] -pub fn fetch_game_status(id: String) -> Result { - let db_handle = DB.borrow_data().unwrap(); - let status = db_handle - .games - .games_statuses - .get(&id) - .unwrap_or(&DatabaseGameStatus::Remote {}) - .clone(); - drop(db_handle); +pub fn fetch_game_status(id: String) -> Result { + let status = GameStatusManager::fetch_state(&id); Ok(status) } @@ -277,7 +262,7 @@ pub fn on_game_complete( let mut handle = DB.borrow_data_mut().unwrap(); handle .games - .game_versions + .versions .entry(game_id.clone()) .or_default() .insert(version_name.clone(), data.clone()); @@ -285,12 +270,12 @@ pub fn on_game_complete( DB.save().unwrap(); let status = if data.setup_command.is_empty() { - DatabaseGameStatus::Installed { + GameStatus::Installed { version_name, install_dir, } } else { - DatabaseGameStatus::SetupRequired { + GameStatus::SetupRequired { version_name, install_dir, } @@ -299,14 +284,17 @@ pub fn on_game_complete( let mut db_handle = DB.borrow_data_mut().unwrap(); db_handle .games - .games_statuses + .statuses .insert(game_id.clone(), status.clone()); drop(db_handle); DB.save().unwrap(); app_handle .emit( &format!("update_game/{}", game_id), - GameUpdateEvent { game_id, status }, + GameUpdateEvent { + game_id, + status: (Some(status), None), + }, ) .unwrap(); diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs index 8eed7ef..ed58d6d 100644 --- a/src-tauri/src/process/process_manager.rs +++ b/src-tauri/src/process/process_manager.rs @@ -10,7 +10,7 @@ use log::info; use serde::{Deserialize, Serialize}; use crate::{ - db::{DatabaseGameStatus, DATA_ROOT_DIR}, + db::{GameStatus, DATA_ROOT_DIR}, DB, }; @@ -74,11 +74,11 @@ impl ProcessManager { let db_lock = DB.borrow_data().unwrap(); let game_status = db_lock .games - .games_statuses + .statuses .get(&game_id) .ok_or("Game not installed")?; - let DatabaseGameStatus::Installed { + let GameStatus::Installed { version_name, install_dir, } = game_status @@ -88,7 +88,7 @@ impl ProcessManager { let game_version = db_lock .games - .game_versions + .versions .get(&game_id) .ok_or("Invalid game ID".to_owned())? .get(version_name) diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs deleted file mode 100644 index 8b13789..0000000 --- a/src-tauri/src/settings.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs new file mode 100644 index 0000000..9f66307 --- /dev/null +++ b/src-tauri/src/state.rs @@ -0,0 +1,31 @@ +use std::collections::HashMap; + +use crate::{ + db::{GameStatus, GameTransientStatus}, + DB, +}; + +pub type GameStatusWithTransient = ( + Option, + Option, +); +pub struct GameStatusManager {} + +impl GameStatusManager { + pub fn fetch_state(game_id: &String) -> GameStatusWithTransient { + let db_lock = DB.borrow_data().unwrap(); + let offline_state = db_lock.games.statuses.get(game_id).cloned(); + let online_state = db_lock.games.transient_statuses.get(game_id).cloned(); + drop(db_lock); + + if online_state.is_some() { + return (None, online_state); + } + + if offline_state.is_some() { + return (offline_state, None); + } + + return (None, None); + } +} From 239b8d53f94349ffffb3d0b93dfb5d0fe6fddf51 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Mon, 23 Dec 2024 20:56:11 +1100 Subject: [PATCH 146/164] feat: quit button --- components/HeaderUserWidget.vue | 6 +++--- pages/library.vue | 2 +- pages/quit.vue | 9 +++++++++ pages/settings/downloads.vue | 2 +- src-tauri/src/lib.rs | 8 +++++++- 5 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 pages/quit.vue diff --git a/components/HeaderUserWidget.vue b/components/HeaderUserWidget.vue index 27f2867..dd9838f 100644 --- a/components/HeaderUserWidget.vue +++ b/components/HeaderUserWidget.vue @@ -107,9 +107,9 @@ const navigation: NavigationItem[] = [ prefix: "", }, { - label: "Sign out", - route: "/signout", + label: "Quit Drop", + route: "/quit", prefix: "", - }, + } ] diff --git a/pages/library.vue b/pages/library.vue index ca01cdb..67e9fe7 100644 --- a/pages/library.vue +++ b/pages/library.vue @@ -23,7 +23,7 @@ navIdx === currentNavigationIndex ? 'text-zinc-100' : 'text-zinc-400 group-hover:text-zinc-300', - 'transition text-sm font-display leading-6', + 'truncate transition text-sm font-display leading-6', ]" > {{ nav.label }} diff --git a/pages/quit.vue b/pages/quit.vue new file mode 100644 index 0000000..808a5ec --- /dev/null +++ b/pages/quit.vue @@ -0,0 +1,9 @@ + + + + + diff --git a/pages/settings/downloads.vue b/pages/settings/downloads.vue index 237ea72..d7e7d53 100644 --- a/pages/settings/downloads.vue +++ b/pages/settings/downloads.vue @@ -129,7 +129,7 @@ : 'text-white bg-blue-600 hover:bg-blue-500', ]" > - Upload + Add