From 65c1942037fc04effdb96fd8c32be8e40d6b28e9 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Mon, 8 May 2023 12:14:08 -0700 Subject: [PATCH] Misc improvements and fixes (#109) * now utilizing tracing better * better tracing * fix mac vs pc oppositional env var issue * modified loading package * added droppable loadingbarid that sends completion message * loading bar * regressed bug on mac * fixed non-updated loading bar on playground * Loading bar improvements --------- Co-authored-by: Jai A --- Cargo.lock | 154 ++++++++++++++++++---- theseus/Cargo.toml | 15 ++- theseus/src/api/auth.rs | 7 +- theseus/src/api/jre.rs | 1 + theseus/src/api/pack.rs | 64 ++++++--- theseus/src/api/profile.rs | 9 +- theseus/src/api/profile_create.rs | 7 +- theseus/src/api/settings.rs | 16 ++- theseus/src/event/emit.rs | 112 ++++++++-------- theseus/src/event/mod.rs | 80 ++++++++++- theseus/src/launcher/auth.rs | 10 +- theseus/src/launcher/download.rs | 114 ++++++++++------ theseus/src/launcher/mod.rs | 53 ++++---- theseus/src/state/children.rs | 5 +- theseus/src/state/metadata.rs | 9 +- theseus/src/state/mod.rs | 58 ++++++-- theseus/src/state/profiles.rs | 26 ++-- theseus/src/state/projects.rs | 18 +-- theseus/src/state/settings.rs | 4 +- theseus/src/state/tags.rs | 18 +-- theseus/src/state/users.rs | 5 +- theseus/src/util/fetch.rs | 67 +++++++--- theseus_cli/Cargo.toml | 2 +- theseus_gui/src-tauri/Cargo.toml | 6 +- theseus_gui/src-tauri/src/api/mod.rs | 13 +- theseus_gui/src-tauri/src/api/pack.rs | 8 +- theseus_gui/src-tauri/src/api/settings.rs | 3 + theseus_gui/src-tauri/src/error.rs | 18 +++ theseus_gui/src-tauri/src/main.rs | 31 +++++ theseus_gui/src/helpers/events.js | 19 +-- theseus_gui/src/helpers/pack.js | 4 +- theseus_playground/Cargo.toml | 6 +- theseus_playground/src/main.rs | 58 +++++--- 33 files changed, 726 insertions(+), 294 deletions(-) create mode 100644 theseus_gui/src-tauri/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 519375f7e..52afa33c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,6 +50,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anyhow" version = "1.0.70" @@ -468,7 +477,7 @@ dependencies = [ "indenter", "once_cell", "owo-colors", - "tracing-error", + "tracing-error 0.2.0", ] [[package]] @@ -480,7 +489,7 @@ dependencies = [ "once_cell", "owo-colors", "tracing-core", - "tracing-error", + "tracing-error 0.2.0", ] [[package]] @@ -1001,9 +1010,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -1011,9 +1020,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" @@ -1028,38 +1037,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.11", ] [[package]] name = "futures-sink" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -1608,6 +1617,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", +] + [[package]] name = "infer" version = "0.7.0" @@ -1820,7 +1841,7 @@ dependencies = [ "serde", "serde_json", "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.16", ] [[package]] @@ -1863,6 +1884,15 @@ dependencies = [ "tendril", ] +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchers" version = "0.1.0" @@ -2077,6 +2107,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "objc" version = "0.2.7" @@ -2458,6 +2494,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2699,10 +2741,12 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg 0.10.1", ] @@ -3516,8 +3560,8 @@ dependencies = [ "dirs", "dunce", "futures", + "indicatif", "lazy_static", - "log", "paste", "regex", "reqwest", @@ -3532,7 +3576,8 @@ dependencies = [ "tokio-stream", "toml 0.7.3", "tracing", - "tracing-error", + "tracing-error 0.1.2", + "tracing-subscriber 0.2.25", "url", "uuid 1.3.0", "winreg 0.11.0", @@ -3557,9 +3602,9 @@ dependencies = [ "tokio", "tokio-stream", "tracing", - "tracing-error", + "tracing-error 0.2.0", "tracing-futures", - "tracing-subscriber", + "tracing-subscriber 0.3.16", "url", "uuid 1.3.0", "webbrowser", @@ -3582,6 +3627,9 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tracing", + "tracing-error 0.1.2", + "tracing-subscriber 0.2.25", "url", "uuid 1.3.0", ] @@ -3600,6 +3648,9 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", + "tracing", + "tracing-error 0.1.2", + "tracing-subscriber 0.2.25", "url", "uuid 1.3.0", "webbrowser", @@ -3842,6 +3893,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d7c0b83d4a500748fa5879461652b361edf5c9d51ede2a2ac03875ca185e24" +dependencies = [ + "tracing", + "tracing-subscriber 0.2.25", +] + [[package]] name = "tracing-error" version = "0.2.0" @@ -3849,7 +3910,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" dependencies = [ "tracing", - "tracing-subscriber", + "tracing-subscriber 0.3.16", ] [[package]] @@ -3873,13 +3934,45 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers 0.0.1", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + [[package]] name = "tracing-subscriber" version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" dependencies = [ - "matchers", + "matchers 0.1.0", "nu-ansi-term", "once_cell", "regex", @@ -4139,6 +4232,19 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +[[package]] +name = "wasm-streams" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" version = "0.3.61" diff --git a/theseus/Cargo.toml b/theseus/Cargo.toml index aa4a757ea..c2cf42c2a 100644 --- a/theseus/Cargo.toml +++ b/theseus/Cargo.toml @@ -22,19 +22,21 @@ chrono = { version = "0.4.19", features = ["serde"] } daedalus = { version = "0.1.20" } dirs = "4.0" -log = "0.4.14" regex = "1.5" sys-info = "0.9.0" thiserror = "1.0" -tracing = "0.1" -tracing-error = "0.2" +tracing = "0.1.37" +tracing-subscriber = "0.2" +tracing-error = "0.1" + +paste = { version = "1.0"} tauri = { version = "1.2", optional = true} -paste = { version = "1.0", optional = true} +indicatif = { version = "0.17.3", optional = true } async-tungstenite = { version = "0.20.0", features = ["tokio-runtime", "tokio-native-tls"] } futures = "0.3" -reqwest = { version = "0.11", features = ["json"] } +reqwest = { version = "0.11", features = ["json", "stream"] } tokio = { version = "1", features = ["full"] } tokio-stream = { version = "0.1", features = ["fs"] } @@ -45,4 +47,5 @@ dunce = "1.0.3" winreg = "0.11.0" [features] -tauri = ["dep:tauri", "dep:paste"] \ No newline at end of file +tauri = ["dep:tauri"] +cli = ["dep:indicatif"] \ No newline at end of file diff --git a/theseus/src/api/auth.rs b/theseus/src/api/auth.rs index fa38c7922..01d338138 100644 --- a/theseus/src/api/auth.rs +++ b/theseus/src/api/auth.rs @@ -46,7 +46,7 @@ pub async fn authenticate( )) })?; - let credentials = flow.extract_credentials(&state.io_semaphore).await?; + let credentials = flow.extract_credentials(&state.fetch_semaphore).await?; users.insert(&credentials).await?; if state.settings.read().await.default_user.is_none() { @@ -64,7 +64,7 @@ pub async fn refresh(user: uuid::Uuid) -> crate::Result { let state = State::get().await?; let mut users = state.users.write().await; - let io_sempahore = &state.io_semaphore; + let fetch_semaphore = &state.fetch_semaphore; futures::future::ready(users.get(user).ok_or_else(|| { crate::ErrorKind::OtherError(format!( "Tried to refresh nonexistent user with ID {user}" @@ -73,7 +73,8 @@ pub async fn refresh(user: uuid::Uuid) -> crate::Result { })) .and_then(|mut credentials| async move { if chrono::offset::Utc::now() > credentials.expires { - inner::refresh_credentials(&mut credentials, io_sempahore).await?; + inner::refresh_credentials(&mut credentials, fetch_semaphore) + .await?; } users.insert(&credentials).await?; Ok(credentials) diff --git a/theseus/src/api/jre.rs b/theseus/src/api/jre.rs index 90dea9851..45844a84d 100644 --- a/theseus/src/api/jre.rs +++ b/theseus/src/api/jre.rs @@ -61,6 +61,7 @@ pub async fn get_optimal_jre_key(profile: &Profile) -> crate::Result { version, profile.metadata.loader_version.as_ref(), None, + None, ) .await?; let optimal_key = match version_info diff --git a/theseus/src/api/pack.rs b/theseus/src/api/pack.rs index 7be9f1e1a..73fda0538 100644 --- a/theseus/src/api/pack.rs +++ b/theseus/src/api/pack.rs @@ -1,12 +1,13 @@ use crate::config::MODRINTH_API_URL; use crate::data::ModLoader; use crate::event::emit::{ - emit_loading, init_loading, loading_try_for_each_concurrent, + emit_loading, init_loading, init_or_edit_loading, + loading_try_for_each_concurrent, }; -use crate::event::LoadingBarType; +use crate::event::{LoadingBarId, LoadingBarType}; use crate::state::{LinkedData, ModrinthProject, ModrinthVersion, SideType}; use crate::util::fetch::{ - fetch, fetch_json, fetch_mirrors, write, write_cached_icon, + fetch, fetch_advanced, fetch_json, fetch_mirrors, write, write_cached_icon, }; use crate::State; use async_zip::tokio::read::seek::ZipFileReader; @@ -75,17 +76,30 @@ enum PackDependency { pub async fn install_pack_from_version_id( version_id: String, + title: Option, ) -> crate::Result { let state = State::get().await?; + let loading_bar = init_loading( + LoadingBarType::PackFileDownload { + pack_name: title, + pack_version: version_id.clone(), + }, + 100.0, + "Downloading pack file", + ) + .await?; + + emit_loading(&loading_bar, 0.0, Some("Fetching version")).await?; let version: ModrinthVersion = fetch_json( Method::GET, &format!("{}version/{}", MODRINTH_API_URL, version_id), None, None, - &state.io_semaphore, + &state.fetch_semaphore, ) .await?; + emit_loading(&loading_bar, 10.0, None).await?; let (url, hash) = if let Some(file) = version.files.iter().find(|x| x.primary) { @@ -102,20 +116,31 @@ pub async fn install_pack_from_version_id( ) })?; - let file = fetch(&url, hash.map(|x| &**x), &state.io_semaphore).await?; + let file = fetch_advanced( + Method::GET, + &url, + hash.map(|x| &**x), + None, + None, + Some((&loading_bar, 70.0)), + &state.fetch_semaphore, + ) + .await?; + emit_loading(&loading_bar, 0.0, Some("Fetching project metadata")).await?; let project: ModrinthProject = fetch_json( Method::GET, &format!("{}project/{}", MODRINTH_API_URL, version.project_id), None, None, - &state.io_semaphore, + &state.fetch_semaphore, ) .await?; + emit_loading(&loading_bar, 10.0, Some("Retrieving icon")).await?; let icon = if let Some(icon_url) = project.icon_url { let state = State::get().await?; - let icon_bytes = fetch(&icon_url, None, &state.io_semaphore).await?; + let icon_bytes = fetch(&icon_url, None, &state.fetch_semaphore).await?; let filename = icon_url.rsplit('/').next(); @@ -135,6 +160,7 @@ pub async fn install_pack_from_version_id( } else { None }; + emit_loading(&loading_bar, 10.0, None).await?; install_pack( file, @@ -142,6 +168,7 @@ pub async fn install_pack_from_version_id( Some(project.title), Some(version.project_id), Some(version.id), + Some(loading_bar), ) .await } @@ -149,7 +176,7 @@ pub async fn install_pack_from_version_id( pub async fn install_pack_from_file(path: PathBuf) -> crate::Result { let file = fs::read(path).await?; - install_pack(bytes::Bytes::from(file), None, None, None, None).await + install_pack(bytes::Bytes::from(file), None, None, None, None, None).await } async fn install_pack( @@ -158,6 +185,7 @@ async fn install_pack( override_title: Option, project_id: Option, version_id: Option, + existing_loading_bar: Option, ) -> crate::Result { let state = &State::get().await?; @@ -242,14 +270,15 @@ async fn install_pack( .await?; let profile = profile_raw.clone(); let result = async { - let loading_bar = init_loading( + let loading_bar = init_or_edit_loading( + existing_loading_bar, LoadingBarType::PackDownload { pack_name: pack.name.clone(), pack_id: project_id, pack_version: version_id, }, 100.0, - "Downloading modpack...", + "Downloading modpack", ) .await?; @@ -260,7 +289,7 @@ async fn install_pack( .map(Ok::), None, Some(&loading_bar), - 80.0, + 70.0, num_files, None, |project| { @@ -287,7 +316,7 @@ async fn install_pack( .hashes .get(&PackFileHash::Sha1) .map(|x| &**x), - &state.io_semaphore, + &state.fetch_semaphore, ) .await?; @@ -365,11 +394,11 @@ async fn install_pack( .await }; - emit_loading(&loading_bar, 0.05, Some("Extracting overrides")) + emit_loading(&loading_bar, 0.0, Some("Extracting overrides")) .await?; extract_overrides("overrides".to_string()).await?; extract_overrides("client_overrides".to_string()).await?; - emit_loading(&loading_bar, 0.1, Some("Done extacting overrides")) + emit_loading(&loading_bar, 29.9, Some("Done extacting overrides")) .await?; if let Some(profile) = crate::api::profile::get(&profile).await? { @@ -380,13 +409,6 @@ async fn install_pack( Some(loading_bar) ), )?; - } else { - emit_loading( - &loading_bar, - 0.1, - Some("Done extacting overrides"), - ) - .await?; } Ok::(profile) diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile.rs index 8ac3a0e44..0162d6279 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile.rs @@ -148,7 +148,7 @@ pub async fn update_all(profile_path: &Path) -> crate::Result<()> { profile_name: profile.metadata.name.clone(), }, 100.0, - "Updating profile...", + "Updating profile", ) .await?; @@ -159,7 +159,7 @@ pub async fn update_all(profile_path: &Path) -> crate::Result<()> { None, Some(&loading_bar), 100.0, - profile.projects.len(), + profile.projects.keys().len(), None, |project| update_project(profile_path, project, Some(true)), ) @@ -344,7 +344,7 @@ pub async fn remove_project( } /// Run Minecraft using a profile and the default credentials, logged in credentials, /// failing with an error if no credentials are available -#[tracing::instrument(skip_all)] +#[tracing::instrument] pub async fn run(path: &Path) -> crate::Result>> { let state = State::get().await?; @@ -367,7 +367,7 @@ pub async fn run(path: &Path) -> crate::Result>> { /// Run Minecraft using a profile, and credentials for authentication /// Returns Arc pointer to RwLock to Child -#[tracing::instrument(skip_all)] +#[tracing::instrument] pub async fn run_credentials( path: &Path, credentials: &auth::Credentials, @@ -398,6 +398,7 @@ pub async fn run_credentials( version, profile.metadata.loader_version.as_ref(), None, + None, ) .await?; let pre_launch_hooks = diff --git a/theseus/src/api/profile_create.rs b/theseus/src/api/profile_create.rs index fa10761d5..c929962a3 100644 --- a/theseus/src/api/profile_create.rs +++ b/theseus/src/api/profile_create.rs @@ -1,4 +1,5 @@ //! Theseus profile management interface +use crate::event::emit::emit_warning; use crate::state::LinkedData; use crate::{ event::{emit::emit_profile, ProfilePayloadType}, @@ -15,6 +16,7 @@ use futures::prelude::*; use std::path::PathBuf; use tokio::fs; use tokio_stream::wrappers::ReadDirStream; +use tracing::{info, trace}; use uuid::Uuid; const DEFAULT_NAME: &str = "Untitled Instance"; @@ -47,6 +49,7 @@ pub async fn profile_create( linked_data: Option, // the linked project ID (mainly for modpacks)- used for updating skip_install_profile: Option, ) -> crate::Result { + trace!("Creating new profile. {}", name); let state = State::get().await?; let metadata = state.metadata.read().await; @@ -74,7 +77,7 @@ pub async fn profile_create( fs::create_dir_all(&path).await?; } - println!( + info!( "Creating profile at path {}", &canonicalize(&path)?.display() ); @@ -173,7 +176,7 @@ pub async fn profile_create( extra_arguments: None, }); } else { - println!("Could not detect optimal JRE: {optimal_version_key}, falling back to system default."); + emit_warning(&format!("Could not detect optimal JRE: {optimal_version_key}, falling back to system default.")).await?; } emit_profile( diff --git a/theseus/src/api/settings.rs b/theseus/src/api/settings.rs index 45b904707..6e1a5c01d 100644 --- a/theseus/src/api/settings.rs +++ b/theseus/src/api/settings.rs @@ -19,8 +19,22 @@ pub async fn get() -> crate::Result { pub async fn set(settings: Settings) -> crate::Result<()> { let state = State::get().await?; // Replaces the settings struct in the RwLock with the passed argument + let (reset_io, reset_fetch) = async { + let read = state.settings.read().await; + ( + settings.max_concurrent_writes != read.max_concurrent_writes, + settings.max_concurrent_downloads != read.max_concurrent_downloads, + ) + } + .await; + *state.settings.write().await = settings; - state.reset_semaphore().await; // reset semaphore to new max + if reset_io { + state.reset_io_semaphore().await; + } + if reset_fetch { + state.reset_fetch_semaphore().await; + } State::sync().await?; Ok(()) } diff --git a/theseus/src/event/emit.rs b/theseus/src/event/emit.rs index 121729ddd..160190fb4 100644 --- a/theseus/src/event/emit.rs +++ b/theseus/src/event/emit.rs @@ -1,9 +1,11 @@ +use super::LoadingBarId; use crate::event::{ EventError, LoadingBar, LoadingBarType, ProcessPayloadType, ProfilePayloadType, }; use futures::prelude::*; use std::path::PathBuf; +use tracing::warn; #[cfg(feature = "tauri")] use crate::event::{ @@ -13,6 +15,9 @@ use crate::event::{ use tauri::Manager; use uuid::Uuid; +#[cfg(feature = "cli")] +const CLI_PROGRESS_BAR_TOTAL: u64 = 1000; + /* Events are a way we can communciate with the Tauri frontend from the Rust backend. We include a feature flag for Tauri, so that we can compile this code without Tauri. @@ -45,18 +50,34 @@ pub async fn init_loading( bar_type: LoadingBarType, total: f64, title: &str, -) -> crate::Result { +) -> crate::Result { let event_state = crate::EventState::get().await?; - let key = Uuid::new_v4(); + let key = LoadingBarId(Uuid::new_v4()); event_state.loading_bars.write().await.insert( - key, + key.0, LoadingBar { - loading_bar_id: key, + loading_bar_uuid: key.0, message: title.to_string(), total, current: 0.0, bar_type, + #[cfg(feature = "cli")] + cli_progress_bar: { + let pb = indicatif::ProgressBar::new(CLI_PROGRESS_BAR_TOTAL); + + pb.set_position(0); + pb.set_style( + indicatif::ProgressStyle::default_bar() + .template( + "{spinner:.green} [{elapsed_precise}] [{bar:.lime/green}] {pos}/{len} {msg}", + ).unwrap() + .progress_chars("#>-"), + ); + //pb.set_message(title); + + pb + }, }, ); // attempt an initial loading_emit event to the frontend @@ -65,13 +86,13 @@ pub async fn init_loading( } pub async fn init_or_edit_loading( - id: Option, + id: Option, bar_type: LoadingBarType, total: f64, title: &str, -) -> crate::Result { +) -> crate::Result { if let Some(id) = id { - edit_loading(id, bar_type, total, title).await?; + edit_loading(&id, bar_type, total, title).await?; Ok(id) } else { @@ -80,21 +101,27 @@ pub async fn init_or_edit_loading( } // Edits a loading bar's type +// This also resets the bar's current progress to 0 pub async fn edit_loading( - id: Uuid, + id: &LoadingBarId, bar_type: LoadingBarType, total: f64, title: &str, ) -> crate::Result<()> { let event_state = crate::EventState::get().await?; - if let Some(bar) = event_state.loading_bars.write().await.get_mut(&id) { + if let Some(bar) = event_state.loading_bars.write().await.get_mut(&id.0) { bar.bar_type = bar_type; bar.total = total; bar.message = title.to_string(); + bar.current = 0.0; + #[cfg(feature = "cli")] + { + bar.cli_progress_bar.reset(); // indicatif::ProgressBar::new(CLI_PROGRESS_BAR_TOTAL as u64); + } }; - emit_loading(&id, 0.0, None).await?; + emit_loading(id, 0.0, None).await?; Ok(()) } @@ -104,18 +131,19 @@ pub async fn edit_loading( // message is the message to display on the loading bar- if None, use the loading bar's default one // By convention, fraction is the fraction of the progress bar that is filled #[allow(unused_variables)] +#[tracing::instrument(level = "debug")] pub async fn emit_loading( - key: &Uuid, + key: &LoadingBarId, increment_frac: f64, message: Option<&str>, ) -> crate::Result<()> { let event_state = crate::EventState::get().await?; let mut loading_bar = event_state.loading_bars.write().await; - let loading_bar = match loading_bar.get_mut(key) { + let loading_bar = match loading_bar.get_mut(&key.0) { Some(f) => f, None => { - return Err(EventError::NoLoadingBar(*key).into()); + return Err(EventError::NoLoadingBar(key.0).into()); } }; @@ -128,6 +156,22 @@ pub async fn emit_loading( } else { Some(display_frac) }; + + // Emit event to indicatif progress bar + #[cfg(feature = "cli")] + { + loading_bar.cli_progress_bar.set_message( + message + .map(|x| x.to_string()) + .unwrap_or(loading_bar.message.clone()), + ); + loading_bar.cli_progress_bar.set_position( + ((loading_bar.current / loading_bar.total) + * CLI_PROGRESS_BAR_TOTAL as f64) + .round() as u64, + ); + } + // Emit event to tauri #[cfg(feature = "tauri")] event_state @@ -138,7 +182,7 @@ pub async fn emit_loading( fraction: display_frac, message: message.unwrap_or(&loading_bar.message).to_string(), event: loading_bar.bar_type.clone(), - loader_uuid: loading_bar.loading_bar_id, + loader_uuid: loading_bar.loading_bar_uuid, }, ) .map_err(EventError::from)?; @@ -162,6 +206,7 @@ pub async fn emit_warning(message: &str) -> crate::Result<()> { ) .map_err(EventError::from)?; } + warn!("{}", message); Ok(()) } @@ -235,7 +280,6 @@ macro_rules! count { () => (0usize); ( $x:tt $($xs:tt)* ) => (1usize + $crate::count!($($xs)*)); } -#[cfg(feature = "tauri")] #[macro_export] macro_rules! loading_join { ($key:expr, $total:expr, $message:expr; $($task:expr $(,)?)+) => { @@ -272,24 +316,16 @@ macro_rules! loading_join { }; } -#[cfg(not(feature = "tauri"))] -#[macro_export] -macro_rules! loading_join { - ($start:expr, $end:expr, $message:expr; $($future:expr $(,)?)+) => {{ - tokio::try_join!($($future),+) - }}; -} // A drop in replacement to try_for_each_concurrent that emits loading events as it goes // Key is the key to use for which loading bar- a LoadingBarId. If None, does nothing // Total is the total amount of progress that the loading bar should take up by all futures in this (will be split evenly amongst them). // If message is Some(t) you will overwrite this loading bar's message with a custom one // num_futs is the number of futures that will be run, which is needed as we allow Iterator to be passed in, which doesn't have a size -#[cfg(feature = "tauri")] pub async fn loading_try_for_each_concurrent( stream: I, limit: Option, - key: Option<&Uuid>, + key: Option<&LoadingBarId>, total: f64, num_futs: usize, // num is in here as we allow Iterator to be passed in, which doesn't have a size message: Option<&str>, @@ -316,31 +352,3 @@ where }) .await } - -#[cfg(not(feature = "tauri"))] -pub async fn loading_try_for_each_concurrent( - stream: I, - limit: Option, - _key: Option<&Uuid>, - _total: f64, - _num_futs: usize, // num is in here as we allow Iterator to be passed in, which doesn't have a size - _message: Option<&str>, - f: F, -) -> crate::Result<()> -where - I: futures::TryStreamExt + TryStream, - F: FnMut(T) -> Fut + Send, - Fut: Future> + Send, - T: Send, -{ - let mut f = f; - stream - .try_for_each_concurrent(limit, |item| { - let f = f(item); - async move { - f.await?; - Ok(()) - } - }) - .await -} diff --git a/theseus/src/event/mod.rs b/theseus/src/event/mod.rs index bec43dab2..9a88666af 100644 --- a/theseus/src/event/mod.rs +++ b/theseus/src/event/mod.rs @@ -48,11 +48,18 @@ impl EventState { Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone()) } + // Values provided should not be used directly, as they are clones and are not guaranteed to be up-to-date pub async fn list_progress_bars() -> crate::Result> { let value = Self::get().await?; let read = value.loading_bars.read().await; - Ok(read.clone()) + + let mut display_list: HashMap = HashMap::new(); + for (uuid, loading_bar) in read.iter() { + display_list.insert(*uuid, loading_bar.clone()); + } + + Ok(display_list) } // Initialization requires no app handle in non-tauri mode, so we can just use the same function @@ -64,16 +71,84 @@ impl EventState { #[derive(Serialize, Debug, Clone)] pub struct LoadingBar { - pub loading_bar_id: Uuid, + // loading_bar_uuid not be used directly by external functions as it may not reflect the current state of the loading bar/hashmap + pub loading_bar_uuid: Uuid, pub message: String, pub total: f64, pub current: f64, pub bar_type: LoadingBarType, + #[cfg(feature = "cli")] + #[serde(skip)] + pub cli_progress_bar: indicatif::ProgressBar, +} + +#[derive(Serialize, Debug)] +pub struct LoadingBarId(Uuid); + +// When Loading bar id is dropped, we should remove it from the hashmap +impl Drop for LoadingBarId { + fn drop(&mut self) { + let loader_uuid = self.0; + let _event = LoadingBarType::StateInit; + let _message = "finished".to_string(); + tokio::spawn(async move { + if let Ok(event_state) = crate::EventState::get().await { + { + let mut bars = event_state.loading_bars.write().await; + bars.remove(&loader_uuid); + } + } + }); + } +} + +// When Loading bar is dropped, should attempt to throw out one last event to indicate that the loading bar is done +#[cfg(feature = "tauri")] +impl Drop for LoadingBar { + fn drop(&mut self) { + let loader_uuid = self.loading_bar_uuid; + let event = self.bar_type.clone(); + let fraction = self.current / self.total; + let cli_progress_bar = self.cli_progress_bar.clone(); + + tokio::spawn(async move { + #[cfg(feature = "tauri")] + { + use tauri::Manager; + if let Ok(event_state) = crate::EventState::get().await { + let _ = event_state.app.emit_all( + "loading", + LoadingPayload { + fraction: None, + message: "Completed".to_string(), + event, + loader_uuid, + }, + ); + tracing::debug!( + "Exited at {fraction} for loading bar: {:?}", + loader_uuid + ); + } + } + // Emit event to indicatif progress bar arc + #[cfg(feature = "cli")] + { + cli_progress_bar.finish(); + } + }); + } } #[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)] +#[serde(tag = "type")] +#[serde(rename_all = "snake_case")] pub enum LoadingBarType { StateInit, + PackFileDownload { + pack_name: Option, + pack_version: String, + }, PackDownload { pack_name: String, pack_id: Option, @@ -124,6 +199,7 @@ pub struct ProfilePayload { pub event: ProfilePayloadType, } #[derive(Serialize, Clone)] +#[serde(rename_all = "snake_case")] pub enum ProfilePayloadType { Created, Added, // also triggered when Created diff --git a/theseus/src/launcher/auth.rs b/theseus/src/launcher/auth.rs index 5da747965..4fb1f71be 100644 --- a/theseus/src/launcher/auth.rs +++ b/theseus/src/launcher/auth.rs @@ -1,12 +1,11 @@ //! Authentication flow based on Hydra -use crate::util::fetch::{fetch_advanced, fetch_json}; +use crate::util::fetch::{fetch_advanced, fetch_json, FetchSemaphore}; use async_tungstenite as ws; use chrono::{prelude::*, Duration}; use futures::prelude::*; use lazy_static::lazy_static; use reqwest::Method; use serde::{Deserialize, Serialize}; -use tokio::sync::{RwLock, Semaphore}; use url::Url; lazy_static! { @@ -95,7 +94,7 @@ impl HydraAuthFlow { pub async fn extract_credentials( &mut self, - semaphore: &RwLock, + semaphore: &FetchSemaphore, ) -> crate::Result { // Minecraft bearer token let token_resp = self @@ -130,7 +129,7 @@ impl HydraAuthFlow { pub async fn refresh_credentials( credentials: &mut Credentials, - semaphore: &RwLock, + semaphore: &FetchSemaphore, ) -> crate::Result<()> { let resp = fetch_json::( Method::POST, @@ -152,7 +151,7 @@ pub async fn refresh_credentials( // Helpers async fn fetch_info( token: &str, - semaphore: &RwLock, + semaphore: &FetchSemaphore, ) -> crate::Result { let result = fetch_advanced( Method::GET, @@ -160,6 +159,7 @@ async fn fetch_info( None, None, Some(("Authorization", &format!("Bearer {token}"))), + None, semaphore, ) .await?; diff --git a/theseus/src/launcher/download.rs b/theseus/src/launcher/download.rs index c3aa4c745..0fbfaf846 100644 --- a/theseus/src/launcher/download.rs +++ b/theseus/src/launcher/download.rs @@ -1,7 +1,10 @@ //! Downloader for Minecraft data use crate::{ - event::emit::{emit_loading, loading_try_for_each_concurrent}, + event::{ + emit::{emit_loading, loading_try_for_each_concurrent}, + LoadingBarId, + }, state::State, util::{fetch::*, platform::OsExt}, }; @@ -15,24 +18,26 @@ use daedalus::{ }; use futures::prelude::*; use tokio::{fs, sync::OnceCell}; -use uuid::Uuid; #[tracing::instrument(skip_all)] pub async fn download_minecraft( st: &State, version: &GameVersionInfo, - loading_bar: Uuid, + loading_bar: &LoadingBarId, ) -> crate::Result<()> { - log::info!("Downloading Minecraft version {}", version.id); - let assets_index = download_assets_index(st, version).await?; + tracing::info!("Downloading Minecraft version {}", version.id); + // 5 + let assets_index = + download_assets_index(st, version, Some(loading_bar)).await?; tokio::try_join! { - download_client(st, version, Some(&loading_bar)), - download_assets(st, version.assets == "legacy", &assets_index, Some(&loading_bar)), - download_libraries(st, version.libraries.as_slice(), &version.id, Some(&loading_bar)) + // Total loading sums to 80 + download_client(st, version, Some(loading_bar)), // 10 + download_assets(st, version.assets == "legacy", &assets_index, Some(loading_bar)), // 35 + download_libraries(st, version.libraries.as_slice(), &version.id, Some(loading_bar)) // 35 }?; - log::info!("Done downloading Minecraft!"); + tracing::info!("Done downloading Minecraft!"); Ok(()) } @@ -42,10 +47,11 @@ pub async fn download_version_info( version: &GameVersion, loader: Option<&LoaderVersion>, force: Option, + loading_bar: Option<&LoadingBarId>, ) -> crate::Result { let version_id = loader .map_or(version.id.clone(), |it| format!("{}-{}", version.id, it.id)); - log::debug!("Loading version info for Minecraft {version_id}"); + tracing::debug!("Loading version info for Minecraft {version_id}"); let path = st .directories .version_dir(&version_id) @@ -57,7 +63,7 @@ pub async fn download_version_info( .await .and_then(|ref it| Ok(serde_json::from_slice(it)?)) } else { - log::info!("Downloading version info for version {}", &version.id); + tracing::info!("Downloading version info for version {}", &version.id); let mut info = d::minecraft::fetch_version_info(version).await?; if let Some(loader) = loader { @@ -70,7 +76,25 @@ pub async fn download_version_info( Ok(info) }?; - log::debug!("Loaded version info for Minecraft {version_id}"); + if let Some(loading_bar) = loading_bar { + emit_loading( + loading_bar, + if res + .processors + .as_ref() + .map(|x| !x.is_empty()) + .unwrap_or(false) + { + 5.0 + } else { + 15.0 + }, + None, + ) + .await?; + } + + tracing::debug!("Loaded version info for Minecraft {version_id}"); Ok(res) } @@ -78,10 +102,10 @@ pub async fn download_version_info( pub async fn download_client( st: &State, version_info: &GameVersionInfo, - loading_bar: Option<&Uuid>, + loading_bar: Option<&LoadingBarId>, ) -> crate::Result<()> { let version = &version_info.id; - log::debug!("Locating client for version {version}"); + tracing::debug!("Locating client for version {version}"); let client_download = version_info .downloads .get(&d::minecraft::DownloadType::Client) @@ -100,17 +124,17 @@ pub async fn download_client( let bytes = fetch( &client_download.url, Some(&client_download.sha1), - &st.io_semaphore, + &st.fetch_semaphore, ) .await?; write(&path, &bytes, &st.io_semaphore).await?; - log::info!("Fetched client version {version}"); + tracing::trace!("Fetched client version {version}"); } if let Some(loading_bar) = loading_bar { - emit_loading(loading_bar, 20.0, None).await?; + emit_loading(loading_bar, 10.0, None).await?; } - log::debug!("Client loaded for version {version}!"); + tracing::debug!("Client loaded for version {version}!"); Ok(()) } @@ -118,8 +142,9 @@ pub async fn download_client( pub async fn download_assets_index( st: &State, version: &GameVersionInfo, + loading_bar: Option<&LoadingBarId>, ) -> crate::Result { - log::debug!("Loading assets index"); + tracing::debug!("Loading assets index"); let path = st .directories .assets_index_dir() @@ -133,11 +158,14 @@ pub async fn download_assets_index( } else { let index = d::minecraft::fetch_assets_index(version).await?; write(&path, &serde_json::to_vec(&index)?, &st.io_semaphore).await?; - log::info!("Fetched assets index"); + tracing::info!("Fetched assets index"); Ok(index) }?; - log::debug!("Assets index successfully loaded!"); + if let Some(loading_bar) = loading_bar { + emit_loading(loading_bar, 5.0, None).await?; + } + tracing::debug!("Assets index successfully loaded!"); Ok(res) } @@ -146,9 +174,9 @@ pub async fn download_assets( st: &State, with_legacy: bool, index: &AssetsIndex, - loading_bar: Option<&Uuid>, + loading_bar: Option<&LoadingBarId>, ) -> crate::Result<()> { - log::debug!("Loading assets"); + tracing::debug!("Loading assets"); let num_futs = index.objects.len(); let assets = stream::iter(index.objects.iter()) .map(Ok::<(&String, &Asset), crate::Error>); @@ -156,7 +184,7 @@ pub async fn download_assets( loading_try_for_each_concurrent(assets, None, loading_bar, - 50.0, + 35.0, num_futs, None, |(name, asset)| async move { @@ -172,33 +200,33 @@ pub async fn download_assets( async { if !resource_path.exists() { let resource = fetch_cell - .get_or_try_init(|| fetch(&url, Some(hash), &st.io_semaphore)) + .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) .await?; write(&resource_path, resource, &st.io_semaphore).await?; - log::info!("Fetched asset with hash {hash}"); + tracing::trace!("Fetched asset with hash {hash}"); } Ok::<_, crate::Error>(()) }, async { if with_legacy { let resource = fetch_cell - .get_or_try_init(|| fetch(&url, Some(hash), &st.io_semaphore)) + .get_or_try_init(|| fetch(&url, Some(hash), &st.fetch_semaphore)) .await?; let resource_path = st.directories.legacy_assets_dir().join( name.replace('/', &String::from(std::path::MAIN_SEPARATOR)) ); write(&resource_path, resource, &st.io_semaphore).await?; - log::info!("Fetched legacy asset with hash {hash}"); + tracing::trace!("Fetched legacy asset with hash {hash}"); } Ok::<_, crate::Error>(()) }, }?; - log::debug!("Loaded asset with hash {hash}"); + tracing::trace!("Loaded asset with hash {hash}"); Ok(()) }).await?; - log::debug!("Done loading assets!"); + tracing::debug!("Done loading assets!"); Ok(()) } @@ -207,9 +235,9 @@ pub async fn download_libraries( st: &State, libraries: &[Library], version: &str, - loading_bar: Option<&Uuid>, + loading_bar: Option<&LoadingBarId>, ) -> crate::Result<()> { - log::debug!("Loading libraries"); + tracing::debug!("Loading libraries"); tokio::try_join! { fs::create_dir_all(st.directories.libraries_dir()), @@ -218,7 +246,7 @@ pub async fn download_libraries( let num_files = libraries.len(); loading_try_for_each_concurrent( stream::iter(libraries.iter()) - .map(Ok::<&Library, crate::Error>), None, loading_bar,50.0,num_files, None,|library| async move { + .map(Ok::<&Library, crate::Error>), None, loading_bar,35.0,num_files, None,|library| async move { if let Some(rules) = &library.rules { if !rules.iter().all(super::parse_rule) { return Ok(()); @@ -235,10 +263,10 @@ pub async fn download_libraries( artifact: Some(ref artifact), .. }) => { - let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.io_semaphore) + let bytes = fetch(&artifact.url, Some(&artifact.sha1), &st.fetch_semaphore) .await?; write(&path, &bytes, &st.io_semaphore).await?; - log::info!("Fetched library {}", &library.name); + tracing::trace!("Fetched library {}", &library.name); Ok::<_, crate::Error>(()) } None => { @@ -250,9 +278,9 @@ pub async fn download_libraries( &artifact_path ].concat(); - let bytes = fetch(&url, None, &st.io_semaphore).await?; + let bytes = fetch(&url, None, &st.fetch_semaphore).await?; write(&path, &bytes, &st.io_semaphore).await?; - log::info!("Fetched library {}", &library.name); + tracing::trace!("Fetched library {}", &library.name); Ok::<_, crate::Error>(()) } _ => Ok(()) @@ -277,15 +305,15 @@ pub async fn download_libraries( ); if let Some(native) = classifiers.get(&parsed_key) { - let data = fetch(&native.url, Some(&native.sha1), &st.io_semaphore).await?; + let data = fetch(&native.url, Some(&native.sha1), &st.fetch_semaphore).await?; let reader = std::io::Cursor::new(&data); if let Ok(mut archive) = zip::ZipArchive::new(reader) { match archive.extract(&st.directories.version_natives_dir(version)) { - Ok(_) => log::info!("Fetched native {}", &library.name), - Err(err) => log::error!("Failed extracting native {}. err: {}", &library.name, err) + Ok(_) => tracing::info!("Fetched native {}", &library.name), + Err(err) => tracing::error!("Failed extracting native {}. err: {}", &library.name, err) } } else { - log::error!("Failed extracting native {}", &library.name) + tracing::error!("Failed extracting native {}", &library.name) } } } @@ -294,11 +322,11 @@ pub async fn download_libraries( } }?; - log::debug!("Loaded library {}", library.name); + tracing::debug!("Loaded library {}", library.name); Ok(()) } ).await?; - log::debug!("Done loading libraries!"); + tracing::debug!("Done loading libraries!"); Ok(()) } diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 4659546b1..7ea40019f 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -1,6 +1,6 @@ //! Logic for launching Minecraft use crate::event::emit::{emit_loading, init_or_edit_loading}; -use crate::event::LoadingBarType; +use crate::event::{LoadingBarId, LoadingBarType}; use crate::{ process, state::{self as st, MinecraftChild}, @@ -53,9 +53,10 @@ macro_rules! processor_rules { } } +#[tracing::instrument(skip(profile))] pub async fn install_minecraft( profile: &Profile, - existing_loading_bar: Option, + existing_loading_bar: Option, ) -> crate::Result<()> { let state = State::get().await?; let instance_path = &canonicalize(&profile.path)?; @@ -79,14 +80,6 @@ pub async fn install_minecraft( format!("{}-{}", version.id.clone(), it.id.clone()) }); - let mut version_info = download::download_version_info( - &state, - version, - profile.metadata.loader_version.as_ref(), - None, - ) - .await?; - let loading_bar = init_or_edit_loading( existing_loading_bar, LoadingBarType::MinecraftDownload { @@ -95,11 +88,22 @@ pub async fn install_minecraft( profile_uuid: profile.uuid, }, 100.0, - "Downloading Minecraft...", + "Downloading Minecraft", ) .await?; - download::download_minecraft(&state, &version_info, loading_bar).await?; + // Download version info + let mut version_info = download::download_version_info( + &state, + version, + profile.metadata.loader_version.as_ref(), + None, + Some(&loading_bar), + ) + .await?; + + // Download minecraft (5-90) + download::download_minecraft(&state, &version_info, &loading_bar).await?; let client_path = state .directories @@ -131,10 +135,11 @@ pub async fn install_minecraft( .await?; let total_length = processors.len(); + // Forge processors (90-100) for (index, processor) in processors.iter().enumerate() { emit_loading( &loading_bar, - index as f64 / total_length as f64, + 10.0 / total_length as f64, Some(&format!( "Running forge processor {}/{}", index, total_length @@ -223,7 +228,7 @@ pub async fn launch_minecraft( install_minecraft(profile, None).await?; } - let state = st::State::get().await?; + let state = State::get().await?; let metadata = state.metadata.read().await; let instance_path = &canonicalize(&profile.path)?; @@ -250,6 +255,7 @@ pub async fn launch_minecraft( version, profile.metadata.loader_version.as_ref(), None, + None, ) .await?; @@ -320,8 +326,12 @@ pub async fn launch_minecraft( .stdout(Stdio::piped()) .stderr(Stdio::piped()); - // Clear cargo-added env varaibles for debugging, and add settings env vars - clear_cargo_env_vals(&mut command).envs(env_args); + // CARGO-set DYLD_LIBRARY_PATH breaks Minecraft on macOS during testing on playground + #[cfg(target_os = "macos")] + if std::env::var("CARGO").is_ok() { + command.env_remove("DYLD_FALLBACK_LIBRARY_PATH"); + } + command.envs(env_args); // Get Modrinth logs directories let datetime_string = @@ -351,14 +361,3 @@ pub async fn launch_minecraft( ) .await } - -fn clear_cargo_env_vals(command: &mut Command) -> &mut Command { - for (key, _) in std::env::vars() { - command.env_remove(key); - - // if key.starts_with("CARGO") { - // command.env_remove(key); - // } - } - command -} diff --git a/theseus/src/state/children.rs b/theseus/src/state/children.rs index 7443e444e..8f5dad29b 100644 --- a/theseus/src/state/children.rs +++ b/theseus/src/state/children.rs @@ -8,6 +8,7 @@ use tokio::process::Child; use tokio::process::Command; use tokio::process::{ChildStderr, ChildStdout}; use tokio::sync::RwLock; +use tracing::error; use crate::event::emit::emit_process; use crate::event::ProcessPayloadType; @@ -55,7 +56,7 @@ impl Children { let stdout_clone = stdout.clone(); tokio::spawn(async move { if let Err(e) = stdout_clone.read_stdout(child_stdout).await { - eprintln!("Stdout process died with error: {}", e); + error!("Stdout process died with error: {}", e); } }); } @@ -64,7 +65,7 @@ impl Children { let stderr_clone = stderr.clone(); tokio::spawn(async move { if let Err(e) = stderr_clone.read_stderr(child_stderr).await { - eprintln!("Stderr process died with error: {}", e); + error!("Stderr process died with error: {}", e); } }); } diff --git a/theseus/src/state/metadata.rs b/theseus/src/state/metadata.rs index 557ef11dd..f79b8d717 100644 --- a/theseus/src/state/metadata.rs +++ b/theseus/src/state/metadata.rs @@ -1,6 +1,6 @@ //! Theseus metadata use crate::data::DirectoryInfo; -use crate::util::fetch::{read_json, write}; +use crate::util::fetch::{read_json, write, IoSemaphore}; use crate::State; use daedalus::{ minecraft::{fetch_version_manifest, VersionManifest as MinecraftManifest}, @@ -9,7 +9,6 @@ use daedalus::{ }, }; use serde::{Deserialize, Serialize}; -use tokio::sync::{RwLock, Semaphore}; const METADATA_URL: &str = "https://meta.modrinth.com"; @@ -51,7 +50,7 @@ impl Metadata { // Attempt to fetch metadata and store in sled DB pub async fn init( dirs: &DirectoryInfo, - io_semaphore: &RwLock, + io_semaphore: &IoSemaphore, ) -> crate::Result { let mut metadata = None; let metadata_path = dirs.caches_meta_dir().join("metadata.json"); @@ -79,7 +78,7 @@ impl Metadata { match res { Ok(()) => {} Err(err) => { - log::warn!("Unable to fetch launcher metadata: {err}") + tracing::warn!("Unable to fetch launcher metadata: {err}") } } } @@ -120,7 +119,7 @@ impl Metadata { match res { Ok(()) => {} Err(err) => { - log::warn!("Unable to update launcher metadata: {err}") + tracing::warn!("Unable to update launcher metadata: {err}") } }; } diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 82fc54ecf..f741676ff 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -6,6 +6,7 @@ use crate::event::LoadingBarType; use crate::loading_join; use crate::state::users::Users; +use crate::util::fetch::{FetchSemaphore, IoSemaphore}; use std::sync::Arc; use tokio::sync::{OnceCell, RwLock, Semaphore}; @@ -44,10 +45,16 @@ static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); pub struct State { /// Information on the location of files used in the launcher pub directories: DirectoryInfo, + + /// Semaphore used to limit concurrent network requests and avoid errors + pub fetch_semaphore: FetchSemaphore, + /// Stored maximum number of sempahores of current fetch_semaphore + pub fetch_semaphore_max: RwLock, /// Semaphore used to limit concurrent I/O and avoid errors - pub io_semaphore: RwLock, + pub io_semaphore: IoSemaphore, /// Stored maximum number of sempahores of current io_semaphore pub io_semaphore_max: RwLock, + /// Launcher metadata pub metadata: RwLock, /// Launcher configuration @@ -73,7 +80,7 @@ impl State { let loading_bar = init_loading( LoadingBarType::StateInit, 100.0, - "Initializing launcher...", + "Initializing launcher", ) .await?; @@ -83,20 +90,26 @@ impl State { // Settings let settings = Settings::init(&directories.settings_file()).await?; - let io_semaphore = RwLock::new(Semaphore::new( - settings.max_concurrent_downloads, + let fetch_semaphore = FetchSemaphore(RwLock::new( + Semaphore::new(settings.max_concurrent_downloads), + )); + let io_semaphore = IoSemaphore(RwLock::new( + Semaphore::new(settings.max_concurrent_writes), )); emit_loading(&loading_bar, 10.0, None).await?; let metadata_fut = Metadata::init(&directories, &io_semaphore); - let profiles_fut = - Profiles::init(&directories, &io_semaphore); - let tags_fut = Tags::init(&directories, &io_semaphore); + let profiles_fut = Profiles::init(&directories); + let tags_fut = Tags::init( + &directories, + &io_semaphore, + &fetch_semaphore, + ); let users_fut = Users::init(&directories, &io_semaphore); // Launcher data let (metadata, profiles, tags, users) = loading_join! { - Some(&loading_bar), 70.0, Some("Initializing..."); + Some(&loading_bar), 70.0, Some("Loading metadata"); metadata_fut, profiles_fut, tags_fut, @@ -109,9 +122,13 @@ impl State { Ok(Arc::new(Self { directories, + fetch_semaphore, + fetch_semaphore_max: RwLock::new( + settings.max_concurrent_downloads as u32, + ), io_semaphore, io_semaphore_max: RwLock::new( - settings.max_concurrent_downloads as u32, + settings.max_concurrent_writes as u32, ), metadata: RwLock::new(metadata), settings: RwLock::new(settings), @@ -169,17 +186,34 @@ impl State { .await } - /// Reset semaphores to default values + /// Reset IO semaphore to default values /// This will block until all uses of the semaphore are complete, so it should only be called /// when we are not in the middle of downloading something (ie: changing the settings!) - pub async fn reset_semaphore(&self) { + pub async fn reset_io_semaphore(&self) { let settings = self.settings.read().await; - let mut io_semaphore = self.io_semaphore.write().await; + let mut io_semaphore = self.io_semaphore.0.write().await; let mut total_permits = self.io_semaphore_max.write().await; // Wait to get all permits back let _ = io_semaphore.acquire_many(*total_permits).await; + // Reset the semaphore + io_semaphore.close(); + *total_permits = settings.max_concurrent_writes as u32; + *io_semaphore = Semaphore::new(settings.max_concurrent_writes); + } + + /// Reset IO semaphore to default values + /// This will block until all uses of the semaphore are complete, so it should only be called + /// when we are not in the middle of downloading something (ie: changing the settings!) + pub async fn reset_fetch_semaphore(&self) { + let settings = self.settings.read().await; + let mut io_semaphore = self.fetch_semaphore.0.write().await; + let mut total_permits = self.fetch_semaphore_max.write().await; + + // Wait to get all permits back + let _ = io_semaphore.acquire_many(*total_permits).await; + // Reset the semaphore io_semaphore.close(); *total_permits = settings.max_concurrent_downloads as u32; diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index bbff16bd8..9597d336a 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -5,7 +5,9 @@ use crate::event::emit::emit_profile; use crate::event::ProfilePayloadType; use crate::state::projects::Project; use crate::state::{ModrinthVersion, ProjectType}; -use crate::util::fetch::{fetch, fetch_json, write, write_cached_icon}; +use crate::util::fetch::{ + fetch, fetch_json, write, write_cached_icon, IoSemaphore, +}; use crate::State; use daedalus::modded::LoaderVersion; use dunce::canonicalize; @@ -17,8 +19,7 @@ use std::{ collections::HashMap, path::{Path, PathBuf}, }; -use tokio::sync::Semaphore; -use tokio::{fs, sync::RwLock}; +use tokio::fs; use uuid::Uuid; const PROFILE_JSON_PATH: &str = "profile.json"; @@ -149,7 +150,7 @@ impl Profile { pub async fn set_icon<'a>( &'a mut self, cache_dir: &Path, - semaphore: &RwLock, + semaphore: &IoSemaphore, icon: bytes::Bytes, file_name: &str, ) -> crate::Result<&'a mut Self> { @@ -168,6 +169,7 @@ impl Profile { paths, state.directories.caches_dir(), &state.io_semaphore, + &state.fetch_semaphore, ) .await?; @@ -218,7 +220,7 @@ impl Profile { &format!("{MODRINTH_API_URL}version/{version_id}"), None, None, - &state.io_semaphore, + &state.fetch_semaphore, ) .await?; @@ -237,7 +239,7 @@ impl Profile { let bytes = fetch( &file.url, file.hashes.get("sha1").map(|x| &**x), - &state.io_semaphore, + &state.fetch_semaphore, ) .await?; @@ -345,10 +347,7 @@ impl Profile { impl Profiles { #[tracing::instrument] - pub async fn init( - dirs: &DirectoryInfo, - io_sempahore: &RwLock, - ) -> crate::Result { + pub async fn init(dirs: &DirectoryInfo) -> crate::Result { let mut profiles = HashMap::new(); fs::create_dir_all(dirs.profiles_dir()).await?; let mut entries = fs::read_dir(dirs.profiles_dir()).await?; @@ -358,7 +357,9 @@ impl Profiles { let prof = match Self::read_profile_from_dir(&path).await { Ok(prof) => Some(prof), Err(err) => { - log::warn!("Error loading profile: {err}. Skipping..."); + tracing::warn!( + "Error loading profile: {err}. Skipping..." + ); None } }; @@ -395,6 +396,7 @@ impl Profiles { files, state.directories.caches_dir(), &state.io_semaphore, + &state.fetch_semaphore, ) .await?; @@ -417,7 +419,7 @@ impl Profiles { match res { Ok(()) => {} Err(err) => { - log::warn!("Unable to fetch profile projects: {err}") + tracing::warn!("Unable to fetch profile projects: {err}") } }; } diff --git a/theseus/src/state/projects.rs b/theseus/src/state/projects.rs index dd9ff403e..ea8fbb977 100644 --- a/theseus/src/state/projects.rs +++ b/theseus/src/state/projects.rs @@ -2,7 +2,9 @@ use crate::config::MODRINTH_API_URL; use crate::state::Profile; -use crate::util::fetch::{fetch_json, write_cached_icon}; +use crate::util::fetch::{ + fetch_json, write_cached_icon, FetchSemaphore, IoSemaphore, +}; use async_zip::tokio::read::fs::ZipFileReader; use chrono::{DateTime, Utc}; use reqwest::Method; @@ -12,7 +14,6 @@ use sha2::Digest; use std::collections::HashMap; use std::path::{Path, PathBuf}; use tokio::io::AsyncReadExt; -use tokio::sync::{RwLock, Semaphore}; #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "lowercase")] @@ -203,7 +204,7 @@ async fn read_icon_from_file( icon_path: Option, cache_dir: &Path, path: &PathBuf, - io_semaphore: &RwLock, + io_semaphore: &IoSemaphore, ) -> crate::Result> { if let Some(icon_path) = icon_path { // we have to repoen the zip twice here :( @@ -252,7 +253,8 @@ pub async fn infer_data_from_files( profile: Profile, paths: Vec, cache_dir: PathBuf, - io_semaphore: &RwLock, + io_semaphore: &IoSemaphore, + fetch_semaphore: &FetchSemaphore, ) -> crate::Result> { let mut file_path_hashes = HashMap::new(); @@ -278,7 +280,7 @@ pub async fn infer_data_from_files( "hashes": file_path_hashes.keys().collect::>(), "algorithm": "sha512", })), - io_semaphore, + fetch_semaphore, ), fetch_json::>( Method::POST, @@ -290,7 +292,7 @@ pub async fn infer_data_from_files( "loaders": [profile.metadata.loader], "game_versions": [profile.metadata.game_version] })), - io_semaphore, + fetch_semaphore, ) )?; @@ -308,7 +310,7 @@ pub async fn infer_data_from_files( ), None, None, - io_semaphore, + fetch_semaphore, ) .await?; @@ -325,7 +327,7 @@ pub async fn infer_data_from_files( ), None, None, - io_semaphore, + fetch_semaphore, ) .await? .into_iter() diff --git a/theseus/src/state/settings.rs b/theseus/src/state/settings.rs index 8548d5855..658af2d95 100644 --- a/theseus/src/state/settings.rs +++ b/theseus/src/state/settings.rs @@ -23,6 +23,7 @@ pub struct Settings { pub default_user: Option, pub hooks: Hooks, pub max_concurrent_downloads: usize, + pub max_concurrent_writes: usize, pub version: u32, pub collapsed_navigation: bool, } @@ -39,6 +40,7 @@ impl Default for Settings { default_user: None, hooks: Hooks::default(), max_concurrent_downloads: 64, + max_concurrent_writes: 100, version: CURRENT_FORMAT_VERSION, collapsed_navigation: false, } @@ -84,7 +86,7 @@ impl Settings { match res { Ok(()) => {} Err(err) => { - log::warn!("Unable to update launcher java: {err}") + tracing::warn!("Unable to update launcher java: {err}") } }; } diff --git a/theseus/src/state/tags.rs b/theseus/src/state/tags.rs index 55805e81c..b6b351192 100644 --- a/theseus/src/state/tags.rs +++ b/theseus/src/state/tags.rs @@ -2,11 +2,12 @@ use std::path::PathBuf; use reqwest::Method; use serde::{Deserialize, Serialize}; -use tokio::sync::{RwLock, Semaphore}; use crate::config::MODRINTH_API_URL; use crate::data::DirectoryInfo; -use crate::util::fetch::{fetch_json, read_json, write}; +use crate::util::fetch::{ + fetch_json, read_json, write, FetchSemaphore, IoSemaphore, +}; // Serializeable struct for all tags to be fetched together by the frontend #[derive(Debug, Clone, Serialize, Deserialize)] @@ -21,7 +22,8 @@ pub struct Tags { impl Tags { pub async fn init( dirs: &DirectoryInfo, - io_semaphore: &RwLock, + io_semaphore: &IoSemaphore, + fetch_sempahore: &FetchSemaphore, ) -> crate::Result { let mut tags = None; let tags_path = dirs.caches_meta_dir().join("tags.json"); @@ -30,10 +32,10 @@ impl Tags { { tags = Some(tags_json); } else { - match Self::fetch(io_semaphore).await { + match Self::fetch(fetch_sempahore).await { Ok(tags_fetch) => tags = Some(tags_fetch), Err(err) => { - log::warn!("Unable to fetch launcher tags: {err}") + tracing::warn!("Unable to fetch launcher tags: {err}") } } } @@ -51,7 +53,7 @@ impl Tags { pub async fn update() { let res = async { let state = crate::State::get().await?; - let tags_fetch = Tags::fetch(&state.io_semaphore).await?; + let tags_fetch = Tags::fetch(&state.fetch_semaphore).await?; let tags_path = state.directories.caches_meta_dir().join("tags.json"); @@ -74,7 +76,7 @@ impl Tags { match res { Ok(()) => {} Err(err) => { - log::warn!("Unable to update launcher tags: {err}") + tracing::warn!("Unable to update launcher tags: {err}") } }; } @@ -116,7 +118,7 @@ impl Tags { } // Fetches the tags from the Modrinth API and stores them in the database - pub async fn fetch(semaphore: &RwLock) -> crate::Result { + pub async fn fetch(semaphore: &FetchSemaphore) -> crate::Result { let categories = format!("{MODRINTH_API_URL}tag/category"); let loaders = format!("{MODRINTH_API_URL}tag/loader"); let game_versions = format!("{MODRINTH_API_URL}tag/game_version"); diff --git a/theseus/src/state/users.rs b/theseus/src/state/users.rs index 6c58a3c41..6d402a6da 100644 --- a/theseus/src/state/users.rs +++ b/theseus/src/state/users.rs @@ -1,10 +1,9 @@ //! User login info use crate::auth::Credentials; use crate::data::DirectoryInfo; -use crate::util::fetch::{read_json, write}; +use crate::util::fetch::{read_json, write, IoSemaphore}; use crate::State; use std::collections::HashMap; -use tokio::sync::{RwLock, Semaphore}; use uuid::Uuid; const USERS_JSON: &str = "users.json"; @@ -16,7 +15,7 @@ pub(crate) struct Users(pub(crate) HashMap); impl Users { pub async fn init( dirs: &DirectoryInfo, - io_semaphore: &RwLock, + io_semaphore: &IoSemaphore, ) -> crate::Result { let users_path = dirs.caches_meta_dir().join(USERS_JSON); let users = read_json(&users_path, io_semaphore).await.ok(); diff --git a/theseus/src/util/fetch.rs b/theseus/src/util/fetch.rs index 2bb286742..58e6270d3 100644 --- a/theseus/src/util/fetch.rs +++ b/theseus/src/util/fetch.rs @@ -1,4 +1,6 @@ //! Functions for fetching infromation from the Internet +use crate::event::emit::emit_loading; +use crate::event::LoadingBarId; use bytes::Bytes; use lazy_static::lazy_static; use reqwest::Method; @@ -12,6 +14,11 @@ use tokio::{ io::AsyncWriteExt, }; +#[derive(Debug)] +pub struct IoSemaphore(pub RwLock); +#[derive(Debug)] +pub struct FetchSemaphore(pub RwLock); + lazy_static! { static ref REQWEST_CLIENT: reqwest::Client = { let mut headers = reqwest::header::HeaderMap::new(); @@ -34,9 +41,9 @@ const FETCH_ATTEMPTS: usize = 3; pub async fn fetch( url: &str, sha1: Option<&str>, - semaphore: &RwLock, + semaphore: &FetchSemaphore, ) -> crate::Result { - fetch_advanced(Method::GET, url, sha1, None, None, semaphore).await + fetch_advanced(Method::GET, url, sha1, None, None, None, semaphore).await } #[tracing::instrument(skip(json_body, semaphore))] @@ -45,13 +52,14 @@ pub async fn fetch_json( url: &str, sha1: Option<&str>, json_body: Option, - semaphore: &RwLock, + semaphore: &FetchSemaphore, ) -> crate::Result where T: DeserializeOwned, { let result = - fetch_advanced(method, url, sha1, json_body, None, semaphore).await?; + fetch_advanced(method, url, sha1, json_body, None, None, semaphore) + .await?; let value = serde_json::from_slice(&result)?; Ok(value) } @@ -64,9 +72,10 @@ pub async fn fetch_advanced( sha1: Option<&str>, json_body: Option, header: Option<(&str, &str)>, - semaphore: &RwLock, + loading_bar: Option<(&LoadingBarId, f64)>, + semaphore: &FetchSemaphore, ) -> crate::Result { - let io_semaphore = semaphore.read().await; + let io_semaphore = semaphore.0.read().await; let _permit = io_semaphore.acquire().await?; for attempt in 1..=(FETCH_ATTEMPTS + 1) { @@ -83,7 +92,35 @@ pub async fn fetch_advanced( let result = req.send().await; match result { Ok(x) => { - let bytes = x.bytes().await; + let bytes = if let Some((bar, total)) = &loading_bar { + let length = x.content_length(); + if let Some(total_size) = length { + use futures::StreamExt; + let mut stream = x.bytes_stream(); + let mut bytes = Vec::new(); + while let Some(item) = stream.next().await { + let chunk = item.or(Err( + crate::error::ErrorKind::NoValueFor( + "fetch bytes".to_string(), + ), + ))?; + bytes.append(&mut chunk.to_vec()); + emit_loading( + bar, + (chunk.len() as f64 / total_size as f64) + * total, + None, + ) + .await?; + } + + Ok(bytes::Bytes::from(bytes)) + } else { + x.bytes().await + } + } else { + x.bytes().await + }; if let Ok(bytes) = bytes { if let Some(sha1) = sha1 { @@ -101,7 +138,7 @@ pub async fn fetch_advanced( } } - log::debug!("Done downloading URL {url}"); + tracing::trace!("Done downloading URL {url}"); return Ok(bytes); } else if attempt <= 3 { continue; @@ -124,7 +161,7 @@ pub async fn fetch_advanced( pub async fn fetch_mirrors( mirrors: &[&str], sha1: Option<&str>, - semaphore: &RwLock, + semaphore: &FetchSemaphore, ) -> crate::Result { if mirrors.is_empty() { return Err(crate::ErrorKind::InputError( @@ -146,12 +183,12 @@ pub async fn fetch_mirrors( pub async fn read_json( path: &Path, - semaphore: &RwLock, + semaphore: &IoSemaphore, ) -> crate::Result where T: DeserializeOwned, { - let io_semaphore = semaphore.read().await; + let io_semaphore = semaphore.0.read().await; let _permit = io_semaphore.acquire().await?; let json = fs::read(path).await?; @@ -164,9 +201,9 @@ where pub async fn write<'a>( path: &Path, bytes: &[u8], - semaphore: &RwLock, + semaphore: &IoSemaphore, ) -> crate::Result<()> { - let io_semaphore = semaphore.read().await; + let io_semaphore = semaphore.0.read().await; let _permit = io_semaphore.acquire().await?; if let Some(parent) = path.parent() { @@ -175,7 +212,7 @@ pub async fn write<'a>( let mut file = File::create(path).await?; file.write_all(bytes).await?; - log::debug!("Done writing file {}", path.display()); + tracing::trace!("Done writing file {}", path.display()); Ok(()) } @@ -184,7 +221,7 @@ pub async fn write_cached_icon( icon_path: &str, cache_dir: &Path, bytes: Bytes, - semaphore: &RwLock, + semaphore: &IoSemaphore, ) -> crate::Result { let extension = Path::new(&icon_path).extension().and_then(OsStr::to_str); let hash = sha1_async(bytes.clone()).await?; diff --git a/theseus_cli/Cargo.toml b/theseus_cli/Cargo.toml index 2e966d69f..46927e83f 100644 --- a/theseus_cli/Cargo.toml +++ b/theseus_cli/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -theseus = { path = "../theseus" } +theseus = { path = "../theseus", features = ["cli"] } daedalus = {version = "0.1.15", features = ["bincode"]} tokio = { version = "1", features = ["full"] } tokio-stream = { version = "0.1", features = ["fs"] } diff --git a/theseus_gui/src-tauri/Cargo.toml b/theseus_gui/src-tauri/Cargo.toml index 2d7c32896..dde54fc27 100644 --- a/theseus_gui/src-tauri/Cargo.toml +++ b/theseus_gui/src-tauri/Cargo.toml @@ -15,7 +15,7 @@ tauri-build = { version = "1.2", features = [] } regex = "1.5" [dependencies] -theseus = { path = "../../theseus", features = ["tauri"] } +theseus = { path = "../../theseus", features = ["tauri", "cli"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } @@ -30,6 +30,10 @@ url = "2.2" uuid = { version = "1.1", features = ["serde", "v4"] } os_info = "3.7.0" +tracing = "0.1.37" +tracing-subscriber = "0.2" +tracing-error = "0.1" + [features] # by default Tauri runs in production mode # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL diff --git a/theseus_gui/src-tauri/src/api/mod.rs b/theseus_gui/src-tauri/src/api/mod.rs index a1b5a33f9..fbcfa3181 100644 --- a/theseus_gui/src-tauri/src/api/mod.rs +++ b/theseus_gui/src-tauri/src/api/mod.rs @@ -47,6 +47,8 @@ where } // Lists active progress bars +// Create a new HashMap with the same keys +// Values provided should not be used directly, as they are not guaranteed to be up-to-date #[tauri::command] pub async fn progress_bars_list( ) -> Result> { @@ -64,6 +66,16 @@ macro_rules! impl_serialize { S: Serializer, { match self { + // For the Theseus variant, we add a special display for the error, + // to view the spans if subscribed to them (which is information that is lost when serializing) + TheseusSerializableError::Theseus(theseus_error) => { + $crate::error::display_tracing_error(theseus_error); + + let mut state = serializer.serialize_struct("Theseus", 2)?; + state.serialize_field("field_name", "Theseus")?; + state.serialize_field("message", &theseus_error.to_string())?; + state.end() + } $( TheseusSerializableError::$variant(message) => { let mut state = serializer.serialize_struct(stringify!($variant), 2)?; @@ -80,7 +92,6 @@ macro_rules! impl_serialize { // Use the macro to implement Serialize for TheseusSerializableError impl_serialize! { - Theseus, IO, NoProfileFound, } diff --git a/theseus_gui/src-tauri/src/api/pack.rs b/theseus_gui/src-tauri/src/api/pack.rs index 6ce6585b6..a745f7894 100644 --- a/theseus_gui/src-tauri/src/api/pack.rs +++ b/theseus_gui/src-tauri/src/api/pack.rs @@ -5,8 +5,12 @@ use theseus::prelude::*; // Creates a pack from a version ID (returns a path to the created profile) // invoke('pack_install_version_id', version_id) #[tauri::command] -pub async fn pack_install_version_id(version_id: String) -> Result { - let res = pack::install_pack_from_version_id(version_id).await?; +pub async fn pack_install_version_id( + version_id: String, + pack_title: Option, +) -> Result { + let res = + pack::install_pack_from_version_id(version_id, pack_title).await?; Ok(res) } diff --git a/theseus_gui/src-tauri/src/api/settings.rs b/theseus_gui/src-tauri/src/api/settings.rs index b3e14cbe1..d19970c0c 100644 --- a/theseus_gui/src-tauri/src/api/settings.rs +++ b/theseus_gui/src-tauri/src/api/settings.rs @@ -15,6 +15,7 @@ pub struct FrontendSettings { pub default_user: Option, pub hooks: Hooks, pub max_concurrent_downloads: usize, + pub max_concurrent_writes: usize, pub version: u32, pub collapsed_navigation: bool, } @@ -39,6 +40,7 @@ pub async fn settings_get() -> Result { default_user: backend_settings.default_user, hooks: backend_settings.hooks, max_concurrent_downloads: backend_settings.max_concurrent_downloads, + max_concurrent_writes: backend_settings.max_concurrent_writes, version: backend_settings.version, collapsed_navigation: backend_settings.collapsed_navigation, }; @@ -77,6 +79,7 @@ pub async fn settings_set(settings: FrontendSettings) -> Result<()> { default_user: settings.default_user, hooks: settings.hooks, max_concurrent_downloads: settings.max_concurrent_downloads, + max_concurrent_writes: settings.max_concurrent_writes, version: settings.version, collapsed_navigation: settings.collapsed_navigation, }; diff --git a/theseus_gui/src-tauri/src/error.rs b/theseus_gui/src-tauri/src/error.rs new file mode 100644 index 000000000..22454d65e --- /dev/null +++ b/theseus_gui/src-tauri/src/error.rs @@ -0,0 +1,18 @@ +use tracing_error::ExtractSpanTrace; + +pub fn display_tracing_error(err: &theseus::Error) { + match get_span_trace(err) { + Some(span_trace) => { + tracing::error!(error = %err, span_trace = %span_trace); + } + None => { + tracing::error!(error = %err); + } + } +} + +pub fn get_span_trace<'a>( + error: &'a (dyn std::error::Error + 'static), +) -> Option<&'a tracing_error::SpanTrace> { + error.source().and_then(|e| e.span_trace()) +} diff --git a/theseus_gui/src-tauri/src/main.rs b/theseus_gui/src-tauri/src/main.rs index 7d5065869..e67a62a70 100644 --- a/theseus_gui/src-tauri/src/main.rs +++ b/theseus_gui/src-tauri/src/main.rs @@ -5,7 +5,11 @@ use theseus::prelude::*; +use tracing_error::ErrorLayer; +use tracing_subscriber::EnvFilter; + mod api; +mod error; // Should be called in launcher initialization #[tauri::command] @@ -37,7 +41,34 @@ async fn should_disable_mouseover() -> bool { false } +use tracing_subscriber::prelude::*; + fn main() { + /* + tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show + ERROR > WARN > INFO > DEBUG > TRACE + eg. RUST_LOG=info will show info, warn, and error logs + RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates) + RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates) + + Error messages returned to Tauri will display as traced error logs if they return an error. + This will also include an attached span trace if the error is from a tracing error, and the level is set to info, debug, or trace + + on unix: + RUST_LOG="theseus=trace" {run command} + + */ + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("theseus=info")); + + let subscriber = tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(filter) + .with(ErrorLayer::default()); + + tracing::subscriber::set_global_default(subscriber) + .expect("setting default subscriber failed"); + tauri::Builder::default() .invoke_handler(tauri::generate_handler![ initialize_state, diff --git a/theseus_gui/src/helpers/events.js b/theseus_gui/src/helpers/events.js index b166d0922..5f7282478 100644 --- a/theseus_gui/src/helpers/events.js +++ b/theseus_gui/src/helpers/events.js @@ -25,15 +25,16 @@ import { listen } from '@tauri-apps/api/event' /// Payload for the 'loading' event /* LoadingPayload { - event: "StateInit", "PackDownload", etc - - Certain states have additional fields: - - PackDownload: { - pack_name: name of the pack - pack_id, optional, the id of the modpack - pack_version, optional, the version of the modpack - - MinecraftDownload: { - profile_name: name of the profile - profile_uuid: unique identification of the profile + event: { + type: string, one of "StateInit", "PackDownload", etc + (Optional fields depending on event type) + pack_name: name of the pack + pack_id, optional, the id of the modpack + pack_version, optional, the version of the modpack + profile_name: name of the profile + profile_uuid: unique identification of the profile + + } loader_uuid: unique identification of the loading bar fraction: number, (as a fraction of 1, how much we'vel oaded so far). If null, by convention, loading is finished message: message to display to the user diff --git a/theseus_gui/src/helpers/pack.js b/theseus_gui/src/helpers/pack.js index f784a50be..2418345ae 100644 --- a/theseus_gui/src/helpers/pack.js +++ b/theseus_gui/src/helpers/pack.js @@ -6,8 +6,8 @@ import { invoke } from '@tauri-apps/api/tauri' // Installs pack from a version ID -export async function install(versionId) { - return await invoke('pack_install_version_id', { versionId }) +export async function install(versionId, packTitle) { + return await invoke('pack_install_version_id', { versionId, packTitle }) } // Installs pack from a path diff --git a/theseus_playground/Cargo.toml b/theseus_playground/Cargo.toml index eec8421dc..89848fd63 100644 --- a/theseus_playground/Cargo.toml +++ b/theseus_playground/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -theseus = { path = "../theseus" } +theseus = { path = "../theseus", features = ["cli"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } @@ -21,3 +21,7 @@ tokio-stream = { version = "0.1", features = ["fs"] } futures = "0.3" daedalus = {version = "0.1.15", features = ["bincode"] } uuid = { version = "1.1", features = ["serde", "v4"] } + +tracing = "0.1.37" +tracing-subscriber = "0.2" +tracing-error = "0.1" diff --git a/theseus_playground/src/main.rs b/theseus_playground/src/main.rs index f1ff2382e..5b966f861 100644 --- a/theseus_playground/src/main.rs +++ b/theseus_playground/src/main.rs @@ -8,6 +8,9 @@ use theseus::jre::autodetect_java_globals; use theseus::prelude::*; use theseus::profile_create::profile_create; use tokio::time::{sleep, Duration}; +use tracing_error::ErrorLayer; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::EnvFilter; // A simple Rust implementation of the authentication run // 1) call the authenticate_begin_flow() function to get the URL to open (like you would in the frontend) @@ -31,16 +34,27 @@ pub async fn authenticate_run() -> theseus::Result { async fn main() -> theseus::Result<()> { println!("Starting."); + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("theseus=info")); + + let subscriber = tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(filter) + .with(ErrorLayer::default()); + + tracing::subscriber::set_global_default(subscriber) + .expect("setting default subscriber failed"); + // Initialize state let st = State::get().await?; //State::update(); st.settings.write().await.java_globals = autodetect_java_globals().await?; - st.settings.write().await.max_concurrent_downloads = 5; + st.settings.write().await.max_concurrent_downloads = 50; st.settings.write().await.hooks.post_exit = Some("echo This is after Minecraft runs- global setting!".to_string()); // Changed the settings, so need to reset the semaphore - st.reset_semaphore().await; + st.reset_fetch_semaphore().await; // Clear profiles println!("Clearing profiles."); @@ -53,21 +67,21 @@ async fn main() -> theseus::Result<()> { println!("Creating/adding profile."); - let name = "Example".to_string(); - let game_version = "1.19.2".to_string(); - let modloader = ModLoader::Vanilla; - let loader_version = "stable".to_string(); - - let profile_path = profile_create( - name.clone(), - game_version, - modloader, - Some(loader_version), - None, - None, - None, - ) - .await?; + // let name = "Example".to_string(); + // let game_version = "1.19.2".to_string(); + // let modloader = ModLoader::Vanilla; + // let loader_version = "stable".to_string(); + // + // let profile_path = profile_create( + // name.clone(), + // game_version, + // modloader, + // Some(loader_version), + // None, + // None, + // None, + // ) + // .await?; // let mut value = list().await?; // let profile_path = value.iter().next().map(|x| x.0).unwrap(); @@ -89,10 +103,12 @@ async fn main() -> theseus::Result<()> { // profile::toggle_disable_project(&profile_path, &sodium_path).await?; // // profile::remove_project(&profile_path, &mod_menu_path).await?; - // let profile_path = - // pack::install_pack_from_version_id("zroFQG1k".to_string()) - // .await - // .unwrap(); + let profile_path = pack::install_pack_from_version_id( + "zroFQG1k".to_string(), + Some("Technical Electrical".to_string()), + ) + .await + .unwrap(); // async closure for testing any desired edits // (ie: changing the java runtime of an added profile)