From 34005dd2e2001c05fef7d86513ea57ac477b1130 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Fri, 7 Apr 2023 13:31:06 -0700 Subject: [PATCH] Jre api (#66) * basic push * actual push * JRE detection, and autosetting * removed a println, retrying CI/CD * new game version compare; preset java 7 and 8 using our jre * 1.8 mislabeled * working JRE changes * fixed bugs with JRE setup * fixed bugs with JRE setup * manual merge * prettier * fixes + jre 17 * clippy, prettier * typo * forgot to hook up a function * pr fix + comment fix * added loader_version * take 2 --- theseus/src/api/jre.rs | 137 +++++++++++++++++++++++++++ theseus/src/api/mod.rs | 8 +- theseus/src/api/profile.rs | 57 ++++++----- theseus/src/api/profile_create.rs | 16 +++- theseus/src/config.rs | 1 - theseus/src/error.rs | 6 ++ theseus/src/launcher/mod.rs | 3 +- theseus/src/state/auth_task.rs | 2 +- theseus/src/state/java_globals.rs | 61 ++++++++++++ theseus/src/state/mod.rs | 12 ++- theseus/src/state/profiles.rs | 11 ++- theseus/src/state/projects.rs | 4 +- theseus/src/state/settings.rs | 13 +-- theseus/src/state/tags.rs | 17 ++-- theseus/src/util/jre.rs | 114 ++++++++++++++++++---- theseus_gui/src-tauri/src/api/jre.rs | 62 ++++++++++++ theseus_gui/src-tauri/src/api/mod.rs | 5 + theseus_gui/src-tauri/src/main.rs | 8 ++ theseus_gui/src/helpers/jre.js | 63 ++++++++++++ theseus_gui/src/helpers/settings.js | 3 +- theseus_playground/src/main.rs | 22 ++--- 21 files changed, 542 insertions(+), 83 deletions(-) create mode 100644 theseus/src/api/jre.rs create mode 100644 theseus/src/state/java_globals.rs create mode 100644 theseus_gui/src-tauri/src/api/jre.rs create mode 100644 theseus_gui/src/helpers/jre.js diff --git a/theseus/src/api/jre.rs b/theseus/src/api/jre.rs new file mode 100644 index 000000000..b8398a360 --- /dev/null +++ b/theseus/src/api/jre.rs @@ -0,0 +1,137 @@ +//! Authentication flow interface +use crate::{ + launcher::download, + prelude::Profile, + state::JavaGlobals, + util::jre::{self, extract_java_majorminor_version, JavaVersion}, + State, +}; + +pub const JAVA_8_KEY: &str = "JAVA_8"; +pub const JAVA_17_KEY: &str = "JAVA_17"; +pub const JAVA_18PLUS_KEY: &str = "JAVA_18PLUS"; + +// Autodetect JavaSettings default +// Make a guess for what the default Java global settings should be +pub fn autodetect_java_globals() -> crate::Result { + let mut java_8 = find_java8_jres()?; + let mut java_17 = find_java17_jres()?; + let mut java_18plus = find_java18plus_jres()?; + + // Simply select last one found for initial guess + let mut java_globals = JavaGlobals::new(); + if let Some(jre) = java_8.pop() { + java_globals.insert(JAVA_8_KEY.to_string(), jre); + } + if let Some(jre) = java_17.pop() { + java_globals.insert(JAVA_17_KEY.to_string(), jre); + } + if let Some(jre) = java_18plus.pop() { + java_globals.insert(JAVA_18PLUS_KEY.to_string(), jre); + } + + Ok(java_globals) +} + +// Gets the optimal JRE key for the given profile, using Daedalus +// Generally this would be used for profile_create, to get the optimal JRE key +// this can be overwritten by the user a profile-by-profile basis +pub async fn get_optimal_jre_key(profile: &Profile) -> crate::Result { + let state = State::get().await?; + + // Fetch version info from stored profile game_version + let version = state + .metadata + .minecraft + .versions + .iter() + .find(|it| it.id == profile.metadata.game_version.as_ref()) + .ok_or_else(|| { + crate::ErrorKind::LauncherError(format!( + "Invalid or unknown Minecraft version: {}", + profile.metadata.game_version + )) + })?; + + // Get detailed manifest info from Daedalus + let version_info = + download::download_version_info(&state, version, profile.metadata.loader_version.as_ref()).await?; + let optimal_key = match version_info + .java_version + .as_ref() + .map(|it| it.major_version) + .unwrap_or(0) + { + 0..=16 => JAVA_8_KEY.to_string(), + 17 => JAVA_17_KEY.to_string(), + _ => JAVA_18PLUS_KEY.to_string(), + }; + Ok(optimal_key) +} + +// Searches for jres on the system that are 1.18 or higher +pub fn find_java18plus_jres() -> crate::Result> { + let version = extract_java_majorminor_version("1.18")?; + let jres = jre::get_all_jre()?; + // Filter out JREs that are not 1.17 or higher + Ok(jres + .into_iter() + .filter(|jre| { + let jre_version = extract_java_majorminor_version(&jre.version); + if let Ok(jre_version) = jre_version { + jre_version >= version + } else { + false + } + }) + .collect()) +} + +// Searches for jres on the system that are 1.8 exactly +pub fn find_java8_jres() -> crate::Result> { + let version = extract_java_majorminor_version("1.8")?; + let jres = jre::get_all_jre()?; + + // Filter out JREs that are not 1.8 + Ok(jres + .into_iter() + .filter(|jre| { + let jre_version = extract_java_majorminor_version(&jre.version); + if let Ok(jre_version) = jre_version { + jre_version == version + } else { + false + } + }) + .collect()) +} + +// Searches for jres on the system that are 1.17 exactly +pub fn find_java17_jres() -> crate::Result> { + let version = extract_java_majorminor_version("1.17")?; + let jres = jre::get_all_jre()?; + + // Filter out JREs that are not 1.8 + Ok(jres + .into_iter() + .filter(|jre| { + let jre_version = extract_java_majorminor_version(&jre.version); + if let Ok(jre_version) = jre_version { + jre_version == version + } else { + false + } + }) + .collect()) +} + +// Get all JREs that exist on the system +pub fn get_all_jre() -> crate::Result> { + Ok(jre::get_all_jre()?) +} + +pub async fn validate_globals() -> crate::Result { + let state = State::get().await?; + let settings = state.settings.read().await; + Ok(settings.java_globals.is_all_valid()) +} diff --git a/theseus/src/api/mod.rs b/theseus/src/api/mod.rs index 0e1757bd6..de53f69df 100644 --- a/theseus/src/api/mod.rs +++ b/theseus/src/api/mod.rs @@ -1,5 +1,6 @@ //! API for interacting with Theseus pub mod auth; +pub mod jre; pub mod pack; pub mod process; pub mod profile; @@ -18,8 +19,11 @@ pub mod prelude { pub use crate::{ auth::{self, Credentials}, data::*, - pack, process, + jre, pack, process, profile::{self, Profile}, - profile_create, settings, State, + profile_create, settings, + state::JavaGlobals, + util::jre::JavaVersion, + State, }; } diff --git a/theseus/src/api/profile.rs b/theseus/src/api/profile.rs index 7e8c1070e..a1b635ae5 100644 --- a/theseus/src/api/profile.rs +++ b/theseus/src/api/profile.rs @@ -1,10 +1,9 @@ //! Theseus profile management interface -use crate::state::MinecraftChild; +use crate::{launcher::download, state::MinecraftChild}; pub use crate::{ state::{JavaSettings, Profile}, State, }; -use daedalus as d; use std::{ future::Future, path::{Path, PathBuf}, @@ -120,8 +119,8 @@ pub async fn run( profile.metadata.game_version )) })?; - let version_info = d::minecraft::fetch_version_info(version).await?; - + let version_info = + download::download_version_info(&state, version, profile.metadata.loader_version.as_ref()).await?; let pre_launch_hooks = &profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch; for hook in pre_launch_hooks.iter() { @@ -145,29 +144,42 @@ pub async fn run( } } - let java_install = match profile.java { + let java_version = match profile.java { + // Load profile-specific Java implementation choice + // (This defaults to Daedalus-decided key on init, but can be changed by the user) Some(JavaSettings { - install: Some(ref install), + jre_key: Some(ref jre_key), .. - }) => install, - _ => if version_info - .java_version - .as_ref() - .filter(|it| it.major_version >= 16) - .is_some() - { - settings.java_17_path.as_ref() - } else { - settings.java_8_path.as_ref() + }) => settings.java_globals.get(jre_key), + // Fall back to Daedalus-decided key if no profile-specific key is set + _ => { + match version_info + .java_version + .as_ref() + .map(|it| it.major_version) + .unwrap_or(0) + { + 0..=16 => settings + .java_globals + .get(&crate::jre::JAVA_8_KEY.to_string()), + 17 => settings + .java_globals + .get(&crate::jre::JAVA_17_KEY.to_string()), + _ => settings + .java_globals + .get(&crate::jre::JAVA_18PLUS_KEY.to_string()), + } } - .ok_or_else(|| { - crate::ErrorKind::LauncherError(format!( - "No Java installed for version {}", - version_info.java_version.map_or(8, |it| it.major_version), - )) - })?, }; + let java_version = java_version.as_ref().ok_or_else(|| { + crate::ErrorKind::LauncherError(format!( + "No Java stored for version {}", + version_info.java_version.map_or(8, |it| it.major_version), + )) + })?; + // Get the path to the Java executable from the chosen Java implementation key + let java_install: &Path = &PathBuf::from(&java_version.path); if !java_install.exists() { return Err(crate::ErrorKind::LauncherError(format!( "Could not find Java install: {}", @@ -175,7 +187,6 @@ pub async fn run( )) .as_error()); } - let java_args = profile .java .as_ref() diff --git a/theseus/src/api/profile_create.rs b/theseus/src/api/profile_create.rs index f8cd9ef1b..7717200a9 100644 --- a/theseus/src/api/profile_create.rs +++ b/theseus/src/api/profile_create.rs @@ -1,5 +1,5 @@ //! Theseus profile management interface -use crate::prelude::ModLoader; +use crate::{jre, prelude::ModLoader}; pub use crate::{ state::{JavaSettings, Profile}, State, @@ -145,6 +145,20 @@ pub async fn profile_create( } profile.metadata.linked_project_id = linked_project_id; + + // Attempts to find optimal JRE for the profile from the JavaGlobals + // Finds optimal key, and see if key has been set in JavaGlobals + let settings = state.settings.read().await; + let optimal_version_key = jre::get_optimal_jre_key(&profile).await?; + if settings.java_globals.get(&optimal_version_key).is_some() { + profile.set_java_settings(Some(JavaSettings { + jre_key: Some(optimal_version_key), + extra_arguments: None, + }))?; + } else { + println!("Could not detect optimal JRE: {optimal_version_key}, falling back to system default."); + } + { let mut profiles = state.profiles.write().await; profiles.insert(profile)?; diff --git a/theseus/src/config.rs b/theseus/src/config.rs index ca4785210..18a121a24 100644 --- a/theseus/src/config.rs +++ b/theseus/src/config.rs @@ -19,7 +19,6 @@ pub static REQWEST_CLIENT: Lazy = Lazy::new(|| { .unwrap(); headers.insert(reqwest::header::USER_AGENT, header); reqwest::Client::builder() - .timeout(time::Duration::from_secs(15)) .tcp_keepalive(Some(time::Duration::from_secs(10))) .default_headers(headers) .build() diff --git a/theseus/src/error.rs b/theseus/src/error.rs index 84526cf80..004610235 100644 --- a/theseus/src/error.rs +++ b/theseus/src/error.rs @@ -76,6 +76,12 @@ pub enum ErrorKind { #[error("Could not create profile: {0}")] ProfileCreationError(#[from] profile_create::ProfileCreationError), + #[error("JRE error: {0}")] + JREError(#[from] crate::util::jre::JREError), + + #[error("Error parsing date: {0}")] + ChronoParseError(#[from] chrono::ParseError), + #[error("Zip error: {0}")] ZipError(#[from] async_zip::error::ZipError), diff --git a/theseus/src/launcher/mod.rs b/theseus/src/launcher/mod.rs index 76d26b297..a13e4c12f 100644 --- a/theseus/src/launcher/mod.rs +++ b/theseus/src/launcher/mod.rs @@ -8,8 +8,7 @@ use tokio::process::{Child, Command}; mod args; pub mod auth; - -mod download; +pub mod download; #[tracing::instrument] pub fn parse_rule(rule: &d::minecraft::Rule) -> bool { diff --git a/theseus/src/state/auth_task.rs b/theseus/src/state/auth_task.rs index 8f23e765a..639a0e162 100644 --- a/theseus/src/state/auth_task.rs +++ b/theseus/src/state/auth_task.rs @@ -42,7 +42,7 @@ impl AuthTask { // Waits for the task to complete, and returns the credentials let credentials = task - .ok_or_else(|| AuthTaskError::TaskMissing)? + .ok_or(AuthTaskError::TaskMissing)? .await .map_err(AuthTaskError::from)??; diff --git a/theseus/src/state/java_globals.rs b/theseus/src/state/java_globals.rs new file mode 100644 index 000000000..14126d475 --- /dev/null +++ b/theseus/src/state/java_globals.rs @@ -0,0 +1,61 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::PathBuf; + +use crate::prelude::JavaVersion; +use crate::util::jre; + +// All stored Java versions, chosen by the user +// A wrapper over a Hashmap connecting key -> java version +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct JavaGlobals(HashMap); + +impl JavaGlobals { + pub fn new() -> JavaGlobals { + JavaGlobals(HashMap::new()) + } + + pub fn insert(&mut self, key: String, java: JavaVersion) { + self.0.insert(key, java); + } + + pub fn remove(&mut self, key: &String) { + self.0.remove(key); + } + + pub fn get(&self, key: &String) -> Option<&JavaVersion> { + self.0.get(key) + } + + pub fn get_mut(&mut self, key: &String) -> Option<&mut JavaVersion> { + self.0.get_mut(key) + } + + pub fn count(&self) -> usize { + self.0.len() + } + + // Validates that every path here is a valid Java version and that the version matches the version stored here + // If false, when checked, the user should be prompted to reselect the Java version + pub fn is_all_valid(&self) -> bool { + for (_, java) in self.0.iter() { + let jre = jre::check_java_at_filepath( + PathBuf::from(&java.path).as_path(), + ); + if let Some(jre) = jre { + if jre.version != java.version { + return false; + } + } else { + return false; + } + } + true + } +} + +impl Default for JavaGlobals { + fn default() -> Self { + Self::new() + } +} diff --git a/theseus/src/state/mod.rs b/theseus/src/state/mod.rs index 4e95e4fe8..ff39d6c7d 100644 --- a/theseus/src/state/mod.rs +++ b/theseus/src/state/mod.rs @@ -1,5 +1,6 @@ //! Theseus state management system use crate::config::sled_config; +use crate::jre; use std::sync::Arc; use tokio::sync::{Mutex, OnceCell, RwLock, Semaphore}; @@ -31,6 +32,9 @@ pub use self::auth_task::*; mod tags; pub use self::tags::*; +mod java_globals; +pub use self::java_globals::*; + // Global state static LAUNCHER_STATE: OnceCell> = OnceCell::const_new(); pub struct State { @@ -74,7 +78,7 @@ impl State { .open()?; // Settings - let settings = + let mut settings = Settings::init(&directories.settings_file()).await?; // Loose initializations @@ -101,6 +105,12 @@ impl State { ); }; + // On launcher initialization, if global java variables are unset, try to find and set them + // (they are required for the game to launch) + if settings.java_globals.count() == 0 { + settings.java_globals = jre::autodetect_java_globals()?; + } + Ok(Arc::new(Self { database, directories, diff --git a/theseus/src/state/profiles.rs b/theseus/src/state/profiles.rs index fa8c7412a..2bc0b7ca4 100644 --- a/theseus/src/state/profiles.rs +++ b/theseus/src/state/profiles.rs @@ -83,7 +83,7 @@ impl std::fmt::Display for ModLoader { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct JavaSettings { #[serde(skip_serializing_if = "Option::is_none")] - pub install: Option, + pub jre_key: Option, #[serde(skip_serializing_if = "Option::is_none")] pub extra_arguments: Option>, } @@ -146,6 +146,15 @@ impl Profile { } } + #[tracing::instrument] + pub fn set_java_settings( + &mut self, + java: Option, + ) -> crate::Result<()> { + self.java = java; + Ok(()) + } + pub fn get_profile_project_paths(&self) -> crate::Result> { let mut files = Vec::new(); let mut read_paths = |path: &str| { diff --git a/theseus/src/state/projects.rs b/theseus/src/state/projects.rs index 32578016a..2f7b86db4 100644 --- a/theseus/src/state/projects.rs +++ b/theseus/src/state/projects.rs @@ -322,7 +322,9 @@ pub async fn infer_data_from_files( title: Some( pack.display_name .clone() - .unwrap_or(pack.mod_id.clone()), + .unwrap_or_else(|| { + pack.mod_id.clone() + }), ), description: pack.description.clone(), authors: pack diff --git a/theseus/src/state/settings.rs b/theseus/src/state/settings.rs index cbdb2f937..c5fa1c94e 100644 --- a/theseus/src/state/settings.rs +++ b/theseus/src/state/settings.rs @@ -1,11 +1,10 @@ //! Theseus settings file use serde::{Deserialize, Serialize}; -use std::{ - collections::HashSet, - path::{Path, PathBuf}, -}; +use std::{collections::HashSet, path::Path}; use tokio::fs; +use super::JavaGlobals; + // TODO: convert to semver? const CURRENT_FORMAT_VERSION: u32 = 1; @@ -18,8 +17,7 @@ pub struct Settings { pub game_resolution: WindowSize, pub custom_java_args: Vec, pub custom_env_args: Vec<(String, String)>, - pub java_8_path: Option, - pub java_17_path: Option, + pub java_globals: JavaGlobals, pub default_user: Option, pub hooks: Hooks, pub max_concurrent_downloads: usize, @@ -33,8 +31,7 @@ impl Default for Settings { game_resolution: WindowSize::default(), custom_java_args: Vec::new(), custom_env_args: Vec::new(), - java_8_path: None, - java_17_path: None, + java_globals: JavaGlobals::new(), default_user: None, hooks: Hooks::default(), max_concurrent_downloads: 64, diff --git a/theseus/src/state/tags.rs b/theseus/src/state/tags.rs index 5ba452741..a088729b7 100644 --- a/theseus/src/state/tags.rs +++ b/theseus/src/state/tags.rs @@ -140,7 +140,6 @@ impl Tags { let licenses = self.fetch_tag("license"); let donation_platforms = self.fetch_tag("donation_platform"); let report_types = self.fetch_tag("report_type"); - let ( categories, loaders, @@ -241,14 +240,6 @@ pub struct Loader { pub supported_project_types: Vec, } -#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)] -pub struct GameVersion { - pub version: String, - pub version_type: String, - pub date: String, - pub major: bool, -} - #[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)] pub struct License { pub short: String, @@ -260,3 +251,11 @@ pub struct DonationPlatform { pub short: String, pub name: String, } + +#[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)] +pub struct GameVersion { + pub version: String, + pub version_type: String, + pub date: String, + pub major: bool, +} diff --git a/theseus/src/util/jre.rs b/theseus/src/util/jre.rs index 2e8c62b9b..06580196c 100644 --- a/theseus/src/util/jre.rs +++ b/theseus/src/util/jre.rs @@ -1,10 +1,11 @@ use dunce::canonicalize; use lazy_static::lazy_static; use regex::Regex; -use std::collections::HashSet; +use serde::{Deserialize, Serialize}; use std::env; use std::path::PathBuf; use std::process::Command; +use std::{collections::HashSet, path::Path}; #[cfg(target_os = "windows")] use winreg::{ @@ -12,7 +13,7 @@ use winreg::{ RegKey, }; -#[derive(Debug, PartialEq, Eq, Hash)] +#[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)] pub struct JavaVersion { pub path: String, pub version: String, @@ -35,11 +36,8 @@ pub fn get_all_jre() -> Result, JREError> { let Ok(java_subpaths) = std::fs::read_dir(java_path) else {continue }; for java_subpath in java_subpaths { let path = java_subpath?.path(); - if let Some(j) = - check_java_at_filepath(PathBuf::from(path).join("bin")) - { + if let Some(j) = check_java_at_filepath(&path.join("bin")) { jres.insert(j); - break; } } } @@ -90,10 +88,9 @@ pub fn get_all_jre_winregkey( subkey.get_value(subkey_value); let Ok(path) = path else {continue}; if let Some(j) = - check_java_at_filepath(PathBuf::from(path).join("bin")) + check_java_at_filepath(&PathBuf::from(path).join("bin")) { jres.insert(j); - break; } } } @@ -118,12 +115,23 @@ pub fn get_all_jre() -> Result, JREError> { r"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands", ]; for path in java_paths { - if let Some(j) = check_java_at_filepath(PathBuf::from(path).join("bin")) + if let Some(j) = + check_java_at_filepath(&PathBuf::from(path).join("bin")) { jres.insert(j); - break; } } + // Iterate over JavaVirtualMachines/(something)/Contents/Home/bin + let base_path = PathBuf::from("/Library/Java/JavaVirtualMachines/"); + if base_path.is_dir() { + for entry in std::fs::read_dir(base_path)? { + let entry = entry?.path().join("Contents/Home/bin"); + if let Some(j) = check_java_at_filepath(entry.as_path()) { + jres.insert(j); + } + } + } + Ok(jres.into_iter().collect()) } @@ -149,29 +157,29 @@ pub fn get_all_jre() -> Result, JREError> { ]; for path in java_paths { if let Some(j) = - check_java_at_filepath(PathBuf::from(path).join("jre").join("bin")) + check_java_at_filepath(&PathBuf::from(path).join("jre").join("bin")) { jres.insert(j); - break; } - if let Some(j) = check_java_at_filepath(PathBuf::from(path).join("bin")) + if let Some(j) = + check_java_at_filepath(&PathBuf::from(path).join("bin")) { jres.insert(j); - break; } } Ok(jres.into_iter().collect()) } +// Gets all JREs from the PATH env variable #[tracing::instrument] -pub fn get_all_jre_path() -> Result, JREError> { +fn get_all_jre_path() -> Result, JREError> { // Iterate over values in PATH variable, where accessible JREs are referenced let paths = env::var("PATH")?; let paths = env::split_paths(&paths); let mut jres = HashSet::new(); for path in paths { - if let Some(j) = check_java_at_filepath(path) { + if let Some(j) = check_java_at_filepath(&path) { jres.insert(j); } } @@ -189,14 +197,19 @@ const JAVA_BIN: &str = "java"; // For example filepath 'path', attempt to resolve it and get a Java version at this path // If no such path exists, or no such valid java at this path exists, returns None #[tracing::instrument] -pub fn check_java_at_filepath(path: PathBuf) -> Option { +pub fn check_java_at_filepath(path: &Path) -> Option { // Attempt to canonicalize the potential java filepath // If it fails, this path does not exist and None is returned (no Java here) let Ok(path) = canonicalize(path) else { return None }; - let Some(path_str) = path.to_str() else { return None }; // Checks for existence of Java at this filepath - let java = path.join(JAVA_BIN); + // Adds JAVA_BIN to the end of the path if it is not already there + let java = if path.file_name()?.to_str()? != JAVA_BIN { + path.join(JAVA_BIN) + } else { + path + }; + if !java.exists() { return None; }; @@ -216,7 +229,8 @@ pub fn check_java_at_filepath(path: PathBuf) -> Option { // Extract version info from it if let Some(captures) = JAVA_VERSION_CAPTURE.captures(&stderr) { if let Some(version) = captures.get(1) { - let path = path_str.to_string(); + let Some(path) = java.to_str() else { return None }; + let path = path.to_string(); return Some(JavaVersion { path, version: version.as_str().to_string(), @@ -226,6 +240,38 @@ pub fn check_java_at_filepath(path: PathBuf) -> Option { None } +/// Extract major/minor version from a java version string +/// Gets the minor version or an error, and assumes 1 for major version if it could not find +/// "1.8.0_361" -> (1, 8) +/// "20" -> (1, 20) +pub fn extract_java_majorminor_version( + version: &str, +) -> Result<(u32, u32), JREError> { + let mut split = version.split('.'); + let major_opt = split.next(); + + let mut major; + // Try minor. If doesn't exist, in format like "20" so use major + let mut minor = if let Some(minor) = split.next() { + major = major_opt.unwrap_or("1").parse::()?; + minor.parse::()? + } else { + // Formatted like "20", only one value means that is minor version + major = 1; + major_opt + .ok_or_else(|| JREError::InvalidJREVersion(version.to_string()))? + .parse::()? + }; + + // Java start should always be 1. If more than 1, it is formatted like "17.0.1.2" and starts with minor version + if major > 1 { + minor = major; + major = 1; + } + + Ok((major, minor)) +} + #[derive(thiserror::Error, Debug)] pub enum JREError { #[error("Command error : {0}")] @@ -233,4 +279,32 @@ pub enum JREError { #[error("Env error: {0}")] EnvError(#[from] env::VarError), + + #[error("No JRE found for required version: {0}")] + NoJREFound(String), + + #[error("Invalid JRE version string: {0}")] + InvalidJREVersion(String), + + #[error("Parsing error: {0}")] + ParseError(#[from] std::num::ParseIntError), + + #[error("No stored tag for Minecraft Version {0}")] + NoMinecraftVersionFound(String), +} + +#[cfg(test)] +mod tests { + use super::extract_java_majorminor_version; + + #[test] + pub fn java_version_parsing() { + assert_eq!(extract_java_majorminor_version("1.8").unwrap(), (1, 8)); + assert_eq!(extract_java_majorminor_version("17.0.6").unwrap(), (1, 17)); + assert_eq!(extract_java_majorminor_version("20").unwrap(), (1, 20)); + assert_eq!( + extract_java_majorminor_version("1.8.0_361").unwrap(), + (1, 8) + ); + } } diff --git a/theseus_gui/src-tauri/src/api/jre.rs b/theseus_gui/src-tauri/src/api/jre.rs new file mode 100644 index 000000000..708f7850a --- /dev/null +++ b/theseus_gui/src-tauri/src/api/jre.rs @@ -0,0 +1,62 @@ +use std::path::Path; + +use crate::api::Result; +use theseus::prelude::JavaVersion; +use theseus::prelude::*; + +use super::TheseusSerializableError; + +/// Get all JREs that exist on the system +#[tauri::command] +pub async fn jre_get_all_jre() -> Result> { + Ok(jre::get_all_jre()?) +} + +// Finds the isntallation of Java 7, if it exists +#[tauri::command] +pub async fn jre_find_jre_8_jres() -> Result> { + Ok(jre::find_java8_jres()?) +} + +// finds the installation of Java 17, if it exists +#[tauri::command] +pub async fn jre_find_jre_17_jres() -> Result> { + Ok(jre::find_java17_jres()?) +} + +// Finds the highest version of Java 18+, if it exists +#[tauri::command] +pub async fn jre_find_jre_18plus_jres() -> Result> { + Ok(jre::find_java18plus_jres()?) +} + +// Autodetect Java globals, by searching the users computer. +// Returns a *NEW* JavaGlobals that can be put into Settings +#[tauri::command] +pub async fn jre_autodetect_java_globals() -> Result { + Ok(jre::autodetect_java_globals()?) +} + +// Gets key for the optimal JRE to use, for a given profile Profile +// The key can be used in the hashmap contained by JavaGlobals in Settings (if it exists) +#[tauri::command] +pub async fn jre_get_optimal_jre_key(profile: Profile) -> Result { + Ok(jre::get_optimal_jre_key(&profile).await?) +} + +// Gets key for the optimal JRE to use, for a given profile path +// The key can be used in the hashmap contained by JavaGlobals in Settings (if it exists) +#[tauri::command] +pub async fn jre_get_optimal_jre_key_by_path(path: &Path) -> Result { + let profile = profile::get(path).await?.ok_or_else(|| { + TheseusSerializableError::NoProfileFound(path.display().to_string()) + })?; + Ok(jre::get_optimal_jre_key(&profile).await?) +} + +// Validates java globals, by checking if the paths exist +// If false, recommend to direct them to reassign, or to re-guess +#[tauri::command] +pub async fn jre_validate_globals() -> Result { + Ok(jre::validate_globals().await?) +} diff --git a/theseus_gui/src-tauri/src/api/mod.rs b/theseus_gui/src-tauri/src/api/mod.rs index 1c16de8d7..672f62a37 100644 --- a/theseus_gui/src-tauri/src/api/mod.rs +++ b/theseus_gui/src-tauri/src/api/mod.rs @@ -3,6 +3,7 @@ use serde::{Serialize, Serializer}; use thiserror::Error; pub mod auth; +pub mod jre; pub mod pack; pub mod process; @@ -29,6 +30,9 @@ pub enum TheseusSerializableError { #[error("IO error: {0}")] IO(#[from] std::io::Error), + + #[error("No profile found at {0}")] + NoProfileFound(String), } // Generic implementation of From for ErrorTypeA @@ -69,4 +73,5 @@ macro_rules! impl_serialize { impl_serialize! { Theseus, IO, + NoProfileFound, } diff --git a/theseus_gui/src-tauri/src/main.rs b/theseus_gui/src-tauri/src/main.rs index d051e31ea..78847df5f 100644 --- a/theseus_gui/src-tauri/src/main.rs +++ b/theseus_gui/src-tauri/src/main.rs @@ -43,6 +43,14 @@ fn main() { api::tags::tags_get_tag_bundle, api::settings::settings_get, api::settings::settings_set, + api::jre::jre_get_all_jre, + api::jre::jre_autodetect_java_globals, + api::jre::jre_find_jre_18plus_jres, + api::jre::jre_find_jre_17_jres, + api::jre::jre_find_jre_8_jres, + api::jre::jre_validate_globals, + api::jre::jre_get_optimal_jre_key, + api::jre::jre_get_optimal_jre_key_by_path, api::process::process_get_all_pids, api::process::process_get_all_running_pids, api::process::process_get_pids_by_profile_path, diff --git a/theseus_gui/src/helpers/jre.js b/theseus_gui/src/helpers/jre.js new file mode 100644 index 000000000..4799fc6d8 --- /dev/null +++ b/theseus_gui/src/helpers/jre.js @@ -0,0 +1,63 @@ +/** + * All theseus API calls return serialized values (both return values and errors); + * So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized, + * and deserialized into a usable JS object. + */ +import { invoke } from '@tauri-apps/api/tauri' + +/* + +JavaVersion { + path: Path + version: String +} + +*/ + +/// Get all JREs that exist on the system +// Returns an array of JavaVersion +export async function get_all_jre() { + return await invoke('jre_get_all_jre') +} + +// Finds all the installation of Java 7, if it exists +// Returns [JavaVersion] +export async function find_jre_8_jres() { + return await invoke('jre_find_jre_8_jres') +} + +// Finds the installation of Java 17, if it exists +// Returns [JavaVersion] +export async function find_jre_17_jres() { + return await invoke('jre_find_jre_17_jres') +} + +// Finds the highest version of Java 18+, if it exists +// Returns [JavaVersion] +export async function find_jre_18plus_jres() { + return await invoke('jre_find_jre_18plus_jres') +} + +// Validates globals. Recommend directing the user to reassigned the globals if this returns false +// Returns [JavaVersion] +export async function validate_globals() { + return await invoke('jre_validate_globals') +} + +// Gets key for the optimal JRE to use, for a given profile path +// The key can be used in the hashmap contained by JavaGlobals in Settings (if it exists) +export async function get_optimal_jre_key_by_path(path) { + return await invoke('jre_get_optimal_jre_key_by_path', { path }) +} + +// Gets key for the optimal JRE to use, for a given profile +// The key can be used in the hashmap contained by JavaGlobals in Settings (if it exists) +export async function get_optimal_jre_ke(path) { + return await invoke('jre_get_optimal_jre_key', { path }) +} + +// Autodetect Java globals, by searching the users computer. +// Returns a *NEW* JavaGlobals that can be put into Settings +export async function autodetect_java_globals(path) { + return await invoke('jre_autodetect_java_globals', { path }) +} diff --git a/theseus_gui/src/helpers/settings.js b/theseus_gui/src/helpers/settings.js index a3d727bf3..3c745d1c7 100644 --- a/theseus_gui/src/helpers/settings.js +++ b/theseus_gui/src/helpers/settings.js @@ -13,8 +13,7 @@ Settings { "game_resolution": [int int], "custom_java_args": [String ...], "custom_env_args" : [(string, string) ... ]>, - "java_8_path": Path (can be null), - "java_17_path": Path (can be null), + "java_globals": Hash of (string, Path), "default_user": Uuid string (can be null), "hooks": Hooks, "max_concurrent_downloads": uint, diff --git a/theseus_playground/src/main.rs b/theseus_playground/src/main.rs index 88d611818..03bda7763 100644 --- a/theseus_playground/src/main.rs +++ b/theseus_playground/src/main.rs @@ -4,7 +4,6 @@ )] use dunce::canonicalize; -use std::path::Path; use theseus::prelude::*; use tokio::time::{sleep, Duration}; @@ -32,7 +31,7 @@ async fn main() -> theseus::Result<()> { // Initialize state let st = State::get().await?; - st.settings.write().await.max_concurrent_downloads = 100; + st.settings.write().await.max_concurrent_downloads = 1; // Clear profiles println!("Clearing profiles."); @@ -52,13 +51,14 @@ async fn main() -> theseus::Result<()> { // async closure for testing any desired edits // (ie: changing the java runtime of an added profile) - println!("Editing."); - profile::edit(&profile_path, |profile| { - // Eg: Java. TODO: hook up to jre.rs class to pick optimal java - profile.java = Some(JavaSettings { - install: Some(Path::new("/usr/bin/java").to_path_buf()), - extra_arguments: None, - }); + // println!("Editing."); + profile::edit(&profile_path, |_profile| { + // Eg: Java- this would let you change the java runtime of the profile instead of using the default + // use theseus::prelude::jre::JAVA__KEY; + // profile.java = Some(JavaSettings { + // jre_key: Some(JAVA_17_KEY.to_string()), + // extra_arguments: None, + // }); async { Ok(()) } }) .await?; @@ -107,8 +107,8 @@ async fn main() -> theseus::Result<()> { println!("Minecraft PID: {}", pid); // Wait 5 seconds - println!("Waiting 10 seconds to gather logs..."); - sleep(Duration::from_secs(10)).await; + println!("Waiting 20 seconds to gather logs..."); + sleep(Duration::from_secs(20)).await; let stdout = process::get_stdout_by_pid(pid).await?; println!("Logs after 5sec <<< {stdout} >>> end stdout");