-
-
-
+
+
+
+
+
Install {{ game.mName }}?
+
+
+
+ Drop will add {{ game.mName }} to the queue to be downloaded.
+ While downloading, Drop may use up a large amount of resources,
+ particularly network bandwidth and CPU utilisation.
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+ {{ installError }}
+
+
+
+
+
+
+ install()"
+ :disabled="
+ !(versionOptions && versionOptions.length > 0 && !installDir)
+ "
+ :loading="installLoading"
+ type="submit"
+ class="w-full sm:w-fit"
+ >
+ Install
+
+
+ Cancel
+
+
+
diff --git a/pages/queue.vue b/pages/queue.vue
index 6572203..d78967c 100644
--- a/pages/queue.vue
+++ b/pages/queue.vue
@@ -1,27 +1,32 @@
-
+
+
+
+ {{ formatKilobytes(stats.speed) }}
+ {{ formatTime(stats.time) }} left
+
+
+
+
-
+
-
+
-
+
- {{ games[element.id].game.mName }}
+ {{ games[element.meta.id].game.mName }}
- {{ games[element.id].game.mShortDescription }}
+ {{ games[element.meta.id].game.mShortDescription }}
@@ -30,31 +35,20 @@
{{ element.status }}
-
- cancelGame(element.id)" class="group">
-
+ cancelGame(element.meta)" class="group">
+
Loading...
-
@@ -63,23 +57,68 @@
diff --git a/pages/settings.vue b/pages/settings.vue
index 1e92a0f..5f60e36 100644
--- a/pages/settings.vue
+++ b/pages/settings.vue
@@ -9,25 +9,18 @@
-
+
-
+ ? 'text-zinc-100'
+ : 'text-zinc-400 group-hover:text-zinc-200',
+ 'transition h-6 w-6 shrink-0',
+ ]" aria-hidden="true" />
{{ item.label }}
@@ -43,11 +36,14 @@
diff --git a/pages/settings/compatibility.vue b/pages/settings/compatibility.vue
new file mode 100644
index 0000000..27e0f69
--- /dev/null
+++ b/pages/settings/compatibility.vue
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/pages/settings/debug.vue b/pages/settings/debug.vue
new file mode 100644
index 0000000..3ef3e06
--- /dev/null
+++ b/pages/settings/debug.vue
@@ -0,0 +1,161 @@
+
+
+
+
+ Debug Information
+
+
+ Technical information about your Drop client installation, helpful for
+ debugging.
+
+
+
+
+
+
+
+ Client ID
+
+
+
+ {{ clientId || "Not signed in" }}
+
+
+
+
+
+
+
+ Platform
+
+
+
+ {{ platformInfo }}
+
+
+
+
+
+
+
+ Server URL
+
+
+
+ {{ baseUrl || "Not connected" }}
+
+
+
+
+
+
+
+ Data Directory
+
+
+
+ {{ dataDir || "Unknown" }}
+
+
+
+
+
+
+
+ Compatibility Settings
+
+
+
+
+ Enabled: {{ compatInfo.enabled ? "Yes" : "No" }}
+
+
+ Runner: {{ compatInfo.runner || "Not configured" }}
+
+
+ Prefix: {{ compatInfo.prefix || "Not configured" }}
+
+
+
+
+
+ openDataDir()"
+ type="button"
+ class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
+ >
+
+ Open Data Directory
+
+ openLogFile()"
+ type="button"
+ class="inline-flex items-center gap-x-2 rounded-md bg-blue-600 px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
+ >
+
+ Open Log File
+
+
+
+
+
+
+
+
diff --git a/pages/settings/index.vue b/pages/settings/index.vue
index 27e0f69..241c840 100644
--- a/pages/settings/index.vue
+++ b/pages/settings/index.vue
@@ -1,3 +1,60 @@
-
-
\ No newline at end of file
+
+
+
General
+
+ Configure basic application settings
+
+
+
+
+
+
Start with system
+
+ Drop will automatically start when you log into your computer
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/settings/interface.vue b/pages/settings/interface.vue
index 27e0f69..3df236b 100644
--- a/pages/settings/interface.vue
+++ b/pages/settings/interface.vue
@@ -1,3 +1,7 @@
-
\ No newline at end of file
+
+
+
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 74a6576..069c108 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -261,6 +261,17 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+[[package]]
+name = "auto-launch"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
+dependencies = [
+ "dirs 4.0.0",
+ "thiserror 1.0.69",
+ "winreg 0.10.1",
+]
+
[[package]]
name = "autocfg"
version = "1.4.0"
@@ -355,6 +366,12 @@ dependencies = [
"piper",
]
+[[package]]
+name = "boxcar"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f839cdf7e2d3198ac6ca003fd8ebc61715755f41c1cad15ff13df67531e00ed"
+
[[package]]
name = "brotli"
version = "7.0.0"
@@ -864,7 +881,16 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
- "dirs-sys",
+ "dirs-sys 0.4.1",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys 0.3.7",
]
[[package]]
@@ -873,7 +899,18 @@ version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
- "dirs-sys",
+ "dirs-sys 0.4.1",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
]
[[package]]
@@ -965,6 +1002,7 @@ dependencies = [
name = "drop-app"
version = "0.1.0"
dependencies = [
+ "boxcar",
"chrono",
"directories",
"hex",
@@ -980,13 +1018,18 @@ dependencies = [
"serde",
"serde-binary",
"serde_json",
+ "serde_with",
+ "shared_child",
"tauri",
"tauri-build",
+ "tauri-plugin-autostart",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
+ "tauri-plugin-os",
"tauri-plugin-shell",
"tauri-plugin-single-instance",
"tokio",
+ "umu-wrapper-lib",
"url",
"urlencoding",
"uuid",
@@ -1037,7 +1080,7 @@ dependencies = [
"rustc_version",
"toml 0.8.2",
"vswhom",
- "winreg",
+ "winreg 0.52.0",
]
[[package]]
@@ -1435,6 +1478,16 @@ dependencies = [
"version_check",
]
+[[package]]
+name = "gethostname"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
+dependencies = [
+ "rustix",
+ "windows-targets 0.52.6",
+]
+
[[package]]
name = "getrandom"
version = "0.1.16"
@@ -2853,6 +2906,17 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "os_info"
+version = "3.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb6651f4be5e39563c4fe5cc8326349eb99a25d805a3493f791d5bfd0269e430"
+dependencies = [
+ "log",
+ "serde",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "os_pipe"
version = "1.2.1"
@@ -3793,9 +3857,9 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.11.0"
+version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
+checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
dependencies = [
"base64 0.22.1",
"chrono",
@@ -3811,9 +3875,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.11.0"
+version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
+checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
dependencies = [
"darling",
"proc-macro2",
@@ -4107,6 +4171,15 @@ dependencies = [
"syn 2.0.91",
]
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
[[package]]
name = "system-configuration"
version = "0.6.1"
@@ -4205,7 +4278,7 @@ checksum = "e545de0a2dfe296fa67db208266cd397c5a55ae782da77973ef4c4fac90e9f2c"
dependencies = [
"anyhow",
"bytes",
- "dirs",
+ "dirs 5.0.1",
"dunce",
"embed_plist",
"futures-util",
@@ -4255,7 +4328,7 @@ checksum = "7bd2a4bcfaf5fb9f4be72520eefcb61ae565038f8ccba2a497d8c28f463b8c01"
dependencies = [
"anyhow",
"cargo_toml",
- "dirs",
+ "dirs 5.0.1",
"glob",
"heck 0.5.0",
"json-patch",
@@ -4327,6 +4400,20 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "tauri-plugin-autostart"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9c13f843e5e5df3eed270fc42b02923cc1a6b5c7e56b0f3ac1d858ab2c8b5fb"
+dependencies = [
+ "auto-launch",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.9",
+]
+
[[package]]
name = "tauri-plugin-deep-link"
version = "2.2.0"
@@ -4388,6 +4475,24 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "tauri-plugin-os"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dda2d571a9baf0664c1f2088db227e3072f9028602fafa885deade7547c3b738"
+dependencies = [
+ "gethostname",
+ "log",
+ "os_info",
+ "serde",
+ "serde_json",
+ "serialize-to-javascript",
+ "sys-locale",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.9",
+]
+
[[package]]
name = "tauri-plugin-shell"
version = "2.2.0"
@@ -4813,7 +4918,7 @@ checksum = "d48a05076dd272615d03033bf04f480199f7d1b66a8ac64d75c625fc4a70c06b"
dependencies = [
"core-graphics",
"crossbeam-channel",
- "dirs",
+ "dirs 5.0.1",
"libappindicator",
"muda",
"objc2",
@@ -4870,6 +4975,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "umu-wrapper-lib"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baa36636bef667cee9eb4f497c24279182b8b9f098fd04b0b8c5d2ebc4e451f1"
+
[[package]]
name = "unic-char-property"
version = "0.9.0"
@@ -5713,6 +5824,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "winreg"
version = "0.52.0"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 64f0f9b..73aa54e 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,8 +1,8 @@
[package]
name = "drop-app"
version = "0.1.0"
-description = "A Tauri App"
-authors = ["you"]
+description = "The client application for the open-source, self-hosted game distribution platform Drop"
+authors = ["Drop OSS"]
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -26,7 +26,6 @@ tauri-build = { version = "2.0.0", features = [] }
[dependencies]
tauri-plugin-shell = "2.0.0"
-serde = { version = "1", features = ["derive", "rc"] }
serde_json = "1"
serde-binary = "0.5.0"
rayon = "1.10.0"
@@ -41,12 +40,16 @@ http = "1.1.0"
urlencoding = "2.1.3"
md5 = "0.7.0"
chrono = "0.4.38"
+tauri-plugin-os = "2"
+boxcar = "0.2.7"
+umu-wrapper-lib = "0.1.0"
+tauri-plugin-autostart = "2.0.0"
+shared_child = "1.0.1"
+serde_with = "3.12.0"
[dependencies.tauri]
version = "2.1.1"
-features = [
- "tray-icon"
-]
+features = ["tray-icon"]
[dependencies.tokio]
@@ -81,6 +84,10 @@ features = [] # You can also use "yaml_enc" or "bin_enc"
version = "0.12"
features = ["json", "blocking"]
+[dependencies.serde]
+version = "1"
+features = ["derive", "rc"]
+
[profile.release]
lto = true
codegen-units = 1
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 4bafeab..1b818b3 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -13,6 +13,7 @@
"core:window:allow-maximize",
"core:window:allow-close",
"deep-link:default",
- "dialog:default"
+ "dialog:default",
+ "os:default"
]
}
\ No newline at end of file
diff --git a/src-tauri/src/auth.rs b/src-tauri/src/auth.rs
index 0f1010f..7044140 100644
--- a/src-tauri/src/auth.rs
+++ b/src-tauri/src/auth.rs
@@ -1,8 +1,4 @@
-use std::{
- env,
- sync::Mutex,
- time::{SystemTime, UNIX_EPOCH},
-};
+use std::{env, sync::Mutex};
use chrono::Utc;
use log::{info, warn};
@@ -13,7 +9,7 @@ use url::Url;
use crate::{
db::{DatabaseAuth, DatabaseImpls},
- remote::RemoteAccessError,
+ remote::{DropServerError, RemoteAccessError},
AppState, AppStatus, User, DB,
};
@@ -78,7 +74,13 @@ pub fn fetch_user() -> Result {
.send()?;
if response.status() != 200 {
- info!("Could not fetch user: {}", response.text().unwrap());
+ let data = response.json::()?;
+ info!("Could not fetch user: {}", data.status_message);
+
+ if data.status_message == "Nonce expired" {
+ return Err(RemoteAccessError::OutOfSync);
+ }
+
return Err(RemoteAccessError::InvalidCodeError(0));
}
@@ -91,7 +93,9 @@ 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::InvalidResponse);
+ return Err(RemoteAccessError::HandshakeFailed(
+ "failed to parse token".to_string(),
+ ));
}
let base_url = {
@@ -133,6 +137,12 @@ fn recieve_handshake_logic(app: &AppHandle, path: String) -> Result<(), RemoteAc
Ok(())
}
+#[tauri::command]
+pub fn manual_recieve_handshake(app: AppHandle, token: String) -> Result<(), String> {
+ recieve_handshake(app, format!("handshake/{}", token));
+ Ok(())
+}
+
pub fn recieve_handshake(app: AppHandle, path: String) {
// Tell the app we're processing
app.emit("auth/processing", ()).unwrap();
@@ -147,7 +157,7 @@ pub fn recieve_handshake(app: AppHandle, path: String) {
app.emit("auth/finished", ()).unwrap();
}
-async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
+fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
let base_url = {
let db_lock = DB.borrow_data().unwrap();
Url::parse(&db_lock.base_url.clone())?
@@ -159,14 +169,17 @@ async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
platform: env::consts::OS.to_string(),
};
- let client = reqwest::Client::new();
- let response = client.post(endpoint.to_string()).json(&body).send().await?;
+ let client = reqwest::blocking::Client::new();
+ let response = client.post(endpoint.to_string()).json(&body).send()?;
if response.status() != 200 {
- return Err(RemoteAccessError::InvalidRedirect);
+ let data = response.json::()?;
+ info!("Could not start handshake: {}", data.status_message);
+
+ return Err(RemoteAccessError::HandshakeFailed(data.status_message));
}
- let redir_url = response.text().await?;
+ let redir_url = response.text()?;
let complete_redir_url = base_url.join(&redir_url)?;
info!("opening web browser to continue authentication");
@@ -176,8 +189,8 @@ async fn auth_initiate_wrapper() -> Result<(), RemoteAccessError> {
}
#[tauri::command]
-pub async fn auth_initiate<'a>() -> Result<(), String> {
- let result = auth_initiate_wrapper().await;
+pub fn auth_initiate<'a>() -> Result<(), String> {
+ let result = auth_initiate_wrapper();
if result.is_err() {
return Err(result.err().unwrap().to_string());
}
@@ -199,8 +212,10 @@ pub fn retry_connect(state: tauri::State<'_, Mutex>) -> Result<(), ()>
pub fn setup() -> Result<(AppStatus, Option), ()> {
let data = DB.borrow_data().unwrap();
+ let auth = data.auth.clone();
+ drop(data);
- if data.auth.is_some() {
+ if auth.is_some() {
let user_result = fetch_user();
if user_result.is_err() {
let error = user_result.err().unwrap();
@@ -215,7 +230,31 @@ pub fn setup() -> Result<(AppStatus, Option), ()> {
return Ok((AppStatus::SignedIn, Some(user_result.unwrap())));
}
- drop(data);
-
Ok((AppStatus::SignedOut, None))
}
+
+#[tauri::command]
+pub fn sign_out(app: AppHandle) -> Result<(), String> {
+ info!("Signing out user");
+
+ // Clear auth from database
+ {
+ let mut handle = DB.borrow_data_mut().unwrap();
+ handle.auth = None;
+ drop(handle);
+ DB.save().unwrap();
+ }
+
+ // Update app state
+ {
+ let app_state = app.state::>();
+ let mut app_state_handle = app_state.lock().unwrap();
+ app_state_handle.status = AppStatus::SignedOut;
+ app_state_handle.user = None;
+ }
+
+ // Emit event for frontend
+ app.emit("auth/signedout", ()).unwrap();
+
+ Ok(())
+}
diff --git a/src-tauri/src/autostart.rs b/src-tauri/src/autostart.rs
new file mode 100644
index 0000000..bd526be
--- /dev/null
+++ b/src-tauri/src/autostart.rs
@@ -0,0 +1,69 @@
+use log::info;
+use tauri::AppHandle;
+use tauri_plugin_autostart::ManagerExt;
+use crate::DB;
+
+#[tauri::command]
+pub async fn toggle_autostart(app: AppHandle, enabled: bool) -> Result<(), String> {
+ let manager = app.autolaunch();
+ if enabled {
+ manager.enable().map_err(|e| e.to_string())?;
+ info!("Enabled autostart");
+ } else {
+ manager.disable().map_err(|e| e.to_string())?;
+ info!("Disabled autostart");
+ }
+
+ // Store the state in DB
+ let mut db_handle = DB.borrow_data_mut().map_err(|e| e.to_string())?;
+ db_handle.settings.autostart = enabled;
+ drop(db_handle);
+ DB.save().map_err(|e| e.to_string())?;
+
+ Ok(())
+}
+
+#[tauri::command]
+pub async fn get_autostart_enabled(app: AppHandle) -> Result {
+ // First check DB state
+ let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
+ let db_state = db_handle.settings.autostart;
+ drop(db_handle);
+
+ // Get actual system state
+ let manager = app.autolaunch();
+ let system_state = manager.is_enabled().map_err(|e| e.to_string())?;
+
+ // If they don't match, sync to DB state
+ if db_state != system_state {
+ if db_state {
+ manager.enable().map_err(|e| e.to_string())?;
+ } else {
+ manager.disable().map_err(|e| e.to_string())?;
+ }
+ }
+
+ Ok(db_state)
+}
+
+// New function to sync state on startup
+pub fn sync_autostart_on_startup(app: &AppHandle) -> Result<(), String> {
+ let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
+ let should_be_enabled = db_handle.settings.autostart;
+ drop(db_handle);
+
+ let manager = app.autolaunch();
+ let current_state = manager.is_enabled().map_err(|e| e.to_string())?;
+
+ if current_state != should_be_enabled {
+ if should_be_enabled {
+ manager.enable().map_err(|e| e.to_string())?;
+ info!("Synced autostart: enabled");
+ } else {
+ manager.disable().map_err(|e| e.to_string())?;
+ info!("Synced autostart: disabled");
+ }
+ }
+
+ Ok(())
+}
diff --git a/src-tauri/src/cleanup.rs b/src-tauri/src/cleanup.rs
index 2fe2d2d..925ea61 100644
--- a/src-tauri/src/cleanup.rs
+++ b/src-tauri/src/cleanup.rs
@@ -1,17 +1,14 @@
-use std::sync::Mutex;
use log::info;
use tauri::AppHandle;
-use crate::AppState;
#[tauri::command]
pub fn quit(app: tauri::AppHandle) {
cleanup_and_exit(&app);
}
-
-pub fn cleanup_and_exit(app: &AppHandle, ) {
+pub fn cleanup_and_exit(app: &AppHandle) {
info!("exiting drop application...");
app.exit(0);
diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs
index 00e93ab..a9ad8da 100644
--- a/src-tauri/src/db.rs
+++ b/src-tauri/src/db.rs
@@ -2,19 +2,21 @@ use std::{
collections::HashMap,
fs::{self, create_dir_all},
path::{Path, PathBuf},
- sync::{LazyLock, Mutex},
+ sync::{Arc, LazyLock, Mutex, RwLockWriteGuard}, time::{Instant, SystemTime, UNIX_EPOCH},
};
+use chrono::Utc;
use directories::BaseDirs;
use log::debug;
-use rustbreak::{DeSerError, DeSerializer, PathDatabase};
+use rustbreak::{DeSerError, DeSerializer, PathDatabase, RustbreakError};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use serde_with::serde_as;
+use tauri::AppHandle;
use url::Url;
-use crate::{process::process_manager::Platform, DB};
+use crate::{download_manager::downloadable_metadata::DownloadableMetadata, games::{library::push_game_update, state::GameStatusManager}, process::process_manager::Platform, DB};
#[derive(serde::Serialize, Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
pub struct DatabaseAuth {
pub private: String,
pub cert: String,
@@ -24,7 +26,7 @@ pub struct DatabaseAuth {
// Strings are version names for a particular game
#[derive(Serialize, Clone, Deserialize)]
#[serde(tag = "type")]
-pub enum GameStatus {
+pub enum GameDownloadStatus {
Remote {},
SetupRequired {
version_name: String,
@@ -38,13 +40,14 @@ pub enum GameStatus {
// Stuff that shouldn't be synced to disk
#[derive(Clone, Serialize)]
-pub enum GameTransientStatus {
+pub enum ApplicationTransientStatus {
Downloading { version_name: String },
Uninstalling {},
Updating { version_name: String },
+ Running {},
}
-#[derive(Serialize, Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct GameVersion {
pub version_index: usize,
@@ -54,24 +57,43 @@ pub struct GameVersion {
pub platform: Platform,
}
+#[serde_as]
#[derive(Serialize, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
-pub struct DatabaseGames {
+pub struct DatabaseApplications {
pub install_dirs: Vec,
// Guaranteed to exist if the game also exists in the app state map
- pub statuses: HashMap,
- pub versions: HashMap>,
+ pub game_statuses: HashMap,
+ pub game_versions: HashMap>,
+ pub installed_game_version: HashMap,
#[serde(skip)]
- pub transient_statuses: HashMap,
+ pub transient_statuses: HashMap,
}
-#[derive(Serialize, Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
+#[derive(Serialize, Deserialize, Clone)]
+pub struct Settings {
+ pub autostart: bool,
+ // ... other settings ...
+}
+
+impl Default for Settings {
+ fn default() -> Self {
+ Self {
+ autostart: false,
+ // ... other settings defaults ...
+ }
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
pub struct Database {
+ #[serde(default)]
+ pub settings: Settings,
pub auth: Option,
pub base_url: String,
- pub games: DatabaseGames,
+ pub applications: DatabaseApplications,
+ pub prev_database: Option
}
pub static DATA_ROOT_DIR: LazyLock> =
LazyLock::new(|| Mutex::new(BaseDirs::new().unwrap().data_dir().join("drop")));
@@ -112,21 +134,27 @@ impl DatabaseImpls for DatabaseInterface {
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();
match exists {
- true => PathDatabase::load_from_path(db_path).expect("Database loading failed"),
+ true =>
+ match PathDatabase::load_from_path(db_path.clone()) {
+ Ok(db) => db,
+ Err(e) => handle_invalid_database(e, db_path, games_base_dir),
+ },
false => {
let default = Database {
+ settings: Settings::default(),
auth: None,
base_url: "".to_string(),
- games: DatabaseGames {
+ applications: DatabaseApplications {
install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
- statuses: HashMap::new(),
+ game_statuses: HashMap::new(),
transient_statuses: HashMap::new(),
- versions: HashMap::new(),
+ game_versions: HashMap::new(),
+ installed_game_version: HashMap::new(),
},
+ prev_database: None,
};
debug!(
"Creating database at path {}",
@@ -172,10 +200,10 @@ pub fn add_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) {
+ if lock.applications.install_dirs.contains(&new_dir) {
return Err("Download directory already used".to_string());
}
- lock.games.install_dirs.push(new_dir);
+ lock.applications.install_dirs.push(new_dir);
drop(lock);
DB.save().unwrap();
@@ -185,7 +213,7 @@ pub fn add_download_dir(new_dir: String) -> Result<(), String> {
#[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);
+ lock.applications.install_dirs.remove(index);
drop(lock);
DB.save().unwrap();
@@ -197,8 +225,54 @@ pub fn delete_download_dir(index: usize) -> Result<(), String> {
#[tauri::command]
pub fn fetch_download_dir_stats() -> Result, String> {
let lock = DB.borrow_data().unwrap();
- let directories = lock.games.install_dirs.clone();
+ let directories = lock.applications.install_dirs.clone();
drop(lock);
Ok(directories)
}
+
+pub fn set_game_status, &DownloadableMetadata)>(
+ app_handle: &AppHandle,
+ meta: DownloadableMetadata,
+ setter: F,
+) {
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ setter(&mut db_handle, &meta);
+ drop(db_handle);
+ DB.save().unwrap();
+
+ let status = GameStatusManager::fetch_state(&meta.id);
+
+ push_game_update(app_handle, &meta, status);
+}
+
+// TODO: Make the error relelvant rather than just assume that it's a Deserialize error
+fn handle_invalid_database(_e: RustbreakError, db_path: PathBuf, games_base_dir: PathBuf) -> rustbreak::Database {
+ let new_path = {
+ let time = Utc::now().timestamp();
+ let mut base = db_path.clone().into_os_string();
+ base.push(".");
+ base.push(time.to_string());
+ base
+ };
+ fs::copy(&db_path, &new_path).unwrap();
+
+ let db = Database {
+ auth: None,
+ base_url: "".to_string(),
+ applications: DatabaseApplications {
+ install_dirs: vec![games_base_dir.to_str().unwrap().to_string()],
+ game_statuses: HashMap::new(),
+ transient_statuses: HashMap::new(),
+ game_versions: HashMap::new(),
+ installed_game_version: HashMap::new(),
+ },
+ prev_database: Some(new_path.into()),
+ settings: Settings { autostart: false },
+ };
+
+ PathDatabase::create_at_path(db_path, db)
+ .expect("Database could not be created")
+
+
+}
\ No newline at end of file
diff --git a/src-tauri/src/debug.rs b/src-tauri/src/debug.rs
new file mode 100644
index 0000000..de42dd6
--- /dev/null
+++ b/src-tauri/src/debug.rs
@@ -0,0 +1,23 @@
+use crate::{DATA_ROOT_DIR, DB};
+use serde::Serialize;
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SystemData {
+ client_id: String,
+ base_url: String,
+ data_dir: String,
+}
+
+#[tauri::command]
+pub fn fetch_system_data() -> Result {
+ let db_handle = DB.borrow_data().map_err(|e| e.to_string())?;
+ let system_data = SystemData {
+ client_id: db_handle.auth.as_ref().unwrap().client_id.clone(),
+ base_url: db_handle.base_url.clone(),
+ data_dir: DATA_ROOT_DIR.lock().unwrap().to_string_lossy().to_string(),
+ };
+ drop(db_handle);
+
+ Ok(system_data)
+}
diff --git a/src-tauri/src/download_manager/application_download_error.rs b/src-tauri/src/download_manager/application_download_error.rs
new file mode 100644
index 0000000..38c690d
--- /dev/null
+++ b/src-tauri/src/download_manager/application_download_error.rs
@@ -0,0 +1,41 @@
+use std::{fmt::{Display, Formatter}, io};
+
+use crate::remote::RemoteAccessError;
+
+// TODO: Rename / separate from downloads
+#[derive(Debug, Clone)]
+pub enum ApplicationDownloadError {
+ Communication(RemoteAccessError),
+ Checksum,
+ Setup(SetupError),
+ Lock,
+ IoError(io::ErrorKind),
+ DownloadError,
+}
+
+impl Display for ApplicationDownloadError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ApplicationDownloadError::Communication(error) => write!(f, "{}", error),
+ ApplicationDownloadError::Setup(error) => write!(f, "An error occurred while setting up the download: {}", error),
+ ApplicationDownloadError::Lock => write!(f, "Failed to acquire lock. Something has gone very wrong internally. Please restart the application"),
+ ApplicationDownloadError::Checksum => write!(f, "Checksum failed to validate for download"),
+ ApplicationDownloadError::IoError(error) => write!(f, "{}", error),
+ ApplicationDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"),
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+pub enum SetupError {
+ Context,
+}
+
+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"),
+ }
+ }
+}
+
diff --git a/src-tauri/src/downloads/download_manager.rs b/src-tauri/src/download_manager/download_manager.rs
similarity index 60%
rename from src-tauri/src/downloads/download_manager.rs
rename to src-tauri/src/download_manager/download_manager.rs
index 8676087..ec950f8 100644
--- a/src-tauri/src/downloads/download_manager.rs
+++ b/src-tauri/src/download_manager/download_manager.rs
@@ -12,47 +12,57 @@ use std::{
use log::info;
use serde::Serialize;
-use super::{
- download_agent::{GameDownloadAgent, GameDownloadError},
- download_manager_builder::CurrentProgressObject,
- progress_object::ProgressObject,
- queue::Queue,
-};
+
+use super::{application_download_error::ApplicationDownloadError, download_manager_builder::{CurrentProgressObject, DownloadAgent}, downloadable_metadata::DownloadableMetadata, queue::Queue};
pub enum DownloadManagerSignal {
/// Resumes (or starts) the DownloadManager
Go,
/// Pauses the DownloadManager
Stop,
- /// Called when a GameDownloadAgent has fully completed a download.
- Completed(String),
- /// Generates and appends a GameDownloadAgent
+ /// Called when a DownloadAgent has fully completed a download.
+ Completed(DownloadableMetadata),
+ /// Generates and appends a DownloadAgent
/// to the registry and queue
- Queue(String, String, usize),
+ Queue(DownloadAgent),
/// Tells the Manager to stop the current
/// download, sync everything to disk, and
/// then exit
Finish,
- /// Stops (but doesn't remove) current download
- Cancel,
- /// Removes a given game
- Remove(String),
+ /// Stops, removes, and tells a download to cleanup
+ Cancel(DownloadableMetadata),
+ /// Removes a given application
+ Remove(DownloadableMetadata),
/// Any error which occurs in the agent
- Error(GameDownloadError),
+ Error(ApplicationDownloadError),
/// Pushes UI update
- Update,
+ UpdateUIQueue,
+ UpdateUIStats(usize, usize), //kb/s and seconds
+ /// Uninstall download
+ /// Takes download ID
+ Uninstall(DownloadableMetadata),
}
+#[derive(Debug, Clone)]
pub enum DownloadManagerStatus {
Downloading,
Paused,
Empty,
- Error(GameDownloadError),
+ Error(ApplicationDownloadError),
Finished,
}
-#[derive(Serialize, Clone)]
-pub enum GameDownloadStatus {
+impl Serialize for DownloadManagerStatus {
+ fn serialize(&self, serializer: S) -> Result
+ where
+ S: serde::Serializer,
+ {
+ serializer.serialize_str(&format!["{:?}", self])
+ }
+}
+
+#[derive(Serialize, Clone, Debug)]
+pub enum DownloadStatus {
Queued,
Downloading,
Error,
@@ -74,27 +84,6 @@ pub struct DownloadManager {
progress: CurrentProgressObject,
command_sender: Sender,
}
-pub struct GameDownloadAgentQueueStandin {
- pub id: String,
- pub status: Mutex,
- pub progress: Arc,
-}
-impl From> for GameDownloadAgentQueueStandin {
- fn from(value: Arc) -> Self {
- Self {
- id: value.id.clone(),
- status: Mutex::from(GameDownloadStatus::Queued),
- progress: value.progress.clone(),
- }
- }
-}
-impl Debug for GameDownloadAgentQueueStandin {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.debug_struct("GameDownloadAgentQueueStandin")
- .field("id", &self.id)
- .finish()
- }
-}
#[allow(dead_code)]
impl DownloadManager {
@@ -112,42 +101,36 @@ impl DownloadManager {
}
}
- pub fn queue_game(
+ pub fn queue_download(
&self,
- id: String,
- version: String,
- target_download_dir: usize,
+ download: DownloadAgent
) -> Result<(), SendError> {
- info!("Adding game id {}", id);
- self.command_sender.send(DownloadManagerSignal::Queue(
- id,
- version,
- target_download_dir,
- ))?;
+ info!("Adding download id {:?}", download.metadata());
+ self.command_sender.send(DownloadManagerSignal::Queue(download))?;
self.command_sender.send(DownloadManagerSignal::Go)
}
- 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 {
+ pub fn get_current_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) {
+ pub fn rearrange_string(&self, meta: &DownloadableMetadata, new_index: usize) {
let mut queue = self.edit();
- let current_index = get_index_from_id(&mut queue, id).unwrap();
+ let current_index = get_index_from_id(&mut queue, meta).unwrap();
let to_move = queue.remove(current_index).unwrap();
queue.insert(new_index, to_move);
self.command_sender
- .send(DownloadManagerSignal::Update)
+ .send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}
- pub fn cancel(&self, game_id: String) {
+ pub fn cancel(&self, meta: DownloadableMetadata) {
self.command_sender
- .send(DownloadManagerSignal::Remove(game_id))
+ .send(DownloadManagerSignal::Cancel(meta))
.unwrap();
}
pub fn rearrange(&self, current_index: usize, new_index: usize) {
@@ -158,7 +141,7 @@ impl DownloadManager {
let needs_pause = current_index == 0 || new_index == 0;
if needs_pause {
self.command_sender
- .send(DownloadManagerSignal::Cancel)
+ .send(DownloadManagerSignal::Stop)
.unwrap();
}
@@ -174,7 +157,7 @@ impl DownloadManager {
self.command_sender.send(DownloadManagerSignal::Go).unwrap();
}
self.command_sender
- .send(DownloadManagerSignal::Update)
+ .send(DownloadManagerSignal::UpdateUIQueue)
.unwrap();
}
pub fn pause_downloads(&self) {
@@ -191,15 +174,23 @@ impl DownloadManager {
.unwrap();
self.terminator.join()
}
+ pub fn uninstall_application(&self, meta: DownloadableMetadata) {
+ self.command_sender
+ .send(DownloadManagerSignal::Uninstall(meta))
+ .unwrap();
+ }
+ pub fn get_sender(&self) -> Sender {
+ self.command_sender.clone()
+ }
}
/// Takes in the locked value from .edit() and attempts to
-/// get the index of whatever game_id is passed in
+/// get the index of whatever id is passed in
fn get_index_from_id(
- queue: &mut MutexGuard<'_, VecDeque>>,
- id: String,
+ queue: &mut MutexGuard<'_, VecDeque>,
+ meta: &DownloadableMetadata,
) -> Option {
queue
.iter()
- .position(|download_agent| download_agent.id == id)
+ .position(|download_agent| download_agent == meta)
}
diff --git a/src-tauri/src/download_manager/download_manager_builder.rs b/src-tauri/src/download_manager/download_manager_builder.rs
new file mode 100644
index 0000000..406a500
--- /dev/null
+++ b/src-tauri/src/download_manager/download_manager_builder.rs
@@ -0,0 +1,343 @@
+use std::{
+ collections::HashMap,
+ fs::remove_dir_all,
+ sync::{
+ mpsc::{channel, Receiver, Sender},
+ Arc, Mutex, RwLockWriteGuard,
+ },
+ thread::{spawn, JoinHandle},
+};
+
+use log::{error, info};
+use tauri::{AppHandle, Emitter};
+
+use crate::games::library::{QueueUpdateEvent, QueueUpdateEventQueueData, StatsUpdateEvent};
+
+use super::{application_download_error::ApplicationDownloadError, download_manager::{DownloadManager, DownloadManagerSignal, DownloadManagerStatus}, download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag}, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject, queue::Queue};
+
+pub type DownloadAgent = Arc>;
+pub type CurrentProgressObject = Arc>>>;
+
+/*
+
+Welcome to the download manager, the most overengineered, glorious piece of bullshit.
+
+The download manager takes a queue of ids and their associated
+DownloadAgents, 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 DownloadAgents. 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: CurrentProgressObject,
+ status: Arc>,
+ app_handle: AppHandle,
+
+ current_download_agent: Option, // Should be the only download agent in the map with the "Go" flag
+ current_download_thread: Mutex>>,
+ active_control_flag: Option,
+}
+impl DownloadManagerBuilder {
+ 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));
+ let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
+
+ let manager = Self {
+ download_agent_registry: HashMap::new(),
+ download_queue: queue.clone(),
+ command_receiver,
+ 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());
+
+ DownloadManager::new(terminator, queue, active_progress, command_sender)
+ }
+
+ fn set_status(&self, status: DownloadManagerStatus) {
+ *self.status.lock().unwrap() = status;
+ }
+
+ fn remove_and_cleanup_front_download(&mut self, meta: &DownloadableMetadata) -> DownloadAgent {
+ self.download_queue.pop_front();
+ let download_agent = self.download_agent_registry.remove(meta).unwrap();
+ self.cleanup_current_download();
+ 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;
+
+ let mut download_thread_lock = self.current_download_thread.lock().unwrap();
+ *download_thread_lock = None;
+ drop(download_thread_lock);
+
+ }
+
+ 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();
+ }
+ }
+
+
+ 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(meta) => {
+ self.manage_completed_signal(meta);
+ }
+ DownloadManagerSignal::Queue(download_agent) => {
+ self.manage_queue_signal(download_agent);
+ }
+ DownloadManagerSignal::Error(e) => {
+ self.manage_error_signal(e);
+ }
+ DownloadManagerSignal::UpdateUIQueue => {
+ self.push_ui_queue_update();
+ }
+ DownloadManagerSignal::UpdateUIStats(kbs, time) => {
+ self.push_ui_stats_update(kbs, time);
+ }
+ DownloadManagerSignal::Finish => {
+ self.stop_and_wait_current_download();
+ return Ok(());
+ }
+ DownloadManagerSignal::Cancel(meta) => {
+ self.manage_cancel_signal(&meta);
+ }
+ _ => {}
+ };
+ }
+ }
+ fn manage_queue_signal(&mut self, download_agent: DownloadAgent) {
+ info!("Got signal Queue");
+ let meta = download_agent.metadata();
+
+ info!("Meta: {:?}", meta);
+
+ if self.download_queue.exists(meta.clone()) {
+ info!("Download with same ID already exists");
+ return;
+ }
+
+ download_agent.on_initialised(&self.app_handle);
+ self.download_queue.append(meta.clone());
+ self.download_agent_registry.insert(meta, download_agent);
+
+ self.sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
+ }
+
+ fn manage_go_signal(&mut self) {
+ info!("Got signal Go");
+ if self.download_agent_registry.is_empty() {
+ info!("Download agent registry: {:?}", self.download_agent_registry.len());
+ return;
+ }
+
+ if self.current_download_agent.is_some() {
+ info!("Current download agent: {:?}", self.current_download_agent.as_ref().unwrap().metadata());
+ return;
+ }
+
+ info!("Current download queue: {:?}", self.download_queue.read());
+
+ // Should always be Some if the above two statements keep going
+ let agent_data = self.download_queue.read().front().unwrap().clone();
+
+ info!("Starting download for {:?}", agent_data);
+
+ let download_agent = self
+ .download_agent_registry
+ .get(&agent_data)
+ .unwrap()
+ .clone();
+
+ self.active_control_flag = Some(download_agent.control_flag());
+ self.current_download_agent = Some(download_agent.clone());
+
+ let sender = self.sender.clone();
+
+ let mut download_thread_lock = self.current_download_thread.lock().unwrap();
+ let app_handle = self.app_handle.clone();
+
+ *download_thread_lock = Some(spawn(move || {
+ match download_agent.download(&app_handle) {
+ // Ok(true) is for completed and exited properly
+ Ok(true) => {
+ download_agent.on_complete(&app_handle);
+ sender.send(DownloadManagerSignal::Completed(download_agent.metadata())).unwrap();
+ },
+ // Ok(false) is for incomplete but exited properly
+ Ok(false) => {
+ download_agent.on_incomplete(&app_handle);
+ },
+ Err(e) => {
+ download_agent.on_error(&app_handle, e.clone());
+ sender.send(DownloadManagerSignal::Error(e)).unwrap();
+ },
+ }
+ sender.send(DownloadManagerSignal::UpdateUIQueue).unwrap();
+ }));
+
+ self.set_status(DownloadManagerStatus::Downloading);
+ let active_control_flag = self.active_control_flag.clone().unwrap();
+ active_control_flag.set(DownloadThreadControlFlag::Go);
+ }
+ fn manage_stop_signal(&mut self) {
+ info!("Got signal Stop");
+
+ if let Some(active_control_flag) = self.active_control_flag.clone() {
+ self.set_status(DownloadManagerStatus::Paused);
+ active_control_flag.set(DownloadThreadControlFlag::Stop);
+ }
+ }
+ fn manage_completed_signal(&mut self, meta: DownloadableMetadata) {
+ info!("Got signal Completed");
+ if let Some(interface) = &self.current_download_agent {
+ if interface.metadata() == meta {
+ info!("Popping consumed data");
+ self.remove_and_cleanup_front_download(&meta);
+ }
+ }
+ self.push_ui_queue_update();
+ self.sender.send(DownloadManagerSignal::Go).unwrap();
+ }
+ fn manage_error_signal(&mut self, error: ApplicationDownloadError) {
+ info!("Got signal Error");
+ let current_agent = self.current_download_agent.clone().unwrap();
+
+ current_agent.on_error(&self.app_handle, error.clone());
+
+ self.stop_and_wait_current_download();
+ self.remove_and_cleanup_front_download(¤t_agent.metadata());
+
+ self.set_status(DownloadManagerStatus::Error(error));
+ }
+ fn manage_cancel_signal(&mut self, meta: &DownloadableMetadata) {
+ info!("Got signal Cancel");
+
+ if let Some(current_download) = &self.current_download_agent {
+ if ¤t_download.metadata() == meta {
+ self.set_status(DownloadManagerStatus::Paused);
+ current_download.on_cancelled(&self.app_handle);
+ self.stop_and_wait_current_download();
+
+ self.download_queue.pop_front();
+
+ self.cleanup_current_download();
+ info!("Current donwload queue: {:?}", self.download_queue.read());
+ }
+ // TODO: Collapse these two into a single if statement somehow
+ else {
+ if let Some(download_agent) = self.download_agent_registry.get(meta) {
+ info!("Object exists in registry");
+ let index = self.download_queue.get_by_meta(meta);
+ if let Some(index) = index {
+ download_agent.on_cancelled(&self.app_handle);
+ let _ = self.download_queue.edit().remove(index).unwrap();
+ let removed = self.download_agent_registry.remove(meta);
+ info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read());
+ }
+ }
+ }
+ }
+ else {
+ if let Some(download_agent) = self.download_agent_registry.get(meta) {
+ info!("Object exists in registry");
+ let index = self.download_queue.get_by_meta(meta);
+ if let Some(index) = index {
+ download_agent.on_cancelled(&self.app_handle);
+ let _ = self.download_queue.edit().remove(index).unwrap();
+ let removed = self.download_agent_registry.remove(meta);
+ info!("Removed {:?} from queue {:?}", removed.and_then(|x| Some(x.metadata())), self.download_queue.read());
+ }
+ }
+ }
+ self.push_ui_queue_update();
+ }
+ fn push_ui_stats_update(&self, kbs: usize, time: usize) {
+ let event_data = StatsUpdateEvent { speed: kbs, time };
+
+ self.app_handle.emit("update_stats", event_data).unwrap();
+ }
+ fn push_ui_queue_update(&self) {
+ let queue = &self.download_queue.read();
+ let queue_objs = queue
+ .iter()
+ .map(|(key)| {
+ let val = self.download_agent_registry.get(key).unwrap();
+ QueueUpdateEventQueueData {
+ meta: DownloadableMetadata::clone(&key),
+ status: val.status(),
+ progress: val.progress().get_progress()
+ }})
+ .collect();
+
+ let event_data = QueueUpdateEvent {
+ queue: queue_objs,
+ };
+ self.app_handle.emit("update_queue", event_data).unwrap();
+ }
+}
\ No newline at end of file
diff --git a/src-tauri/src/downloads/download_thread_control_flag.rs b/src-tauri/src/download_manager/download_thread_control_flag.rs
similarity index 100%
rename from src-tauri/src/downloads/download_thread_control_flag.rs
rename to src-tauri/src/download_manager/download_thread_control_flag.rs
diff --git a/src-tauri/src/download_manager/downloadable.rs b/src-tauri/src/download_manager/downloadable.rs
new file mode 100644
index 0000000..e917882
--- /dev/null
+++ b/src-tauri/src/download_manager/downloadable.rs
@@ -0,0 +1,20 @@
+use std::{fmt::{self, Debug}, sync::{mpsc::Sender, Arc}};
+
+use tauri::AppHandle;
+
+use super::{
+ application_download_error::ApplicationDownloadError, download_manager::{DownloadManagerSignal, DownloadStatus}, download_thread_control_flag::DownloadThreadControl, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject
+};
+
+pub trait Downloadable: Send + Sync {
+ fn download(&self, app_handle: &AppHandle) -> Result;
+ fn progress(&self) -> Arc;
+ fn control_flag(&self) -> DownloadThreadControl;
+ fn status(&self) -> DownloadStatus;
+ fn metadata(&self) -> DownloadableMetadata;
+ fn on_initialised(&self, app_handle: &AppHandle);
+ fn on_error(&self, app_handle: &AppHandle, error: ApplicationDownloadError);
+ fn on_complete(&self, app_handle: &AppHandle);
+ fn on_incomplete(&self, app_handle: &AppHandle);
+ fn on_cancelled(&self, app_handle: &AppHandle);
+}
\ No newline at end of file
diff --git a/src-tauri/src/download_manager/downloadable_metadata.rs b/src-tauri/src/download_manager/downloadable_metadata.rs
new file mode 100644
index 0000000..7512af4
--- /dev/null
+++ b/src-tauri/src/download_manager/downloadable_metadata.rs
@@ -0,0 +1,26 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone, Copy)]
+pub enum DownloadType {
+ Game,
+ Tool,
+ DLC,
+ Mod
+}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct DownloadableMetadata {
+ pub id: String,
+ pub version: Option,
+ pub download_type: DownloadType
+}
+impl DownloadableMetadata {
+ pub fn new(id: String, version: Option, download_type: DownloadType) -> Self {
+ Self {
+ id,
+ version,
+ download_type
+ }
+ }
+}
\ No newline at end of file
diff --git a/src-tauri/src/download_manager/generate_downloadable.rs b/src-tauri/src/download_manager/generate_downloadable.rs
new file mode 100644
index 0000000..8f53185
--- /dev/null
+++ b/src-tauri/src/download_manager/generate_downloadable.rs
@@ -0,0 +1,7 @@
+use std::sync::Arc;
+
+use super::{download_manager_builder::DownloadAgent, downloadable_metadata::DownloadableMetadata};
+
+pub fn generate_downloadable(meta: DownloadableMetadata) -> DownloadAgent {
+ todo!()
+}
\ No newline at end of file
diff --git a/src-tauri/src/download_manager/mod.rs b/src-tauri/src/download_manager/mod.rs
new file mode 100644
index 0000000..6299068
--- /dev/null
+++ b/src-tauri/src/download_manager/mod.rs
@@ -0,0 +1,9 @@
+pub mod download_manager;
+pub mod download_manager_builder;
+pub mod progress_object;
+pub mod queue;
+pub mod download_thread_control_flag;
+pub mod downloadable;
+pub mod application_download_error;
+pub mod downloadable_metadata;
+pub mod generate_downloadable;
\ No newline at end of file
diff --git a/src-tauri/src/downloads/progress_object.rs b/src-tauri/src/download_manager/progress_object.rs
similarity index 58%
rename from src-tauri/src/downloads/progress_object.rs
rename to src-tauri/src/download_manager/progress_object.rs
index 143656d..a3c3c24 100644
--- a/src-tauri/src/downloads/progress_object.rs
+++ b/src-tauri/src/download_manager/progress_object.rs
@@ -2,7 +2,7 @@ use std::{
sync::{
atomic::{AtomicUsize, Ordering},
mpsc::Sender,
- Arc, Mutex,
+ Arc, Mutex, RwLock,
},
time::Instant,
};
@@ -19,7 +19,9 @@ pub struct ProgressObject {
sender: Sender,
points_towards_update: Arc,
- points_to_push_update: Arc>,
+ points_to_push_update: Arc,
+ last_update: Arc>,
+ amount_last_update: Arc,
}
pub struct ProgressHandle {
@@ -58,7 +60,9 @@ impl ProgressObject {
sender,
points_towards_update: Arc::new(AtomicUsize::new(0)),
- points_to_push_update: Arc::new(Mutex::new(points_to_push_update)),
+ points_to_push_update: Arc::new(AtomicUsize::new(points_to_push_update)),
+ last_update: Arc::new(RwLock::new(Instant::now())),
+ amount_last_update: Arc::new(AtomicUsize::new(0)),
}
}
@@ -67,16 +71,44 @@ impl ProgressObject {
.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;
- drop(to_update_handle);
+ let to_update = self.points_to_push_update.fetch_add(0, Ordering::Relaxed);
- if current_amount < to_update {
- return;
+ if current_amount >= to_update {
+ self.points_towards_update
+ .fetch_sub(to_update, Ordering::Relaxed);
+ self.sender
+ .send(DownloadManagerSignal::UpdateUIQueue)
+ .unwrap();
+ }
+
+ let last_update = self.last_update.read().unwrap();
+ let last_update_difference = Instant::now().duration_since(*last_update).as_millis();
+ if last_update_difference > 1000 {
+ // push update
+ drop(last_update);
+ let mut last_update = self.last_update.write().unwrap();
+ *last_update = Instant::now();
+ drop(last_update);
+
+ let current_amount = self.sum();
+ let max = self.get_max();
+ let amount_at_last_update = self.amount_last_update.fetch_add(0, Ordering::Relaxed);
+ self.amount_last_update
+ .store(current_amount, Ordering::Relaxed);
+
+ let amount_since_last_update = current_amount - amount_at_last_update;
+
+ let kilobytes_per_second = amount_since_last_update / (last_update_difference as usize).max(1);
+
+ let remaining = max - current_amount; // bytes
+ let time_remaining = (remaining / 1000) / kilobytes_per_second.max(1);
+ self.sender
+ .send(DownloadManagerSignal::UpdateUIStats(
+ kilobytes_per_second,
+ time_remaining,
+ ))
+ .unwrap();
}
- self.points_towards_update
- .fetch_sub(to_update, Ordering::Relaxed);
- self.sender.send(DownloadManagerSignal::Update).unwrap();
}
pub fn set_time_now(&self) {
@@ -95,7 +127,8 @@ impl ProgressObject {
}
pub fn set_max(&self, new_max: usize) {
*self.max.lock().unwrap() = new_max;
- *self.points_to_push_update.lock().unwrap() = new_max / PROGRESS_UPDATES;
+ self.points_to_push_update
+ .store(new_max / PROGRESS_UPDATES, Ordering::Relaxed);
info!("points to push update: {}", new_max / PROGRESS_UPDATES);
}
pub fn set_size(&self, length: usize) {
diff --git a/src-tauri/src/downloads/queue.rs b/src-tauri/src/download_manager/queue.rs
similarity index 56%
rename from src-tauri/src/downloads/queue.rs
rename to src-tauri/src/download_manager/queue.rs
index 0ea65ca..cda08df 100644
--- a/src-tauri/src/downloads/queue.rs
+++ b/src-tauri/src/download_manager/queue.rs
@@ -3,11 +3,11 @@ use std::{
sync::{Arc, Mutex, MutexGuard},
};
-use super::download_manager::GameDownloadAgentQueueStandin;
+use super::downloadable_metadata::DownloadableMetadata;
#[derive(Clone)]
pub struct Queue {
- inner: Arc>>>,
+ inner: Arc>>,
}
#[allow(dead_code)]
@@ -17,49 +17,52 @@ 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 {
self.inner.lock().unwrap().len() == 0
}
+ pub fn exists(&self, meta: DownloadableMetadata) -> bool {
+ self.read().contains(&meta)
+ }
/// 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: GameDownloadAgentQueueStandin, index: usize) {
+ pub fn insert(&self, interface: DownloadableMetadata, index: usize) {
if self.read().len() > index {
self.append(interface);
} else {
- self.edit().insert(index, Arc::new(interface));
+ self.edit().insert(index, interface);
}
}
- pub fn append(&self, interface: GameDownloadAgentQueueStandin) {
- self.edit().push_back(Arc::new(interface));
+ pub fn append(&self, interface: DownloadableMetadata) {
+ self.edit().push_back(interface);
}
pub fn pop_front_if_equal(
&self,
- game_id: String,
- ) -> Option> {
+ meta: &DownloadableMetadata,
+ ) -> Option {
let mut queue = self.edit();
let front = match queue.front() {
Some(front) => front,
None => return None,
};
- if front.id == game_id {
+ if front == meta {
return queue.pop_front();
}
None
}
- pub fn get_by_id(&self, game_id: String) -> Option {
- self.read().iter().position(|data| data.id == game_id)
+ pub fn get_by_meta(&self, meta: &DownloadableMetadata) -> Option {
+ self.read().iter().position(|data| data == meta)
}
- pub fn move_to_index_by_id(&self, game_id: String, new_index: usize) -> Result<(), ()> {
- let index = match self.get_by_id(game_id) {
+ pub fn move_to_index_by_meta(&self, meta: &DownloadableMetadata, new_index: usize) -> Result<(), ()> {
+ let index = match self.get_by_meta(meta) {
Some(index) => index,
None => return Err(()),
};
diff --git a/src-tauri/src/downloads/download_agent.rs b/src-tauri/src/downloads/download_agent.rs
deleted file mode 100644
index 99d8217..0000000
--- a/src-tauri/src/downloads/download_agent.rs
+++ /dev/null
@@ -1,335 +0,0 @@
-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 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;
-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")]
-use rustix::fs::{fallocate, FallocateFlags};
-
-use super::download_logic::download_game_chunk;
-use super::download_manager::DownloadManagerSignal;
-use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
-use super::progress_object::ProgressObject;
-use super::stored_manifest::StoredManifest;
-
-pub struct GameDownloadAgent {
- pub id: String,
- pub version: String,
- pub control_flag: DownloadThreadControl,
- contexts: Vec,
- completed_contexts: Mutex>,
- pub manifest: Mutex>,
- pub progress: Arc,
- sender: Sender,
- pub stored_manifest: StoredManifest,
-}
-
-#[derive(Debug)]
-pub enum GameDownloadError {
- Communication(RemoteAccessError),
- Checksum,
- Setup(SetupError),
- Lock,
- IoError(io::Error),
- DownloadError,
-}
-
-#[derive(Debug)]
-pub enum SetupError {
- Context,
-}
-
-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, "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),
- GameDownloadError::DownloadError => write!(f, "Download failed. See Download Manager status for specific error"),
- }
- }
-}
-
-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,
- version: String,
- target_download_dir: usize,
- sender: Sender,
- ) -> 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());
-
- let stored_manifest =
- StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone());
-
- Self {
- id,
- version,
- control_flag,
- manifest: Mutex::new(None),
- contexts: Vec::new(),
- completed_contexts: Mutex::new(Vec::new()),
- progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
- sender,
- stored_manifest,
- }
- }
-
- // Blocking
- pub fn setup_download(&mut self) -> Result<(), GameDownloadError> {
- self.ensure_manifest_exists()?;
- info!("Ensured manifest exists");
-
- self.ensure_contexts()?;
- info!("Ensured contexts exists");
-
- self.control_flag.set(DownloadThreadControlFlag::Go);
-
- Ok(())
- }
-
- // Blocking
- pub fn download(&mut 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(())
- }
-
- pub fn ensure_manifest_exists(&self) -> Result<(), GameDownloadError> {
- if self.manifest.lock().unwrap().is_some() {
- return Ok(());
- }
-
- self.download_manifest()
- }
-
- fn download_manifest(&self) -> Result<(), GameDownloadError> {
- let base_url = DB.fetch_base_url();
- let manifest_url = base_url
- .join(
- format!(
- "/api/v1/client/metadata/manifest?id={}&version={}",
- self.id,
- encode(&self.version)
- )
- .as_str(),
- )
- .unwrap();
-
- let header = generate_authorization_header();
- let client = reqwest::blocking::Client::new();
- let response = client
- .get(manifest_url.to_string())
- .header("Authorization", header)
- .send()
- .unwrap();
-
- if response.status() != 200 {
- return Err(GameDownloadError::Communication(
- RemoteAccessError::ManifestDownloadFailed(
- response.status(),
- response.text().unwrap(),
- ),
- ));
- }
-
- let manifest_download = response.json::().unwrap();
-
- if let Ok(mut manifest) = self.manifest.lock() {
- *manifest = Some(manifest_download);
- return Ok(());
- }
-
- Err(GameDownloadError::Lock)
- }
-
- fn set_progress_object_params(&self) {
- // Avoid re-setting it
- if self.progress.get_max() != 0 {
- return;
- }
-
- let length = self.contexts.len();
-
- let chunk_count = self.contexts.iter().map(|chunk| chunk.length).sum();
-
- debug!("Setting ProgressObject max to {}", chunk_count);
- self.progress.set_max(chunk_count);
- debug!("Setting ProgressObject size to {}", length);
- self.progress.set_size(length);
- debug!("Setting ProgressObject time to now");
- self.progress.set_time_now();
- }
-
- pub fn ensure_contexts(&mut self) -> Result<(), GameDownloadError> {
- if !self.contexts.is_empty() {
- return Ok(());
- }
-
- self.generate_contexts()?;
- Ok(())
- }
-
- pub fn generate_contexts(&mut self) -> Result<(), GameDownloadError> {
- let manifest = self.manifest.lock().unwrap().clone().unwrap();
- let game_id = self.id.clone();
-
- let mut contexts = Vec::new();
- let base_path = Path::new(&self.stored_manifest.base_path);
- create_dir_all(base_path).unwrap();
-
- *self.completed_contexts.lock().unwrap() = self.stored_manifest.get_completed_contexts();
-
- info!(
- "Completed contexts: {:?}",
- *self.completed_contexts.lock().unwrap()
- );
-
- for (raw_path, chunk) in manifest {
- let path = base_path.join(Path::new(&raw_path));
-
- let container = path.parent().unwrap();
- create_dir_all(container).unwrap();
-
- let file = File::create(path.clone()).unwrap();
- let mut running_offset = 0;
-
- for (index, length) in chunk.lengths.iter().enumerate() {
- contexts.push(DropDownloadContext {
- file_name: raw_path.to_string(),
- version: chunk.version_name.to_string(),
- offset: running_offset,
- index,
- game_id: game_id.to_string(),
- path: path.clone(),
- checksum: chunk.checksums[index].clone(),
- length: *length,
- permissions: chunk.permissions,
- });
- running_offset += *length as u64;
- }
-
- #[cfg(target_os = "linux")]
- if running_offset > 0 {
- let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
- }
- }
- self.contexts = contexts;
-
- Ok(())
- }
-
- pub fn run(&self) -> Result<(), ()> {
- info!("downloading game: {}", self.id);
- const DOWNLOAD_MAX_THREADS: usize = 1;
-
- let pool = ThreadPoolBuilder::new()
- .num_threads(DOWNLOAD_MAX_THREADS)
- .build()
- .unwrap();
-
- let completed_indexes = Arc::new(Mutex::new(Vec::new()));
- let completed_indexes_loop_arc = completed_indexes.clone();
-
- pool.scope(move |scope| {
- let completed_lock = self.completed_contexts.lock().unwrap();
-
- for (index, context) in self.contexts.iter().enumerate() {
- let progress = self.progress.get(index); // Clone arcs
- let progress_handle = ProgressHandle::new(progress, self.progress.clone());
- // If we've done this one already, skip it
- if completed_lock.contains(&index) {
- progress_handle.add(context.length);
- continue;
- }
-
- let context = context.clone();
- let control_flag = self.control_flag.clone(); // Clone arcs
- let completed_indexes_ref = completed_indexes_loop_arc.clone();
-
- scope.spawn(move |_| {
- match download_game_chunk(context.clone(), control_flag, progress_handle) {
- Ok(res) => {
- if res {
- let mut lock = completed_indexes_ref.lock().unwrap();
- lock.push(index);
- }
- }
- Err(e) => {
- error!("GameDownloadError: {}", e);
- self.sender.send(DownloadManagerSignal::Error(e)).unwrap();
- }
- }
- });
- }
- });
-
- let completed_lock_len = {
- let mut completed_lock = self.completed_contexts.lock().unwrap();
- let newly_completed_lock = completed_indexes.lock().unwrap();
-
- completed_lock.extend(newly_completed_lock.iter());
-
- completed_lock.len()
- };
-
- // If we're not out of contexts, we're not done, so we don't fire completed
- if completed_lock_len != self.contexts.len() {
- info!("da for {} exited without completing", self.id.clone());
- self.stored_manifest
- .set_completed_contexts(&self.completed_contexts);
- info!("Setting completed contexts");
- self.stored_manifest.write();
- info!("Wrote completed contexts");
- return Ok(());
- }
-
- // We've completed
- self.sender
- .send(DownloadManagerSignal::Completed(self.id.clone()))
- .unwrap();
-
- Ok(())
- }
-}
diff --git a/src-tauri/src/downloads/download_manager_builder.rs b/src-tauri/src/downloads/download_manager_builder.rs
deleted file mode 100644
index 9d2ab9d..0000000
--- a/src-tauri/src/downloads/download_manager_builder.rs
+++ /dev/null
@@ -1,417 +0,0 @@
-use std::{
- collections::HashMap,
- sync::{
- mpsc::{channel, Receiver, Sender},
- Arc, Mutex, RwLockWriteGuard,
- },
- thread::{spawn, JoinHandle},
-};
-
-use log::{error, info};
-use tauri::{AppHandle, Emitter};
-
-use crate::{
- db::{Database, GameStatus, GameTransientStatus},
- library::{on_game_complete, GameUpdateEvent, QueueUpdateEvent, QueueUpdateEventQueueData},
- state::GameStatusManager,
- DB,
-};
-
-use super::{
- download_agent::{GameDownloadAgent, GameDownloadError},
- download_manager::{
- DownloadManager, DownloadManagerSignal, DownloadManagerStatus,
- GameDownloadAgentQueueStandin, 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
-
-*/
-
-// 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: CurrentProgressObject,
- status: Arc>,
- 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,
-}
-
-impl DownloadManagerBuilder {
- 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));
- let status = Arc::new(Mutex::new(DownloadManagerStatus::Empty));
-
- let manager = Self {
- download_agent_registry: HashMap::new(),
- download_queue: queue.clone(),
- command_receiver,
- 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());
-
- DownloadManager::new(terminator, queue, active_progress, command_sender)
- }
-
- fn set_game_status, &String) -> ()>(
- &self,
- id: String,
- setter: F,
- ) {
- let mut db_handle = DB.borrow_data_mut().unwrap();
- 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),
- GameUpdateEvent {
- game_id: id,
- status,
- },
- )
- .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 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> {
- self.download_queue.pop_front();
- let download_agent = self.download_agent_registry.remove(game_id).unwrap();
- self.cleanup_current_download();
- 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;
-
- 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<(), ()> {
- 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::Error(e) => {
- self.manage_error_signal(e);
- }
- DownloadManagerSignal::Cancel => {
- self.manage_cancel_signal();
- }
- DownloadManagerSignal::Update => {
- self.push_manager_update();
- }
- DownloadManagerSignal::Finish => {
- 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);
- 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_download_agent {
- // When if let chains are stabilised, combine these two statements
- 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();
-
- let version = download_agent_lock.version.clone();
- let install_dir = download_agent_lock.stored_manifest.base_path.clone().to_string_lossy().to_string();
-
- 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),
- ))
- .unwrap();
- }
- }
- }
- self.sender.send(DownloadManagerSignal::Update).unwrap();
- 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(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_lock.progress.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);
-
- 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();
- }
-
- fn manage_go_signal(&mut self) {
- if !(!self.download_agent_registry.is_empty() && !self.download_queue.empty()) {
- return;
- }
-
- if self.current_download_agent.is_some() {
- info!("skipping go signal due to existing download job");
- return;
- }
-
- 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)
- .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_lock.version.clone();
-
- let progress_object = download_agent_lock.progress.clone();
- *self.progress.lock().unwrap() = Some(progress_object);
-
- 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 || {
- 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
- Ok(_) => {}
- // If an error occurred while *starting* the download
- Err(err) => {
- error!("error while managing download: {}", err);
- sender.send(DownloadManagerSignal::Error(err)).unwrap();
- }
- };
- drop(download_agent_lock);
- }));
-
- // Set status for games
- for queue_game in self.download_queue.read() {
- let mut status_handle = queue_game.status.lock().unwrap();
- if queue_game.id == agent_data.id {
- *status_handle = GameDownloadStatus::Downloading;
- } else {
- *status_handle = GameDownloadStatus::Queued;
- }
- drop(status_handle);
- }
-
- // Set flags for download manager
- active_control_flag.set(DownloadThreadControlFlag::Go);
- self.set_status(DownloadManagerStatus::Downloading);
- 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();
- }
- fn manage_error_signal(&mut self, error: GameDownloadError) {
- let current_status = self.current_download_agent.clone().unwrap();
-
- 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;
- self.set_status(DownloadManagerStatus::Error(error));
-
- let game_id = current_status.id.clone();
- self.set_game_status(game_id, |db_handle, id| {
- db_handle.games.transient_statuses.remove(id);
- });
-
- self.sender.send(DownloadManagerSignal::Update).unwrap();
- }
- fn manage_cancel_signal(&mut self) {
- self.stop_and_wait_current_download();
-
- info!("cancel waited for download to finish");
-
- self.cleanup_current_download();
- }
- fn set_status(&self, status: DownloadManagerStatus) {
- *self.status.lock().unwrap() = status;
- }
-}
diff --git a/src-tauri/src/downloads/mod.rs b/src-tauri/src/downloads/mod.rs
deleted file mode 100644
index 023b2c7..0000000
--- a/src-tauri/src/downloads/mod.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-pub mod download_agent;
-pub mod download_commands;
-mod download_logic;
-pub mod download_manager;
-pub mod download_manager_builder;
-mod download_thread_control_flag;
-mod manifest;
-mod progress_object;
-pub mod queue;
-mod stored_manifest;
\ No newline at end of file
diff --git a/src-tauri/src/games/downloads/download_agent.rs b/src-tauri/src/games/downloads/download_agent.rs
new file mode 100644
index 0000000..5a92380
--- /dev/null
+++ b/src-tauri/src/games/downloads/download_agent.rs
@@ -0,0 +1,404 @@
+use crate::auth::generate_authorization_header;
+use crate::db::{set_game_status, GameDownloadStatus, ApplicationTransientStatus, DatabaseImpls};
+use crate::download_manager::application_download_error::ApplicationDownloadError;
+use crate::download_manager::download_manager::{DownloadManagerSignal, DownloadStatus};
+use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
+use crate::download_manager::downloadable::Downloadable;
+use crate::download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata};
+use crate::download_manager::progress_object::{ProgressHandle, ProgressObject};
+use crate::games::downloads::manifest::{DropDownloadContext, DropManifest};
+use crate::games::library::{on_game_complete, push_game_update};
+use crate::remote::RemoteAccessError;
+use crate::DB;
+use log::{debug, error, info};
+use rayon::ThreadPoolBuilder;
+use tauri::{AppHandle, Emitter};
+use std::collections::VecDeque;
+use std::fs::{create_dir_all, remove_dir_all, File};
+use std::path::Path;
+use std::sync::mpsc::Sender;
+use std::sync::{Arc, Mutex};
+use std::thread::spawn;
+use std::time::Instant;
+use urlencoding::encode;
+
+#[cfg(target_os = "linux")]
+use rustix::fs::{fallocate, FallocateFlags};
+
+use super::download_logic::download_game_chunk;
+use super::stored_manifest::StoredManifest;
+
+pub struct GameDownloadAgent {
+ pub id: String,
+ pub version: String,
+ pub control_flag: DownloadThreadControl,
+ contexts: Mutex>,
+ completed_contexts: Mutex>,
+ pub manifest: Mutex>,
+ pub progress: Arc,
+ sender: Sender,
+ pub stored_manifest: StoredManifest,
+ status: Mutex
+}
+
+impl GameDownloadAgent {
+ 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);
+
+ let db_lock = DB.borrow_data().unwrap();
+ let base_dir = db_lock.applications.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());
+
+ let stored_manifest =
+ StoredManifest::generate(id.clone(), version.clone(), data_base_dir_path.clone());
+
+ Self {
+ id,
+ version,
+ control_flag,
+ manifest: Mutex::new(None),
+ contexts: Mutex::new(Vec::new()),
+ completed_contexts: Mutex::new(VecDeque::new()),
+ progress: Arc::new(ProgressObject::new(0, 0, sender.clone())),
+ sender,
+ stored_manifest,
+ status: Mutex::new(DownloadStatus::Queued),
+ }
+ }
+
+ // Blocking
+ pub fn setup_download(&self) -> Result<(), ApplicationDownloadError> {
+ self.ensure_manifest_exists()?;
+ info!("Ensured manifest exists");
+
+ self.ensure_contexts()?;
+ info!("Ensured contexts exists");
+
+ self.control_flag.set(DownloadThreadControlFlag::Go);
+
+ Ok(())
+ }
+
+ // Blocking
+ pub fn download(&self, app_handle: &AppHandle) -> Result {
+ info!("Setting up download");
+ self.setup_download()?;
+ info!("Setting progress object params");
+ self.set_progress_object_params();
+ info!("Running");
+ let timer = Instant::now();
+ push_game_update(app_handle, &self.metadata(), (None, Some(ApplicationTransientStatus::Downloading { version_name: self.version.clone() })));
+ let res = self.run().map_err(|_| ApplicationDownloadError::DownloadError);
+
+ info!(
+ "{} took {}ms to download",
+ self.id,
+ timer.elapsed().as_millis()
+ );
+ res
+ }
+
+ pub fn ensure_manifest_exists(&self) -> Result<(), ApplicationDownloadError> {
+ if self.manifest.lock().unwrap().is_some() {
+ return Ok(());
+ }
+
+ self.download_manifest()
+ }
+
+ fn download_manifest(&self) -> Result<(), ApplicationDownloadError> {
+ let base_url = DB.fetch_base_url();
+ let manifest_url = base_url
+ .join(
+ format!(
+ "/api/v1/client/metadata/manifest?id={}&version={}",
+ self.id,
+ encode(&self.version)
+ )
+ .as_str(),
+ )
+ .unwrap();
+
+ let header = generate_authorization_header();
+ let client = reqwest::blocking::Client::new();
+ let response = client
+ .get(manifest_url.to_string())
+ .header("Authorization", header)
+ .send()
+ .unwrap();
+
+ if response.status() != 200 {
+ return Err(ApplicationDownloadError::Communication(
+ RemoteAccessError::ManifestDownloadFailed(
+ response.status(),
+ response.text().unwrap(),
+ ),
+ ));
+ }
+
+ let manifest_download = response.json::().unwrap();
+
+ if let Ok(mut manifest) = self.manifest.lock() {
+ *manifest = Some(manifest_download);
+ return Ok(());
+ }
+
+ Err(ApplicationDownloadError::Lock)
+ }
+
+ fn set_progress_object_params(&self) {
+ // Avoid re-setting it
+ if self.progress.get_max() != 0 {
+ return;
+ }
+
+ let contexts = self.contexts.lock().unwrap();
+
+ let length = contexts.len();
+
+ let chunk_count = contexts.iter().map(|chunk| chunk.length).sum();
+
+ debug!("Setting ProgressObject max to {}", chunk_count);
+ self.progress.set_max(chunk_count);
+ debug!("Setting ProgressObject size to {}", length);
+ self.progress.set_size(length);
+ debug!("Setting ProgressObject time to now");
+ self.progress.set_time_now();
+ }
+
+ pub fn ensure_contexts(&self) -> Result<(), ApplicationDownloadError> {
+ if !self.contexts.lock().unwrap().is_empty() {
+ return Ok(());
+ }
+
+ self.generate_contexts()?;
+ Ok(())
+ }
+
+ pub fn generate_contexts(&self) -> Result<(), ApplicationDownloadError> {
+ let manifest = self.manifest.lock().unwrap().clone().unwrap();
+ let game_id = self.id.clone();
+
+ let mut contexts = Vec::new();
+ let base_path = Path::new(&self.stored_manifest.base_path);
+ create_dir_all(base_path).unwrap();
+
+
+ {
+ let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
+ completed_contexts_lock.clear();
+ completed_contexts_lock
+ .extend(self.stored_manifest.get_completed_contexts());
+ }
+
+ for (raw_path, chunk) in manifest {
+ let path = base_path.join(Path::new(&raw_path));
+
+ let container = path.parent().unwrap();
+ create_dir_all(container).unwrap();
+
+ let file = File::create(path.clone()).unwrap();
+ let mut running_offset = 0;
+
+ for (index, length) in chunk.lengths.iter().enumerate() {
+ contexts.push(DropDownloadContext {
+ file_name: raw_path.to_string(),
+ version: chunk.version_name.to_string(),
+ offset: running_offset,
+ index,
+ game_id: game_id.to_string(),
+ path: path.clone(),
+ checksum: chunk.checksums[index].clone(),
+ length: *length,
+ permissions: chunk.permissions,
+ });
+ running_offset += *length as u64;
+ }
+
+ #[cfg(target_os = "linux")]
+ if running_offset > 0 {
+ let _ = fallocate(file, FallocateFlags::empty(), 0, running_offset);
+ }
+ }
+ *self.contexts.lock().unwrap() = contexts;
+
+ Ok(())
+ }
+
+ pub fn run(&self) -> Result {
+ info!("downloading game: {}", self.id);
+ const DOWNLOAD_MAX_THREADS: usize = 1;
+
+ let pool = ThreadPoolBuilder::new()
+ .num_threads(DOWNLOAD_MAX_THREADS)
+ .build()
+ .unwrap();
+
+ let completed_indexes = Arc::new(boxcar::Vec::new());
+ let completed_indexes_loop_arc = completed_indexes.clone();
+
+ let base_url = DB.fetch_base_url();
+
+
+
+ let contexts = self.contexts.lock().unwrap();
+ pool.scope(|scope| {
+ let client = &reqwest::blocking::Client::new();
+ for (index, context) in contexts.iter().enumerate() {
+ let client = client.clone();
+ let completed_indexes = completed_indexes_loop_arc.clone();
+
+ let progress = self.progress.get(index);
+ let progress_handle = ProgressHandle::new(progress, self.progress.clone());
+ // If we've done this one already, skip it
+ if self.completed_contexts.lock().unwrap().contains(&index) {
+ progress_handle.add(context.length);
+ continue;
+ }
+
+ let sender = self.sender.clone();
+
+ let request = generate_request(&base_url, client, &context);
+
+
+ scope.spawn(move |_| {
+ match download_game_chunk(context, &self.control_flag, progress_handle, request) {
+ Ok(res) => {
+ if res {
+ completed_indexes.push(index);
+ }
+ }
+ Err(e) => {
+ error!("{}", e);
+ sender.send(DownloadManagerSignal::Error(e)).unwrap();
+ }
+ }
+ info!("Completed context id {}", index);
+ });
+ }
+ });
+
+ let newly_completed = completed_indexes.to_owned();
+
+
+ let completed_lock_len = {
+ let mut completed_contexts_lock = self.completed_contexts.lock().unwrap();
+ for (item, _) in newly_completed.iter() {
+ completed_contexts_lock.push_front(item);
+ }
+
+ completed_contexts_lock.len()
+ };
+
+ info!("Got newly completed");
+
+ // If we're not out of contexts, we're not done, so we don't fire completed
+ if completed_lock_len != contexts.len() {
+ info!("da for {} exited without completing", self.id.clone());
+ self.stored_manifest
+ .set_completed_contexts(&self.completed_contexts.lock().unwrap().clone().into());
+ info!("Setting completed contexts");
+ self.stored_manifest.write();
+ info!("Wrote completed contexts");
+ return Ok(false);
+ }
+
+ info!("Sending completed signal");
+
+
+ // We've completed
+ self.sender
+ .send(DownloadManagerSignal::Completed(self.metadata()))
+ .unwrap();
+
+ Ok(true)
+ }
+}
+
+fn generate_request(base_url: &url::Url, client: reqwest::blocking::Client, context: &DropDownloadContext) -> reqwest::blocking::RequestBuilder {
+ let chunk_url = base_url
+ .join(&format!(
+ "/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
+ // Encode the parts we don't trust
+ context.game_id,
+ encode(&context.version),
+ encode(&context.file_name),
+ context.index
+ ))
+ .unwrap();
+
+ let header = generate_authorization_header();
+
+ let request = client
+ .get(chunk_url)
+ .header("Authorization", header);
+ request
+}
+
+impl Downloadable for GameDownloadAgent {
+ fn download(&self, app_handle: &AppHandle) -> Result {
+ *self.status.lock().unwrap() = DownloadStatus::Downloading;
+ self.download(app_handle)
+ }
+
+ fn progress(&self) -> Arc {
+ self.progress.clone()
+ }
+
+ fn control_flag(&self) -> DownloadThreadControl {
+ self.control_flag.clone()
+ }
+
+ fn metadata(&self) -> DownloadableMetadata {
+ DownloadableMetadata {
+ id: self.id.clone(),
+ version: Some(self.version.clone()),
+ download_type: DownloadType::Game,
+ }
+ }
+
+ fn on_initialised(&self, _app_handle: &tauri::AppHandle) {
+ *self.status.lock().unwrap() = DownloadStatus::Queued;
+ return;
+ }
+
+ fn on_error(&self, app_handle: &tauri::AppHandle, error: ApplicationDownloadError) {
+ *self.status.lock().unwrap() = DownloadStatus::Error;
+ app_handle
+ .emit("download_error", error.to_string())
+ .unwrap();
+
+ error!("error while managing download: {}", error);
+
+ set_game_status(app_handle, self.metadata(), |db_handle, meta| {
+ db_handle.applications.transient_statuses.remove(meta);
+ });
+
+ }
+
+ fn on_complete(&self, app_handle: &tauri::AppHandle) {
+ on_game_complete(&self.metadata(), self.stored_manifest.base_path.to_string_lossy().to_string(), app_handle).unwrap();
+ }
+
+ fn on_incomplete(&self, _app_handle: &tauri::AppHandle) {
+ *self.status.lock().unwrap() = DownloadStatus::Queued;
+ return;
+ }
+
+ fn on_cancelled(&self, _app_handle: &tauri::AppHandle) {
+ return;
+ }
+
+ fn status(&self) -> DownloadStatus {
+ self.status.lock().unwrap().clone()
+ }
+}
\ No newline at end of file
diff --git a/src-tauri/src/downloads/download_commands.rs b/src-tauri/src/games/downloads/download_commands.rs
similarity index 69%
rename from src-tauri/src/downloads/download_commands.rs
rename to src-tauri/src/games/downloads/download_commands.rs
index d3ca2e7..2bbd5e8 100644
--- a/src-tauri/src/downloads/download_commands.rs
+++ b/src-tauri/src/games/downloads/download_commands.rs
@@ -1,6 +1,8 @@
-use std::sync::Mutex;
+use std::sync::{Arc, Mutex};
-use crate::AppState;
+use crate::{download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, AppState};
+
+use super::download_agent::GameDownloadAgent;
#[tauri::command]
pub fn download_game(
@@ -9,11 +11,15 @@ pub fn download_game(
install_dir: usize,
state: tauri::State<'_, Mutex>,
) -> Result<(), String> {
+ let sender = state.lock().unwrap().download_manager.get_sender();
+ let game_download_agent = Arc::new(
+ Box::new(GameDownloadAgent::new(game_id, game_version, install_dir, sender)) as Box
+ );
state
.lock()
.unwrap()
.download_manager
- .queue_game(game_id, game_version, install_dir)
+ .queue_download(game_download_agent)
.map_err(|_| "An error occurred while communicating with the download manager.".to_string())
}
@@ -41,8 +47,8 @@ pub fn move_game_in_queue(
}
#[tauri::command]
-pub fn cancel_game(state: tauri::State<'_, Mutex>, game_id: String) {
- state.lock().unwrap().download_manager.cancel(game_id)
+pub fn cancel_game(state: tauri::State<'_, Mutex>, meta: DownloadableMetadata) {
+ state.lock().unwrap().download_manager.cancel(meta)
}
/*
diff --git a/src-tauri/src/downloads/download_logic.rs b/src-tauri/src/games/downloads/download_logic.rs
similarity index 71%
rename from src-tauri/src/downloads/download_logic.rs
rename to src-tauri/src/games/downloads/download_logic.rs
index 52cd6ba..ec4e87f 100644
--- a/src-tauri/src/downloads/download_logic.rs
+++ b/src-tauri/src/games/downloads/download_logic.rs
@@ -1,19 +1,20 @@
use crate::auth::generate_authorization_header;
use crate::db::DatabaseImpls;
-use crate::downloads::manifest::DropDownloadContext;
-use crate::remote::RemoteAccessError;
+use crate::download_manager::application_download_error::ApplicationDownloadError;
+use crate::download_manager::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
+use crate::download_manager::progress_object::ProgressHandle;
+use crate::games::downloads::manifest::DropDownloadContext;
+use crate::remote::{DropServerError, RemoteAccessError};
use crate::DB;
-use log::warn;
+use log::{error, info, warn};
use md5::{Context, Digest};
-use reqwest::blocking::Response;
-use tauri::utils::acl::Permission;
+use reqwest::blocking::{Client, Request, RequestBuilder, Response};
use std::fs::{set_permissions, Permissions};
use std::io::Read;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
-use std::thread::sleep;
-use std::time::Duration;
+use std::sync::Arc;
use std::{
fs::{File, OpenOptions},
io::{self, BufWriter, Seek, SeekFrom, Write},
@@ -21,10 +22,6 @@ use std::{
};
use urlencoding::encode;
-use super::download_agent::GameDownloadError;
-use super::download_thread_control_flag::{DownloadThreadControl, DownloadThreadControlFlag};
-use super::progress_object::ProgressHandle;
-
pub struct DropWriter {
hasher: Context,
destination: W,
@@ -68,18 +65,18 @@ impl Seek for DropWriter {
}
}
-pub struct DropDownloadPipeline {
+pub struct DropDownloadPipeline<'a, R: Read, W: Write> {
pub source: R,
pub destination: DropWriter,
- pub control_flag: DownloadThreadControl,
+ pub control_flag: &'a DownloadThreadControl,
pub progress: ProgressHandle,
pub size: usize,
}
-impl DropDownloadPipeline {
+impl<'a> DropDownloadPipeline<'a, Response, File> {
fn new(
source: Response,
destination: DropWriter,
- control_flag: DownloadThreadControl,
+ control_flag: &'a DownloadThreadControl,
progress: ProgressHandle,
size: usize,
) -> Self {
@@ -124,41 +121,24 @@ impl DropDownloadPipeline {
}
pub fn download_game_chunk(
- ctx: DropDownloadContext,
- control_flag: DownloadThreadControl,
+ ctx: &DropDownloadContext,
+ control_flag: &DownloadThreadControl,
progress: ProgressHandle,
-) -> Result {
+ request: RequestBuilder
+) -> Result {
// If we're paused
if control_flag.get() == DownloadThreadControlFlag::Stop {
progress.set(0);
return Ok(false);
}
- let base_url = DB.fetch_base_url();
-
- let client = reqwest::blocking::Client::new();
- let chunk_url = base_url
- .join(&format!(
- "/api/v1/client/chunk?id={}&version={}&name={}&chunk={}",
- // Encode the parts we don't trust
- ctx.game_id,
- encode(&ctx.version),
- encode(&ctx.file_name),
- ctx.index
- ))
- .unwrap();
-
- let header = generate_authorization_header();
-
- let response = client
- .get(chunk_url)
- .header("Authorization", header)
+ let response = request
.send()
- .map_err(|e| GameDownloadError::Communication(e.into()))?;
+ .map_err(|e| ApplicationDownloadError::Communication(e.into()))?;
if response.status() != 200 {
warn!("{}", response.text().unwrap());
- return Err(GameDownloadError::Communication(
+ return Err(ApplicationDownloadError::Communication(
RemoteAccessError::InvalidCodeError(400),
));
}
@@ -173,8 +153,9 @@ pub fn download_game_chunk(
let content_length = response.content_length();
if content_length.is_none() {
- return Err(GameDownloadError::Communication(
- RemoteAccessError::InvalidResponse,
+ error!("Recieved 0 length content from server");
+ return Err(ApplicationDownloadError::Communication(
+ RemoteAccessError::InvalidResponse(response.json::().unwrap()),
));
}
@@ -186,7 +167,9 @@ pub fn download_game_chunk(
content_length.unwrap().try_into().unwrap(),
);
- let completed = pipeline.copy().map_err(GameDownloadError::IoError)?;
+ let completed = pipeline
+ .copy()
+ .map_err(|e| ApplicationDownloadError::IoError(e.kind()))?;
if !completed {
return Ok(false);
};
@@ -195,7 +178,7 @@ pub fn download_game_chunk(
#[cfg(unix)]
{
let permissions = Permissions::from_mode(ctx.permissions);
- set_permissions(ctx.path, permissions).unwrap();
+ set_permissions(ctx.path.clone(), permissions).unwrap();
}
/*
diff --git a/src-tauri/src/downloads/manifest.rs b/src-tauri/src/games/downloads/manifest.rs
similarity index 100%
rename from src-tauri/src/downloads/manifest.rs
rename to src-tauri/src/games/downloads/manifest.rs
diff --git a/src-tauri/src/games/downloads/mod.rs b/src-tauri/src/games/downloads/mod.rs
new file mode 100644
index 0000000..ba0fac2
--- /dev/null
+++ b/src-tauri/src/games/downloads/mod.rs
@@ -0,0 +1,5 @@
+pub mod download_agent;
+pub mod download_commands;
+mod download_logic;
+mod manifest;
+mod stored_manifest;
\ No newline at end of file
diff --git a/src-tauri/src/downloads/stored_manifest.rs b/src-tauri/src/games/downloads/stored_manifest.rs
similarity index 87%
rename from src-tauri/src/downloads/stored_manifest.rs
rename to src-tauri/src/games/downloads/stored_manifest.rs
index cd4006d..bb5e6b9 100644
--- a/src-tauri/src/downloads/stored_manifest.rs
+++ b/src-tauri/src/games/downloads/stored_manifest.rs
@@ -1,12 +1,11 @@
use std::{
- default,
fs::File,
io::{Read, Write},
- path::{Path, PathBuf},
+ path::PathBuf,
sync::Mutex,
};
-use log::{error, info};
+use log::error;
use serde::{Deserialize, Serialize};
use serde_binary::binary_stream::Endian;
@@ -44,15 +43,15 @@ impl StoredManifest {
}
};
- let manifest = match serde_binary::from_vec::(s, Endian::Little) {
+
+
+ match serde_binary::from_vec::(s, Endian::Little) {
Ok(manifest) => manifest,
Err(e) => {
error!("{}", e);
StoredManifest::new(game_id, game_version, base_path)
}
- };
-
- return manifest;
+ }
}
pub fn write(&self) {
let manifest_raw = match serde_binary::to_vec(&self, Endian::Little) {
@@ -73,8 +72,8 @@ impl StoredManifest {
Err(e) => error!("{}", e),
};
}
- pub fn set_completed_contexts(&self, completed_contexts: &Mutex>) {
- *self.completed_contexts.lock().unwrap() = completed_contexts.lock().unwrap().clone();
+ pub fn set_completed_contexts(&self, completed_contexts: &Vec) {
+ *self.completed_contexts.lock().unwrap() = completed_contexts.clone();
}
pub fn get_completed_contexts(&self) -> Vec {
self.completed_contexts.lock().unwrap().clone()
diff --git a/src-tauri/src/library.rs b/src-tauri/src/games/library.rs
similarity index 58%
rename from src-tauri/src/library.rs
rename to src-tauri/src/games/library.rs
index 6e8536e..8d40274 100644
--- a/src-tauri/src/library.rs
+++ b/src-tauri/src/games/library.rs
@@ -1,17 +1,20 @@
+use std::fs::remove_dir_all;
use std::sync::Mutex;
+use std::thread::spawn;
+use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use tauri::Emitter;
use tauri::{AppHandle, Manager};
use urlencoding::encode;
-use crate::db::DatabaseImpls;
+use crate::db::{ApplicationTransientStatus, DatabaseImpls, GameDownloadStatus};
use crate::db::GameVersion;
-use crate::db::{GameStatus, GameTransientStatus};
-use crate::downloads::download_manager::GameDownloadStatus;
+use crate::download_manager::download_manager::DownloadStatus;
+use crate::download_manager::downloadable_metadata::DownloadableMetadata;
use crate::process::process_manager::Platform;
use crate::remote::RemoteAccessError;
-use crate::state::{GameStatusManager, GameStatusWithTransient};
+use crate::games::state::{GameStatusManager, GameStatusWithTransient};
use crate::{auth::generate_authorization_header, AppState, DB};
#[derive(serde::Serialize)]
@@ -37,13 +40,13 @@ pub struct Game {
#[derive(serde::Serialize, Clone)]
pub struct GameUpdateEvent {
pub game_id: String,
- pub status: (Option, Option),
+ pub status: (Option, Option),
}
#[derive(Serialize, Clone)]
pub struct QueueUpdateEventQueueData {
- pub id: String,
- pub status: GameDownloadStatus,
+ pub meta: DownloadableMetadata,
+ pub status: DownloadStatus,
pub progress: f64,
}
@@ -52,6 +55,12 @@ pub struct QueueUpdateEvent {
pub queue: Vec,
}
+#[derive(serde::Serialize, Clone)]
+pub struct StatsUpdateEvent {
+ pub speed: usize,
+ pub time: usize,
+}
+
// Game version with some fields missing and size information
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")]
@@ -91,11 +100,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.statuses.contains_key(&game.id) {
+ if !db_handle.applications.game_statuses.contains_key(&game.id) {
db_handle
- .games
- .statuses
- .insert(game.id.clone(), GameStatus::Remote {});
+ .applications
+ .game_statuses
+ .insert(game.id.clone(), GameDownloadStatus::Remote {});
}
}
@@ -154,10 +163,10 @@ fn fetch_game_logic(
let mut db_handle = DB.borrow_data_mut().unwrap();
db_handle
- .games
- .statuses
+ .applications
+ .game_statuses
.entry(id.clone())
- .or_insert(GameStatus::Remote {});
+ .or_insert(GameDownloadStatus::Remote {});
drop(db_handle);
let status = GameStatusManager::fetch_state(&id);
@@ -171,8 +180,8 @@ fn fetch_game_logic(
}
#[tauri::command]
-pub fn fetch_game(id: String, app: tauri::AppHandle) -> Result {
- let result = fetch_game_logic(id, app);
+pub fn fetch_game(game_id: String, app: tauri::AppHandle) -> Result {
+ let result = fetch_game_logic(game_id, app);
if result.is_err() {
return Err(result.err().unwrap().to_string());
@@ -224,6 +233,89 @@ fn fetch_game_verion_options_logic<'a>(
Ok(data)
}
+#[tauri::command]
+pub fn uninstall_game(
+ game_id: String,
+ state: tauri::State<'_, Mutex>,
+ app_handle: AppHandle
+) -> Result<(), String> {
+ let meta = get_current_meta(&game_id)?;
+ println!("{:?}", meta);
+ uninstall_game_logic(meta, &app_handle);
+
+ Ok(())
+}
+
+fn uninstall_game_logic(meta: DownloadableMetadata, app_handle: &AppHandle) {
+ println!("Triggered uninstall for agent");
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ db_handle
+ .applications
+ .transient_statuses
+ .entry(meta.clone())
+ .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
+
+ push_game_update(
+ app_handle,
+ &meta,
+ (None, Some(ApplicationTransientStatus::Uninstalling {})),
+ );
+
+ let previous_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
+ if previous_state.is_none() {
+ info!("uninstall job doesn't have previous state, failing silently");
+ return;
+ }
+ let previous_state = previous_state.unwrap();
+ if let Some((version_name, install_dir)) = match previous_state {
+ GameDownloadStatus::Installed {
+ version_name,
+ install_dir,
+ } => Some((version_name, install_dir)),
+ GameDownloadStatus::SetupRequired {
+ version_name,
+ install_dir,
+ } => Some((version_name, install_dir)),
+ _ => None,
+ } {
+ db_handle
+ .applications
+ .transient_statuses
+ .entry(meta.clone())
+ .and_modify(|v| *v = ApplicationTransientStatus::Uninstalling {});
+ drop(db_handle);
+
+ let app_handle = app_handle.clone();
+ spawn(move || match remove_dir_all(install_dir) {
+ Err(e) => {
+ error!("{}", e);
+ }
+ Ok(_) => {
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ db_handle.applications.transient_statuses.remove(&meta);
+ db_handle
+ .applications
+ .game_statuses
+ .entry(meta.id.clone())
+ .and_modify(|e| *e = GameDownloadStatus::Remote {});
+ drop(db_handle);
+ DB.save().unwrap();
+
+ info!("uninstalled game id {}", &meta.id);
+
+ push_game_update(&app_handle, &meta, (Some(GameDownloadStatus::Remote {}), None));
+ }
+ });
+ }
+}
+
+pub fn get_current_meta(game_id: &String) -> Result {
+ match DB.borrow_data().unwrap().applications.installed_game_version.get(game_id) {
+ Some(meta) => Ok(meta.clone()),
+ None => Err(String::from("Could not find installed version")),
+ }
+}
+
#[tauri::command]
pub fn fetch_game_verion_options<'a>(
game_id: String,
@@ -233,19 +325,19 @@ pub fn fetch_game_verion_options<'a>(
}
pub fn on_game_complete(
- game_id: String,
- version_name: String,
+ meta: &DownloadableMetadata,
install_dir: String,
app_handle: &AppHandle,
) -> Result<(), RemoteAccessError> {
// Fetch game version information from remote
let base_url = DB.fetch_base_url();
+ if meta.version.is_none() { return Err(RemoteAccessError::GameNotFound) }
let endpoint = base_url.join(
format!(
"/api/v1/client/metadata/version?id={}&version={}",
- game_id,
- encode(&version_name)
+ meta.id,
+ encode(meta.version.as_ref().unwrap())
)
.as_str(),
)?;
@@ -261,38 +353,43 @@ pub fn on_game_complete(
let mut handle = DB.borrow_data_mut().unwrap();
handle
- .games
- .versions
- .entry(game_id.clone())
+ .applications
+ .game_versions
+ .entry(meta.id.clone())
.or_default()
- .insert(version_name.clone(), data.clone());
+ .insert(meta.version.clone().unwrap(), data.clone());
+ handle
+ .applications
+ .installed_game_version
+ .insert(meta.id.clone(), meta.clone());
+
drop(handle);
DB.save().unwrap();
let status = if data.setup_command.is_empty() {
- GameStatus::Installed {
- version_name,
+ GameDownloadStatus::Installed {
+ version_name: meta.version.clone().unwrap(),
install_dir,
}
} else {
- GameStatus::SetupRequired {
- version_name,
+ GameDownloadStatus::SetupRequired {
+ version_name: meta.version.clone().unwrap(),
install_dir,
}
};
let mut db_handle = DB.borrow_data_mut().unwrap();
db_handle
- .games
- .statuses
- .insert(game_id.clone(), status.clone());
+ .applications
+ .game_statuses
+ .insert(meta.id.clone(), status.clone());
drop(db_handle);
DB.save().unwrap();
app_handle
.emit(
- &format!("update_game/{}", game_id),
+ &format!("update_game/{}", meta.id),
GameUpdateEvent {
- game_id,
+ game_id: meta.id.clone(),
status: (Some(status), None),
},
)
@@ -300,3 +397,15 @@ pub fn on_game_complete(
Ok(())
}
+
+pub fn push_game_update(app_handle: &AppHandle, meta: &DownloadableMetadata, status: GameStatusWithTransient) {
+ app_handle
+ .emit(
+ &format!("update_game/{}", meta.id),
+ GameUpdateEvent {
+ game_id: meta.id.clone(),
+ status,
+ },
+ )
+ .unwrap();
+}
\ No newline at end of file
diff --git a/src-tauri/src/games/mod.rs b/src-tauri/src/games/mod.rs
new file mode 100644
index 0000000..e49a4ef
--- /dev/null
+++ b/src-tauri/src/games/mod.rs
@@ -0,0 +1,3 @@
+pub mod downloads;
+pub mod library;
+pub mod state;
\ No newline at end of file
diff --git a/src-tauri/src/games/state.rs b/src-tauri/src/games/state.rs
new file mode 100644
index 0000000..778da84
--- /dev/null
+++ b/src-tauri/src/games/state.rs
@@ -0,0 +1,32 @@
+use crate::{
+ db::{ApplicationTransientStatus, GameDownloadStatus}, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, fetch_state, 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 online_state = match db_lock.applications.installed_game_version.get(game_id) {
+ Some(meta) => db_lock
+ .applications
+ .transient_statuses
+ .get(meta)
+ .cloned(),
+ None => None,
+ };
+ let offline_state = db_lock.applications.game_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);
+ }
+
+ (None, None)
+ }
+}
\ No newline at end of file
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 6e6dcf5..5bc1538 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,46 +1,58 @@
mod auth;
mod db;
-mod downloads;
-mod library;
+mod games;
+mod autostart;
+mod cleanup;
+mod debug;
mod process;
mod remote;
-mod state;
+mod tools;
+pub mod download_manager;
#[cfg(test)]
mod tests;
-mod cleanup;
+use crate::autostart::{get_autostart_enabled, toggle_autostart};
use crate::db::DatabaseImpls;
-use auth::{auth_initiate, generate_authorization_header, recieve_handshake, retry_connect};
+use auth::{
+ auth_initiate, generate_authorization_header, manual_recieve_handshake, recieve_handshake,
+ retry_connect, sign_out,
+};
use cleanup::{cleanup_and_exit, quit};
use db::{
- add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface,
+ add_download_dir, delete_download_dir, fetch_download_dir_stats, DatabaseInterface, GameDownloadStatus,
DATA_ROOT_DIR,
};
-use downloads::download_commands::*;
-use downloads::download_manager::DownloadManager;
-use downloads::download_manager_builder::DownloadManagerBuilder;
+use debug::fetch_system_data;
+use download_manager::download_manager::DownloadManager;
+use download_manager::download_manager_builder::DownloadManagerBuilder;
+use games::downloads::download_commands::{cancel_game, download_game, move_game_in_queue, pause_game_downloads, resume_game_downloads};
+use http::Response;
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, LevelFilter};
+use games::library::{
+ fetch_game, fetch_game_status, fetch_game_verion_options, fetch_library, uninstall_game, Game
+};
+use log::{debug, info, warn, LevelFilter};
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
-use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::config::{Appender, Root};
use log4rs::encode::pattern::PatternEncoder;
use log4rs::Config;
-use process::process_commands::launch_game;
+use process::compat::CompatibilityManager;
+use process::process_commands::{kill_game, launch_game};
use process::process_manager::ProcessManager;
use remote::{gen_drop_url, use_remote};
use serde::{Deserialize, Serialize};
+use tauri_plugin_dialog::DialogExt;
+use std::path::Path;
use std::sync::Arc;
use std::{
collections::HashMap,
sync::{LazyLock, Mutex},
};
-use tauri::menu::{Menu, MenuItem, MenuItemBuilder, PredefinedMenuItem};
+use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
use tauri::tray::TrayIconBuilder;
-use tauri::{AppHandle, Manager, RunEvent, WindowEvent};
+use tauri::{AppHandle, Emitter, Manager, RunEvent, WindowEvent};
use tauri_plugin_deep_link::DeepLinkExt;
#[derive(Clone, Copy, Serialize)]
@@ -65,7 +77,7 @@ pub struct User {
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
-pub struct AppState {
+pub struct AppState<'a> {
status: AppStatus,
user: Option,
games: HashMap,
@@ -73,18 +85,20 @@ pub struct AppState {
#[serde(skip_serializing)]
download_manager: Arc,
#[serde(skip_serializing)]
- process_manager: Arc>,
+ process_manager: Arc>>,
+ #[serde(skip_serializing)]
+ compat_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();
+ let cloned_state = serde_json::to_string(&guard.clone()).map_err(|e| e.to_string())?;
drop(guard);
Ok(cloned_state)
}
-fn setup(handle: AppHandle) -> AppState {
+fn setup(handle: AppHandle) -> AppState<'static> {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} | {l} | {f} - {m}{n}")))
.append(false)
@@ -110,8 +124,9 @@ fn setup(handle: AppHandle) -> AppState {
log4rs::init_config(config).unwrap();
let games = HashMap::new();
- let download_manager = Arc::new(DownloadManagerBuilder::build(handle));
- let process_manager = Arc::new(Mutex::new(ProcessManager::new()));
+ let download_manager = Arc::new(DownloadManagerBuilder::build(handle.clone()));
+ let process_manager = Arc::new(Mutex::new(ProcessManager::new(handle.clone())));
+ let compat_manager = Arc::new(Mutex::new(CompatibilityManager::new()));
debug!("Checking if database is set up");
let is_set_up = DB.database_is_set_up();
@@ -122,27 +137,80 @@ fn setup(handle: AppHandle) -> AppState {
games,
download_manager,
process_manager,
+ compat_manager,
};
}
debug!("Database is set up");
let (app_status, user) = auth::setup().unwrap();
+
+ let db_handle = DB.borrow_data().unwrap();
+ let mut missing_games = Vec::new();
+ let statuses = db_handle.applications.game_statuses.clone();
+ drop(db_handle);
+ for (game_id, status) in statuses.into_iter() {
+ match status {
+ db::GameDownloadStatus::Remote {} => {}
+ db::GameDownloadStatus::SetupRequired {
+ version_name: _,
+ install_dir,
+ } => {
+ let install_dir_path = Path::new(&install_dir);
+ if !install_dir_path.exists() {
+ missing_games.push(game_id);
+ }
+ }
+ db::GameDownloadStatus::Installed {
+ version_name: _,
+ install_dir,
+ } => {
+ let install_dir_path = Path::new(&install_dir);
+ if !install_dir_path.exists() {
+ missing_games.push(game_id);
+ }
+ }
+ }
+ }
+
+ info!("detected games missing: {:?}", missing_games);
+
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ for game_id in missing_games {
+ db_handle
+ .applications
+ .game_statuses
+ .entry(game_id)
+ .and_modify(|v| *v = GameDownloadStatus::Remote {});
+ }
+
+ drop(db_handle);
+
+
+ info!("finished setup!");
+
+ // Sync autostart state
+ if let Err(e) = autostart::sync_autostart_on_startup(&handle) {
+ warn!("Failed to sync autostart state: {}", e);
+ }
+
AppState {
status: app_status,
user,
games,
download_manager,
process_manager,
+ compat_manager,
}
}
-
pub static DB: LazyLock = LazyLock::new(DatabaseInterface::set_up_database);
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
- let mut builder = tauri::Builder::default().plugin(tauri_plugin_dialog::init());
+ let mut builder = tauri::Builder::default()
+ .plugin(tauri_plugin_os::init())
+ .plugin(tauri_plugin_dialog::init());
#[cfg(desktop)]
#[allow(unused_variables)]
@@ -152,15 +220,18 @@ pub fn run() {
}));
}
- let mut app = builder
+ let app = builder
.plugin(tauri_plugin_deep_link::init())
.invoke_handler(tauri::generate_handler![
// Core utils
fetch_state,
quit,
+ fetch_system_data,
// Auth
auth_initiate,
retry_connect,
+ manual_recieve_handshake,
+ sign_out,
// Remote
use_remote,
gen_drop_url,
@@ -178,11 +249,19 @@ pub fn run() {
pause_game_downloads,
resume_game_downloads,
cancel_game,
+ uninstall_game,
// Processes
launch_game,
+ kill_game,
+ toggle_autostart,
+ get_autostart_enabled,
])
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
+ .plugin(tauri_plugin_autostart::init(
+ tauri_plugin_autostart::MacosLauncher::LaunchAgent,
+ Some(vec!["--minimize"]),
+ ))
.setup(|app| {
let handle = app.handle().clone();
let state = setup(handle);
@@ -253,6 +332,18 @@ pub fn run() {
.build(app)
.expect("error while setting up tray menu");
+ {
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ if let Some(original) = db_handle.prev_database.take() {
+ warn!("Database corrupted. Original file at {}", original.canonicalize().unwrap().to_string_lossy().to_string());
+ app.dialog()
+ .message("Database corrupted. A copy has been saved at: ".to_string() + original.to_str().unwrap())
+ .title("Database corrupted")
+ .show(|_| {});
+ }
+ }
+
+
Ok(())
})
.register_asynchronous_uri_scheme_protocol("object", move |_ctx, request, responder| {
@@ -272,8 +363,16 @@ pub fn run() {
let response = client
.get(object_url.to_string())
.header("Authorization", header)
- .send()
- .unwrap();
+ .send();
+ if response.is_err() {
+ warn!(
+ "failed to fetch object with error: {}",
+ response.err().unwrap()
+ );
+ responder.respond(Response::builder().status(500).body(Vec::new()).unwrap());
+ return;
+ }
+ let response = response.unwrap();
let resp_builder = ResponseBuilder::new().header(
CONTENT_TYPE,
@@ -284,22 +383,20 @@ pub fn run() {
responder.respond(resp);
})
- .on_window_event(|window, event| match event {
- WindowEvent::CloseRequested { api, .. } => {
+ .on_window_event(|window, event| {
+ if let WindowEvent::CloseRequested { api, .. } = event {
window.hide().unwrap();
api.prevent_close();
}
- _ => (),
})
.build(tauri::generate_context!())
.expect("error while running tauri application");
- app.run(|app_handle, event| match event {
- RunEvent::ExitRequested { code, api, .. } => {
+ app.run(|app_handle, event| {
+ if let RunEvent::ExitRequested { code, api, .. } = event {
if code.is_none() {
api.prevent_exit();
}
}
- _ => {}
});
}
diff --git a/src-tauri/src/process/compat.rs b/src-tauri/src/process/compat.rs
new file mode 100644
index 0000000..65e8ece
--- /dev/null
+++ b/src-tauri/src/process/compat.rs
@@ -0,0 +1,51 @@
+use std::{
+ fs::create_dir_all,
+ path::PathBuf,
+ sync::atomic::{AtomicBool, Ordering},
+};
+
+use crate::db::DATA_ROOT_DIR;
+
+pub struct CompatibilityManager {
+ compat_tools_path: PathBuf,
+ prefixes_path: PathBuf,
+ created_paths: AtomicBool,
+}
+
+/*
+This gets built into both the Windows & Linux client, but
+we only need it in the Linux client. Therefore, it should
+do nothing but take a little bit of memory if we're on
+Windows.
+*/
+impl CompatibilityManager {
+ pub fn new() -> Self {
+ let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
+ let compat_tools_path = root_dir_lock.join("compatibility_tools");
+ let prefixes_path = root_dir_lock.join("prefixes");
+ drop(root_dir_lock);
+
+ Self {
+ compat_tools_path,
+ prefixes_path,
+ created_paths: AtomicBool::new(false),
+ }
+ }
+
+ fn ensure_paths_exist(&self) -> Result<(), String> {
+ if self.created_paths.fetch_and(true, Ordering::Relaxed) {
+ return Ok(());
+ }
+ if !self.compat_tools_path.exists() {
+ create_dir_all(self.compat_tools_path.clone()).map_err(|e| e.to_string())?;
+ }
+ if !self.prefixes_path.exists() {
+ create_dir_all(self.prefixes_path.clone()).map_err(|e| e.to_string())?;
+ }
+ self.created_paths.store(true, Ordering::Relaxed);
+
+ Ok(())
+ }
+
+
+}
diff --git a/src-tauri/src/process/mod.rs b/src-tauri/src/process/mod.rs
index a4ceb45..85692c9 100644
--- a/src-tauri/src/process/mod.rs
+++ b/src-tauri/src/process/mod.rs
@@ -1,2 +1,3 @@
+pub mod compat;
+pub mod process_commands;
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
index 327728f..c74ff8b 100644
--- a/src-tauri/src/process/process_commands.rs
+++ b/src-tauri/src/process/process_commands.rs
@@ -1,16 +1,40 @@
use std::sync::Mutex;
-use crate::AppState;
+use crate::{db::GameDownloadStatus, download_manager::downloadable_metadata::{DownloadType, DownloadableMetadata}, games::library::get_current_meta, AppState, DB};
#[tauri::command]
-pub fn launch_game(game_id: String, state: tauri::State<'_, Mutex>) -> Result<(), String> {
+pub fn launch_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)?;
+ let version = match DB.borrow_data().unwrap().applications.game_statuses.get(&id).cloned() {
+ Some(GameDownloadStatus::Installed { version_name, install_dir }) => version_name,
+ Some(GameDownloadStatus::SetupRequired { version_name, install_dir }) => return Err(String::from("Game setup still required")),
+ _ => return Err(String::from("Game not installed"))
+ };
+
+
+ let meta = DownloadableMetadata { id, version: Some(version), download_type: DownloadType::Game };
+
+
+ process_manager_lock.launch_process(meta)?;
drop(process_manager_lock);
drop(state_lock);
Ok(())
}
+
+#[tauri::command]
+pub fn kill_game(
+ game_id: String,
+ state: tauri::State<'_, Mutex>,
+) -> Result<(), String> {
+ let meta = get_current_meta(&game_id)?;
+ let state_lock = state.lock().unwrap();
+ let mut process_manager_lock = state_lock.process_manager.lock().unwrap();
+ process_manager_lock.kill_game(meta).map_err(|x| x.to_string())
+}
\ No newline at end of file
diff --git a/src-tauri/src/process/process_manager.rs b/src-tauri/src/process/process_manager.rs
index 5266849..c376037 100644
--- a/src-tauri/src/process/process_manager.rs
+++ b/src-tauri/src/process/process_manager.rs
@@ -1,28 +1,33 @@
use std::{
collections::HashMap,
fs::{File, OpenOptions},
- io::{Stdout, Write},
+ io,
path::{Path, PathBuf},
- process::{Child, Command},
- sync::LazyLock,
+ process::{Child, Command, ExitStatus},
+ sync::{Arc, Mutex},
+ thread::spawn,
};
-use log::info;
+use log::{info, warn};
use serde::{Deserialize, Serialize};
+use shared_child::SharedChild;
+use tauri::{AppHandle, Manager};
+use umu_wrapper_lib::command_builder::UmuCommandBuilder;
use crate::{
- db::{GameStatus, DATA_ROOT_DIR},
- DB,
+ db::{GameDownloadStatus, ApplicationTransientStatus, DATA_ROOT_DIR}, download_manager::{downloadable::Downloadable, downloadable_metadata::DownloadableMetadata}, games::library::push_game_update, games::state::GameStatusManager, AppState, DB
};
-pub struct ProcessManager {
+pub struct ProcessManager<'a> {
current_platform: Platform,
log_output_dir: PathBuf,
- processes: HashMap,
+ processes: HashMap>,
+ app_handle: AppHandle,
+ game_launchers: HashMap<(Platform, Platform), &'a (dyn ProcessHandler + Sync + Send + 'static)>,
}
-impl ProcessManager {
- pub fn new() -> Self {
+impl ProcessManager<'_> {
+ pub fn new(app_handle: AppHandle) -> Self {
let root_dir_lock = DATA_ROOT_DIR.lock().unwrap();
let log_output_dir = root_dir_lock.join("logs");
drop(root_dir_lock);
@@ -34,119 +39,311 @@ impl ProcessManager {
Platform::Linux
},
+ app_handle,
processes: HashMap::new(),
log_output_dir,
+ game_launchers: HashMap::from([
+ // Current platform to target platform
+ (
+ (Platform::Windows, Platform::Windows),
+ &NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
+ ),
+ (
+ (Platform::Linux, Platform::Linux),
+ &NativeGameLauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
+ ),
+ (
+ (Platform::Linux, Platform::Windows),
+ &UMULauncher {} as &(dyn ProcessHandler + Sync + Send + 'static),
+ ),
+ ]),
}
}
- fn process_command(&self, install_dir: &String, raw_command: String) -> (String, Vec) {
- let command_components = raw_command.split(" ").collect::>();
- let root = command_components[0].to_string();
+ // There's no easy way to distinguish between an executable name with
+ // spaces and it's arguments.
+ // I think if we just join the install_dir to whatever the user provides us, we'll be alright
+ // In future, we should have a separate field for executable name and it's arguments
+ fn process_command(&self, install_dir: &String, raw_command: String) -> (PathBuf, Vec) {
+ // let command_components = raw_command.split(" ").collect::>();
+ let root = raw_command;
let install_dir = Path::new(install_dir);
let absolute_exe = install_dir.join(root);
+ /*
let args = command_components[1..]
- .into_iter()
+ .iter()
.map(|v| v.to_string())
.collect();
- (absolute_exe.to_str().unwrap().to_owned(), args)
+ */
+ (absolute_exe, Vec::new())
+ }
+ pub fn kill_game(&mut self, meta: DownloadableMetadata) -> Result<(), io::Error> {
+ return match self.processes.get(&meta) {
+ Some(child) => {
+ child.kill()?;
+ child.wait()?;
+ Ok(())
+ },
+ None => Err(io::Error::new(
+ io::ErrorKind::NotFound,
+ "Game ID not running",
+ )),
+ };
+ }
+
+ fn on_process_finish(&mut self, meta: DownloadableMetadata, result: Result) {
+ if !self.processes.contains_key(&meta) {
+ warn!("process on_finish was called, but game_id is no longer valid. finished with result: {:?}", result);
+ return;
+ }
+
+ info!("process for {:?} exited with {:?}", meta, result);
+
+ self.processes.remove(&meta);
+
+ let mut db_handle = DB.borrow_data_mut().unwrap();
+ db_handle.applications.transient_statuses.remove(&meta);
+
+ let current_state = db_handle.applications.game_statuses.get(&meta.id).cloned();
+ if let Some(saved_state) = current_state {
+ if let GameDownloadStatus::SetupRequired {
+ version_name,
+ install_dir,
+ } = saved_state
+ {
+ if let Ok(exit_code) = result {
+ if exit_code.success() {
+ db_handle.applications.game_statuses.insert(
+ meta.id.clone(),
+ GameDownloadStatus::Installed {
+ version_name: version_name.to_string(),
+ install_dir: install_dir.to_string(),
+ },
+ );
+ }
+ }
+ }
+ }
+ drop(db_handle);
+
+ let status = GameStatusManager::fetch_state(&meta.id);
+
+ push_game_update(&self.app_handle, &meta, status);
+
+ // TODO better management
}
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))
+ Ok(self
+ .game_launchers
+ .contains_key(&(current.clone(), platform.clone())))
}
- pub fn launch_game(&mut self, game_id: String) -> Result<(), String> {
- if self.processes.contains_key(&game_id) {
+ pub fn launch_process(&mut self, meta: DownloadableMetadata) -> Result<(), String> {
+ if self.processes.contains_key(&meta) {
return Err("Game or setup is already running.".to_owned());
}
- let db_lock = DB.borrow_data().unwrap();
+ let mut db_lock = DB.borrow_data_mut().unwrap();
+ info!("Launching process {:?} with games {:?}", meta, db_lock.applications.game_versions);
+
let game_status = db_lock
- .games
- .statuses
- .get(&game_id)
+ .applications
+ .game_statuses
+ .get(&meta.id)
.ok_or("Game not installed")?;
- let GameStatus::Installed {
- version_name,
- install_dir,
- } = game_status
- else {
- return Err("Game not installed.".to_owned());
+ let status_metadata: Option<(&String, &String)> = match game_status {
+ GameDownloadStatus::Installed {
+ version_name,
+ install_dir,
+ } => Some((version_name, install_dir)),
+ GameDownloadStatus::SetupRequired {
+ version_name,
+ install_dir,
+ } => Some((version_name, install_dir)),
+ _ => None,
};
+ if status_metadata.is_none() {
+ return Err("Game has not been downloaded.".to_owned());
+ }
+
+ let (version_name, install_dir) = status_metadata.unwrap();
+
let game_version = db_lock
- .games
- .versions
- .get(&game_id)
+ .applications
+ .game_versions
+ .get(&meta.id)
.ok_or("Invalid game ID".to_owned())?
.get(version_name)
.ok_or("Invalid version name".to_owned())?;
- let (command, args) =
- self.process_command(install_dir, game_version.launch_command.clone());
+ let raw_command: String = match game_status {
+ GameDownloadStatus::Installed {
+ version_name: _,
+ install_dir: _,
+ } => game_version.launch_command.clone(),
+ GameDownloadStatus::SetupRequired {
+ version_name: _,
+ install_dir: _,
+ } => game_version.setup_command.clone(),
+ _ => panic!("unreachable code"),
+ };
- info!("launching process {} in {}", command, install_dir);
+ let (command, args) = self.process_command(install_dir, raw_command);
+
+ let target_current_dir = command.parent().unwrap().to_str().unwrap();
+
+ info!(
+ "launching process {} in {}",
+ command.to_str().unwrap(),
+ target_current_dir
+ );
let current_time = chrono::offset::Local::now();
- let mut log_file = OpenOptions::new()
+ let log_file = OpenOptions::new()
.write(true)
.truncate(true)
.read(true)
.create(true)
.open(
self.log_output_dir
- .join(format!("{}-{}.log", game_id, current_time.timestamp())),
+ .join(format!("{}-{}-{}.log", meta.id.clone(), meta.version.clone().unwrap_or_default(), current_time.timestamp())),
)
.map_err(|v| v.to_string())?;
- let mut error_file = OpenOptions::new()
+ let error_file = OpenOptions::new()
.write(true)
.truncate(true)
.read(true)
.create(true)
- .open(
- self.log_output_dir
- .join(format!("{}-{}-error.log", game_id, current_time.timestamp())),
- )
+ .open(self.log_output_dir.join(format!(
+ "{}-{}-{}-error.log",
+ meta.id.clone(),
+ meta.version.clone().unwrap_or_default(),
+ current_time.timestamp()
+ )))
.map_err(|v| v.to_string())?;
- info!("opened log file for {}", command);
+ let current_platform = self.current_platform.clone();
+ let target_platform = game_version.platform.clone();
- let launch_process = Command::new(command)
- .current_dir(install_dir)
- .stdout(log_file)
- .stderr(error_file)
- .args(args)
- .spawn()
- .map_err(|v| v.to_string())?;
+ let game_launcher = self
+ .game_launchers
+ .get(&(current_platform, target_platform))
+ .ok_or("Invalid version for this platform.")
+ .map_err(|e| e.to_string())?;
- self.processes.insert(game_id, launch_process);
+ let launch_process = game_launcher.launch_process(
+ &meta,
+ command.to_str().unwrap().to_owned(),
+ args,
+ &target_current_dir.to_string(),
+ log_file,
+ error_file,
+ )?;
+
+ let launch_process_handle =
+ Arc::new(SharedChild::new(launch_process).map_err(|e| e.to_string())?);
+
+ db_lock
+ .applications
+ .transient_statuses
+ .insert(meta.clone(), ApplicationTransientStatus::Running {});
+
+ push_game_update(
+ &self.app_handle,
+ &meta,
+ (None, Some(ApplicationTransientStatus::Running {})),
+ );
+
+ let wait_thread_handle = launch_process_handle.clone();
+ let wait_thread_apphandle = self.app_handle.clone();
+ let wait_thread_game_id = meta.clone();
+
+ spawn(move || {
+ let result: Result = launch_process_handle.wait();
+
+ let app_state = wait_thread_apphandle.state::>();
+ let app_state_handle = app_state.lock().unwrap();
+
+ let mut process_manager_handle = app_state_handle.process_manager.lock().unwrap();
+ process_manager_handle.on_process_finish(wait_thread_game_id, result);
+
+ // As everything goes out of scope, they should get dropped
+ // But just to explicit about it
+ drop(process_manager_handle);
+ drop(app_state_handle);
+ });
+
+ self.processes.insert(meta, wait_thread_handle);
+
+ info!("finished spawning process");
Ok(())
}
}
-#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone)]
+#[derive(Eq, Hash, PartialEq, Serialize, Deserialize, Clone, Debug)]
pub enum Platform {
Windows,
Linux,
}
-pub type ProcessCompatabilityMatrix = HashMap>;
-pub static PROCESS_COMPATABILITY_MATRIX: LazyLock =
- LazyLock::new(|| {
- let mut matrix: ProcessCompatabilityMatrix = HashMap::new();
+pub trait ProcessHandler: Send + 'static {
+ fn launch_process(
+ &self,
+ meta: &DownloadableMetadata,
+ command: String,
+ args: Vec,
+ current_dir: &String,
+ log_file: File,
+ error_file: File,
+ ) -> Result;
+}
- matrix.insert(Platform::Windows, vec![Platform::Windows]);
- matrix.insert(Platform::Linux, vec![Platform::Linux]); // TODO: add Proton support
+struct NativeGameLauncher;
+impl ProcessHandler for NativeGameLauncher {
+ fn launch_process(
+ &self,
+ meta: &DownloadableMetadata,
+ command: String,
+ args: Vec,
+ current_dir: &String,
+ log_file: File,
+ error_file: File,
+ ) -> Result {
+ Command::new(command)
+ .current_dir(current_dir)
+ .stdout(log_file)
+ .stderr(error_file)
+ .args(args)
+ .spawn()
+ .map_err(|v| v.to_string())
+ }
+}
- return matrix;
- });
+const UMU_LAUNCHER_EXECUTABLE: &str = "umu-run";
+struct UMULauncher;
+impl ProcessHandler for UMULauncher {
+ fn launch_process(
+ &self,
+ meta: &DownloadableMetadata,
+ command: String,
+ args: Vec,
+ current_dir: &String,
+ log_file: File,
+ error_file: File,
+ ) -> Result {
+ UmuCommandBuilder::new(UMU_LAUNCHER_EXECUTABLE, command)
+ .game_id(String::from("0"))
+ .launch_args(args)
+ .build()
+ .spawn()
+ .map_err(|x| x.to_string())
+ }
+}
diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs
index 33655c2..04c56db 100644
--- a/src-tauri/src/remote.rs
+++ b/src-tauri/src/remote.rs
@@ -1,10 +1,12 @@
use std::{
+ error::Error,
fmt::{Display, Formatter},
sync::{Arc, Mutex},
};
use http::StatusCode;
use log::{info, warn};
+use reqwest::blocking::Response;
use serde::Deserialize;
use url::{ParseError, Url};
@@ -16,31 +18,44 @@ pub enum RemoteAccessError {
ParsingError(ParseError),
InvalidCodeError(u16),
InvalidEndpoint,
- HandshakeFailed,
+ HandshakeFailed(String),
GameNotFound,
- InvalidResponse,
+ InvalidResponse(DropServerError),
InvalidRedirect,
ManifestDownloadFailed(StatusCode, String),
+ OutOfSync,
+ Generic(String),
}
impl Display for RemoteAccessError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
- RemoteAccessError::FetchError(error) => write!(f, "{}", error),
+ RemoteAccessError::FetchError(error) => write!(
+ f,
+ "{}: {}",
+ error,
+ error
+ .source()
+ .map(|e| e.to_string())
+ .or_else(|| Some("Unknown error".to_string()))
+ .unwrap()
+ ),
RemoteAccessError::ParsingError(parse_error) => {
write!(f, "{}", parse_error)
}
RemoteAccessError::InvalidCodeError(error) => write!(f, "Invalid HTTP code {}", error),
RemoteAccessError::InvalidEndpoint => write!(f, "Invalid drop endpoint"),
- RemoteAccessError::HandshakeFailed => write!(f, "Failed to complete handshake"),
+ RemoteAccessError::HandshakeFailed(message) => write!(f, "Failed to complete handshake: {}", message),
RemoteAccessError::GameNotFound => write!(f, "Could not find game on server"),
- RemoteAccessError::InvalidResponse => write!(f, "Server returned an invalid response"),
+ RemoteAccessError::InvalidResponse(error) => write!(f, "Server returned an invalid response: {} {}", error.status_code, error.status_message),
RemoteAccessError::InvalidRedirect => write!(f, "Server redirect was invalid"),
RemoteAccessError::ManifestDownloadFailed(status, response) => write!(
f,
"Failed to download game manifest: {} {}",
status, response
),
+ RemoteAccessError::OutOfSync => write!(f, "Server's and client's time are out of sync. Please ensure they are within at least 30 seconds of each other."),
+ RemoteAccessError::Generic(message) => write!(f, "{}", message),
}
}
}
@@ -63,6 +78,15 @@ impl From for RemoteAccessError {
impl std::error::Error for RemoteAccessError {}
+#[derive(Deserialize, Debug, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct DropServerError {
+ pub status_code: usize,
+ pub status_message: String,
+ pub message: String,
+ pub url: String,
+}
+
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DropHealthcheck {
@@ -71,7 +95,7 @@ struct DropHealthcheck {
async fn use_remote_logic<'a>(
url: String,
- state: tauri::State<'_, Mutex>,
+ state: tauri::State<'_, Mutex>>,
) -> Result<(), RemoteAccessError> {
info!("connecting to url {}", url);
let base_url = Url::parse(&url)?;
@@ -103,7 +127,7 @@ async fn use_remote_logic<'a>(
#[tauri::command]
pub async fn use_remote<'a>(
url: String,
- state: tauri::State<'_, Mutex>,
+ state: tauri::State<'_, Mutex>>,
) -> Result<(), String> {
let result = use_remote_logic(url, state).await;
diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs
deleted file mode 100644
index 9f66307..0000000
--- a/src-tauri/src/state.rs
+++ /dev/null
@@ -1,31 +0,0 @@
-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);
- }
-}
diff --git a/src-tauri/src/tools/compatibility_layer.rs b/src-tauri/src/tools/compatibility_layer.rs
new file mode 100644
index 0000000..3644319
--- /dev/null
+++ b/src-tauri/src/tools/compatibility_layer.rs
@@ -0,0 +1,3 @@
+pub struct CompatibilityLayer {
+
+}
\ No newline at end of file
diff --git a/src-tauri/src/tools/mod.rs b/src-tauri/src/tools/mod.rs
new file mode 100644
index 0000000..0279e82
--- /dev/null
+++ b/src-tauri/src/tools/mod.rs
@@ -0,0 +1,4 @@
+mod prefix;
+mod registry;
+mod tool;
+mod compatibility_layer;
\ No newline at end of file
diff --git a/src-tauri/src/tools/prefix.rs b/src-tauri/src/tools/prefix.rs
new file mode 100644
index 0000000..e69de29
diff --git a/src-tauri/src/tools/registry.rs b/src-tauri/src/tools/registry.rs
new file mode 100644
index 0000000..fa50426
--- /dev/null
+++ b/src-tauri/src/tools/registry.rs
@@ -0,0 +1,7 @@
+use std::collections::HashMap;
+
+use crate::download_manager::downloadable::Downloadable;
+
+pub struct Registry {
+ tools: HashMap
+}
diff --git a/src-tauri/src/tools/tool.rs b/src-tauri/src/tools/tool.rs
new file mode 100644
index 0000000..43fc9f0
--- /dev/null
+++ b/src-tauri/src/tools/tool.rs
@@ -0,0 +1,54 @@
+use std::sync::Arc;
+
+use tauri::AppHandle;
+
+use crate::download_manager::{application_download_error::ApplicationDownloadError, download_thread_control_flag::DownloadThreadControl, downloadable::Downloadable, downloadable_metadata::DownloadableMetadata, progress_object::ProgressObject};
+
+pub struct ToolDownloadAgent {
+ id: String,
+ version: String,
+ location: String,
+ control_flag: DownloadThreadControl,
+ progress: Arc,
+}
+impl Downloadable for ToolDownloadAgent {
+ fn download(&self, app_handle: &AppHandle) -> Result {
+ todo!()
+ }
+
+ fn progress(&self) -> Arc {
+ todo!()
+ }
+
+ fn control_flag(&self) -> DownloadThreadControl {
+ todo!()
+ }
+
+ fn status(&self) -> crate::download_manager::download_manager::DownloadStatus {
+ todo!()
+ }
+
+ fn metadata(&self) -> DownloadableMetadata {
+ todo!()
+ }
+
+ fn on_initialised(&self, app_handle: &tauri::AppHandle) {
+ todo!()
+ }
+
+ fn on_error(&self, app_handle: &tauri::AppHandle, error: crate::download_manager::application_download_error::ApplicationDownloadError) {
+ todo!()
+ }
+
+ fn on_complete(&self, app_handle: &tauri::AppHandle) {
+ todo!()
+ }
+
+ fn on_incomplete(&self, app_handle: &tauri::AppHandle) {
+ todo!()
+ }
+
+ fn on_cancelled(&self, app_handle: &tauri::AppHandle) {
+ todo!()
+ }
+}
\ No newline at end of file
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 8fecb09..0efb8e0 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -40,6 +40,7 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
- ]
+ ],
+ "externalBin": []
}
}
diff --git a/types.ts b/types.ts
index 5702594..720ec28 100644
--- a/types.ts
+++ b/types.ts
@@ -52,9 +52,23 @@ export enum GameStatusEnum {
Updating = "Updating",
Uninstalling = "Uninstalling",
SetupRequired = "SetupRequired",
+ Running = "Running"
}
export type GameStatus = {
type: GameStatusEnum;
version_name?: string;
};
+
+export enum DownloadableType {
+ Game = "Game",
+ Tool = "Tool",
+ DLC = "DLC",
+ Mod = "Mod"
+}
+
+export type DownloadableMetadata = {
+ id: string,
+ version: string,
+ downloadType: DownloadableType
+}
\ No newline at end of file
diff --git a/utils/generateGameMeta.ts b/utils/generateGameMeta.ts
new file mode 100644
index 0000000..fdfc4b7
--- /dev/null
+++ b/utils/generateGameMeta.ts
@@ -0,0 +1,9 @@
+import { type DownloadableMetadata, DownloadableType } from '~/types'
+
+export default function generateGameMeta(gameId: string, version: string): DownloadableMetadata {
+ return {
+ id: gameId,
+ version,
+ downloadType: DownloadableType.Game
+ }
+}
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 669d164..2c770d1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1392,6 +1392,13 @@
dependencies:
"@tauri-apps/api" "^2.0.0"
+"@tauri-apps/plugin-os@~2":
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-os/-/plugin-os-2.2.0.tgz#ef5511269f59c0ccc580a9d09600034cfaa9743b"
+ integrity sha512-HszbCdbisMlu5QhCNAN8YIWyz2v33abAWha6+uvV2CKX8P5VSct/y+kEe22JeyqrxCnWlQ3DRx7s49Byg7/0EA==
+ dependencies:
+ "@tauri-apps/api" "^2.0.0"
+
"@tauri-apps/plugin-shell@>=2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz#b6fc88ab070fd5f620e46405715779aa44eb8428"
@@ -3928,11 +3935,6 @@ mlly@^1.3.0, mlly@^1.4.2, mlly@^1.6.1, mlly@^1.7.1:
pkg-types "^1.2.0"
ufo "^1.5.4"
-moment@^2.30.1:
- version "2.30.1"
- resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae"
- integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==
-
mri@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"