* 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
This commit is contained in:
Wyatt Verchere
2023-04-07 13:31:06 -07:00
committed by GitHub
parent 4b41ffbd8a
commit 34005dd2e2
21 changed files with 542 additions and 83 deletions

137
theseus/src/api/jre.rs Normal file
View File

@@ -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<JavaGlobals> {
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<String> {
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<Vec<JavaVersion>> {
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<Vec<JavaVersion>> {
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<Vec<JavaVersion>> {
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<Vec<JavaVersion>> {
Ok(jre::get_all_jre()?)
}
pub async fn validate_globals() -> crate::Result<bool> {
let state = State::get().await?;
let settings = state.settings.read().await;
Ok(settings.java_globals.is_all_valid())
}

View File

@@ -1,5 +1,6 @@
//! API for interacting with Theseus //! API for interacting with Theseus
pub mod auth; pub mod auth;
pub mod jre;
pub mod pack; pub mod pack;
pub mod process; pub mod process;
pub mod profile; pub mod profile;
@@ -18,8 +19,11 @@ pub mod prelude {
pub use crate::{ pub use crate::{
auth::{self, Credentials}, auth::{self, Credentials},
data::*, data::*,
pack, process, jre, pack, process,
profile::{self, Profile}, profile::{self, Profile},
profile_create, settings, State, profile_create, settings,
state::JavaGlobals,
util::jre::JavaVersion,
State,
}; };
} }

View File

@@ -1,10 +1,9 @@
//! Theseus profile management interface //! Theseus profile management interface
use crate::state::MinecraftChild; use crate::{launcher::download, state::MinecraftChild};
pub use crate::{ pub use crate::{
state::{JavaSettings, Profile}, state::{JavaSettings, Profile},
State, State,
}; };
use daedalus as d;
use std::{ use std::{
future::Future, future::Future,
path::{Path, PathBuf}, path::{Path, PathBuf},
@@ -120,8 +119,8 @@ pub async fn run(
profile.metadata.game_version 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 = let pre_launch_hooks =
&profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch; &profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
for hook in pre_launch_hooks.iter() { 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 { Some(JavaSettings {
install: Some(ref install), jre_key: Some(ref jre_key),
.. ..
}) => install, }) => settings.java_globals.get(jre_key),
_ => if version_info // Fall back to Daedalus-decided key if no profile-specific key is set
.java_version _ => {
.as_ref() match version_info
.filter(|it| it.major_version >= 16) .java_version
.is_some() .as_ref()
{ .map(|it| it.major_version)
settings.java_17_path.as_ref() .unwrap_or(0)
} else { {
settings.java_8_path.as_ref() 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() { if !java_install.exists() {
return Err(crate::ErrorKind::LauncherError(format!( return Err(crate::ErrorKind::LauncherError(format!(
"Could not find Java install: {}", "Could not find Java install: {}",
@@ -175,7 +187,6 @@ pub async fn run(
)) ))
.as_error()); .as_error());
} }
let java_args = profile let java_args = profile
.java .java
.as_ref() .as_ref()

View File

@@ -1,5 +1,5 @@
//! Theseus profile management interface //! Theseus profile management interface
use crate::prelude::ModLoader; use crate::{jre, prelude::ModLoader};
pub use crate::{ pub use crate::{
state::{JavaSettings, Profile}, state::{JavaSettings, Profile},
State, State,
@@ -145,6 +145,20 @@ pub async fn profile_create(
} }
profile.metadata.linked_project_id = linked_project_id; 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; let mut profiles = state.profiles.write().await;
profiles.insert(profile)?; profiles.insert(profile)?;

View File

@@ -19,7 +19,6 @@ pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| {
.unwrap(); .unwrap();
headers.insert(reqwest::header::USER_AGENT, header); headers.insert(reqwest::header::USER_AGENT, header);
reqwest::Client::builder() reqwest::Client::builder()
.timeout(time::Duration::from_secs(15))
.tcp_keepalive(Some(time::Duration::from_secs(10))) .tcp_keepalive(Some(time::Duration::from_secs(10)))
.default_headers(headers) .default_headers(headers)
.build() .build()

View File

@@ -76,6 +76,12 @@ pub enum ErrorKind {
#[error("Could not create profile: {0}")] #[error("Could not create profile: {0}")]
ProfileCreationError(#[from] profile_create::ProfileCreationError), 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}")] #[error("Zip error: {0}")]
ZipError(#[from] async_zip::error::ZipError), ZipError(#[from] async_zip::error::ZipError),

View File

@@ -8,8 +8,7 @@ use tokio::process::{Child, Command};
mod args; mod args;
pub mod auth; pub mod auth;
pub mod download;
mod download;
#[tracing::instrument] #[tracing::instrument]
pub fn parse_rule(rule: &d::minecraft::Rule) -> bool { pub fn parse_rule(rule: &d::minecraft::Rule) -> bool {

View File

@@ -42,7 +42,7 @@ impl AuthTask {
// Waits for the task to complete, and returns the credentials // Waits for the task to complete, and returns the credentials
let credentials = task let credentials = task
.ok_or_else(|| AuthTaskError::TaskMissing)? .ok_or(AuthTaskError::TaskMissing)?
.await .await
.map_err(AuthTaskError::from)??; .map_err(AuthTaskError::from)??;

View File

@@ -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<String, JavaVersion>);
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()
}
}

View File

@@ -1,5 +1,6 @@
//! Theseus state management system //! Theseus state management system
use crate::config::sled_config; use crate::config::sled_config;
use crate::jre;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{Mutex, OnceCell, RwLock, Semaphore}; use tokio::sync::{Mutex, OnceCell, RwLock, Semaphore};
@@ -31,6 +32,9 @@ pub use self::auth_task::*;
mod tags; mod tags;
pub use self::tags::*; pub use self::tags::*;
mod java_globals;
pub use self::java_globals::*;
// Global state // Global state
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new(); static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
pub struct State { pub struct State {
@@ -74,7 +78,7 @@ impl State {
.open()?; .open()?;
// Settings // Settings
let settings = let mut settings =
Settings::init(&directories.settings_file()).await?; Settings::init(&directories.settings_file()).await?;
// Loose initializations // 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 { Ok(Arc::new(Self {
database, database,
directories, directories,

View File

@@ -83,7 +83,7 @@ impl std::fmt::Display for ModLoader {
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct JavaSettings { pub struct JavaSettings {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub install: Option<PathBuf>, pub jre_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub extra_arguments: Option<Vec<String>>, pub extra_arguments: Option<Vec<String>>,
} }
@@ -146,6 +146,15 @@ impl Profile {
} }
} }
#[tracing::instrument]
pub fn set_java_settings(
&mut self,
java: Option<JavaSettings>,
) -> crate::Result<()> {
self.java = java;
Ok(())
}
pub fn get_profile_project_paths(&self) -> crate::Result<Vec<PathBuf>> { pub fn get_profile_project_paths(&self) -> crate::Result<Vec<PathBuf>> {
let mut files = Vec::new(); let mut files = Vec::new();
let mut read_paths = |path: &str| { let mut read_paths = |path: &str| {

View File

@@ -322,7 +322,9 @@ pub async fn infer_data_from_files(
title: Some( title: Some(
pack.display_name pack.display_name
.clone() .clone()
.unwrap_or(pack.mod_id.clone()), .unwrap_or_else(|| {
pack.mod_id.clone()
}),
), ),
description: pack.description.clone(), description: pack.description.clone(),
authors: pack authors: pack

View File

@@ -1,11 +1,10 @@
//! Theseus settings file //! Theseus settings file
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{collections::HashSet, path::Path};
collections::HashSet,
path::{Path, PathBuf},
};
use tokio::fs; use tokio::fs;
use super::JavaGlobals;
// TODO: convert to semver? // TODO: convert to semver?
const CURRENT_FORMAT_VERSION: u32 = 1; const CURRENT_FORMAT_VERSION: u32 = 1;
@@ -18,8 +17,7 @@ pub struct Settings {
pub game_resolution: WindowSize, pub game_resolution: WindowSize,
pub custom_java_args: Vec<String>, pub custom_java_args: Vec<String>,
pub custom_env_args: Vec<(String, String)>, pub custom_env_args: Vec<(String, String)>,
pub java_8_path: Option<PathBuf>, pub java_globals: JavaGlobals,
pub java_17_path: Option<PathBuf>,
pub default_user: Option<uuid::Uuid>, pub default_user: Option<uuid::Uuid>,
pub hooks: Hooks, pub hooks: Hooks,
pub max_concurrent_downloads: usize, pub max_concurrent_downloads: usize,
@@ -33,8 +31,7 @@ impl Default for Settings {
game_resolution: WindowSize::default(), game_resolution: WindowSize::default(),
custom_java_args: Vec::new(), custom_java_args: Vec::new(),
custom_env_args: Vec::new(), custom_env_args: Vec::new(),
java_8_path: None, java_globals: JavaGlobals::new(),
java_17_path: None,
default_user: None, default_user: None,
hooks: Hooks::default(), hooks: Hooks::default(),
max_concurrent_downloads: 64, max_concurrent_downloads: 64,

View File

@@ -140,7 +140,6 @@ impl Tags {
let licenses = self.fetch_tag("license"); let licenses = self.fetch_tag("license");
let donation_platforms = self.fetch_tag("donation_platform"); let donation_platforms = self.fetch_tag("donation_platform");
let report_types = self.fetch_tag("report_type"); let report_types = self.fetch_tag("report_type");
let ( let (
categories, categories,
loaders, loaders,
@@ -241,14 +240,6 @@ pub struct Loader {
pub supported_project_types: Vec<String>, pub supported_project_types: Vec<String>,
} }
#[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)] #[derive(Debug, Clone, Decode, Encode, Serialize, Deserialize)]
pub struct License { pub struct License {
pub short: String, pub short: String,
@@ -260,3 +251,11 @@ pub struct DonationPlatform {
pub short: String, pub short: String,
pub name: 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,
}

View File

@@ -1,10 +1,11 @@
use dunce::canonicalize; use dunce::canonicalize;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use std::collections::HashSet; use serde::{Deserialize, Serialize};
use std::env; use std::env;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Command; use std::process::Command;
use std::{collections::HashSet, path::Path};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use winreg::{ use winreg::{
@@ -12,7 +13,7 @@ use winreg::{
RegKey, RegKey,
}; };
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)]
pub struct JavaVersion { pub struct JavaVersion {
pub path: String, pub path: String,
pub version: String, pub version: String,
@@ -35,11 +36,8 @@ pub fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
let Ok(java_subpaths) = std::fs::read_dir(java_path) else {continue }; let Ok(java_subpaths) = std::fs::read_dir(java_path) else {continue };
for java_subpath in java_subpaths { for java_subpath in java_subpaths {
let path = java_subpath?.path(); let path = java_subpath?.path();
if let Some(j) = if let Some(j) = check_java_at_filepath(&path.join("bin")) {
check_java_at_filepath(PathBuf::from(path).join("bin"))
{
jres.insert(j); jres.insert(j);
break;
} }
} }
} }
@@ -90,10 +88,9 @@ pub fn get_all_jre_winregkey(
subkey.get_value(subkey_value); subkey.get_value(subkey_value);
let Ok(path) = path else {continue}; let Ok(path) = path else {continue};
if let Some(j) = 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); jres.insert(j);
break;
} }
} }
} }
@@ -118,12 +115,23 @@ pub fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
r"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands", r"/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands",
]; ];
for path in java_paths { 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); 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()) Ok(jres.into_iter().collect())
} }
@@ -149,29 +157,29 @@ pub fn get_all_jre() -> Result<Vec<JavaVersion>, JREError> {
]; ];
for path in java_paths { for path in java_paths {
if let Some(j) = 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); 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); jres.insert(j);
break;
} }
} }
Ok(jres.into_iter().collect()) Ok(jres.into_iter().collect())
} }
// Gets all JREs from the PATH env variable
#[tracing::instrument] #[tracing::instrument]
pub fn get_all_jre_path() -> Result<HashSet<JavaVersion>, JREError> { fn get_all_jre_path() -> Result<HashSet<JavaVersion>, JREError> {
// Iterate over values in PATH variable, where accessible JREs are referenced // Iterate over values in PATH variable, where accessible JREs are referenced
let paths = env::var("PATH")?; let paths = env::var("PATH")?;
let paths = env::split_paths(&paths); let paths = env::split_paths(&paths);
let mut jres = HashSet::new(); let mut jres = HashSet::new();
for path in paths { 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); 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 // 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 // If no such path exists, or no such valid java at this path exists, returns None
#[tracing::instrument] #[tracing::instrument]
pub fn check_java_at_filepath(path: PathBuf) -> Option<JavaVersion> { pub fn check_java_at_filepath(path: &Path) -> Option<JavaVersion> {
// Attempt to canonicalize the potential java filepath // Attempt to canonicalize the potential java filepath
// If it fails, this path does not exist and None is returned (no Java here) // If it fails, this path does not exist and None is returned (no Java here)
let Ok(path) = canonicalize(path) else { return None }; 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 // 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() { if !java.exists() {
return None; return None;
}; };
@@ -216,7 +229,8 @@ pub fn check_java_at_filepath(path: PathBuf) -> Option<JavaVersion> {
// Extract version info from it // Extract version info from it
if let Some(captures) = JAVA_VERSION_CAPTURE.captures(&stderr) { if let Some(captures) = JAVA_VERSION_CAPTURE.captures(&stderr) {
if let Some(version) = captures.get(1) { 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 { return Some(JavaVersion {
path, path,
version: version.as_str().to_string(), version: version.as_str().to_string(),
@@ -226,6 +240,38 @@ pub fn check_java_at_filepath(path: PathBuf) -> Option<JavaVersion> {
None 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::<u32>()?;
minor.parse::<u32>()?
} 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::<u32>()?
};
// 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)] #[derive(thiserror::Error, Debug)]
pub enum JREError { pub enum JREError {
#[error("Command error : {0}")] #[error("Command error : {0}")]
@@ -233,4 +279,32 @@ pub enum JREError {
#[error("Env error: {0}")] #[error("Env error: {0}")]
EnvError(#[from] env::VarError), 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)
);
}
} }

View File

@@ -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<Vec<JavaVersion>> {
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<Vec<JavaVersion>> {
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<Vec<JavaVersion>> {
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<Vec<JavaVersion>> {
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<JavaGlobals> {
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<String> {
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<String> {
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<bool> {
Ok(jre::validate_globals().await?)
}

View File

@@ -3,6 +3,7 @@ use serde::{Serialize, Serializer};
use thiserror::Error; use thiserror::Error;
pub mod auth; pub mod auth;
pub mod jre;
pub mod pack; pub mod pack;
pub mod process; pub mod process;
@@ -29,6 +30,9 @@ pub enum TheseusSerializableError {
#[error("IO error: {0}")] #[error("IO error: {0}")]
IO(#[from] std::io::Error), IO(#[from] std::io::Error),
#[error("No profile found at {0}")]
NoProfileFound(String),
} }
// Generic implementation of From<T> for ErrorTypeA // Generic implementation of From<T> for ErrorTypeA
@@ -69,4 +73,5 @@ macro_rules! impl_serialize {
impl_serialize! { impl_serialize! {
Theseus, Theseus,
IO, IO,
NoProfileFound,
} }

View File

@@ -43,6 +43,14 @@ fn main() {
api::tags::tags_get_tag_bundle, api::tags::tags_get_tag_bundle,
api::settings::settings_get, api::settings::settings_get,
api::settings::settings_set, 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_pids,
api::process::process_get_all_running_pids, api::process::process_get_all_running_pids,
api::process::process_get_pids_by_profile_path, api::process::process_get_pids_by_profile_path,

View File

@@ -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 })
}

View File

@@ -13,8 +13,7 @@ Settings {
"game_resolution": [int int], "game_resolution": [int int],
"custom_java_args": [String ...], "custom_java_args": [String ...],
"custom_env_args" : [(string, string) ... ]>, "custom_env_args" : [(string, string) ... ]>,
"java_8_path": Path (can be null), "java_globals": Hash of (string, Path),
"java_17_path": Path (can be null),
"default_user": Uuid string (can be null), "default_user": Uuid string (can be null),
"hooks": Hooks, "hooks": Hooks,
"max_concurrent_downloads": uint, "max_concurrent_downloads": uint,

View File

@@ -4,7 +4,6 @@
)] )]
use dunce::canonicalize; use dunce::canonicalize;
use std::path::Path;
use theseus::prelude::*; use theseus::prelude::*;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
@@ -32,7 +31,7 @@ async fn main() -> theseus::Result<()> {
// Initialize state // Initialize state
let st = State::get().await?; let st = State::get().await?;
st.settings.write().await.max_concurrent_downloads = 100; st.settings.write().await.max_concurrent_downloads = 1;
// Clear profiles // Clear profiles
println!("Clearing profiles."); println!("Clearing profiles.");
@@ -52,13 +51,14 @@ async fn main() -> theseus::Result<()> {
// async closure for testing any desired edits // async closure for testing any desired edits
// (ie: changing the java runtime of an added profile) // (ie: changing the java runtime of an added profile)
println!("Editing."); // println!("Editing.");
profile::edit(&profile_path, |profile| { profile::edit(&profile_path, |_profile| {
// Eg: Java. TODO: hook up to jre.rs class to pick optimal java // Eg: Java- this would let you change the java runtime of the profile instead of using the default
profile.java = Some(JavaSettings { // use theseus::prelude::jre::JAVA__KEY;
install: Some(Path::new("/usr/bin/java").to_path_buf()), // profile.java = Some(JavaSettings {
extra_arguments: None, // jre_key: Some(JAVA_17_KEY.to_string()),
}); // extra_arguments: None,
// });
async { Ok(()) } async { Ok(()) }
}) })
.await?; .await?;
@@ -107,8 +107,8 @@ async fn main() -> theseus::Result<()> {
println!("Minecraft PID: {}", pid); println!("Minecraft PID: {}", pid);
// Wait 5 seconds // Wait 5 seconds
println!("Waiting 10 seconds to gather logs..."); println!("Waiting 20 seconds to gather logs...");
sleep(Duration::from_secs(10)).await; sleep(Duration::from_secs(20)).await;
let stdout = process::get_stdout_by_pid(pid).await?; let stdout = process::get_stdout_by_pid(pid).await?;
println!("Logs after 5sec <<< {stdout} >>> end stdout"); println!("Logs after 5sec <<< {stdout} >>> end stdout");