diff --git a/Cargo.lock b/Cargo.lock index 2a1c0c54f6..04ef3790d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8889,6 +8889,7 @@ dependencies = [ "flate2", "fs4", "futures", + "hashlink", "hickory-resolver", "indicatif", "notify", diff --git a/Cargo.toml b/Cargo.toml index b612f75ae1..7bee1888b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ flate2 = "1.1.2" fs4 = { version = "0.13.1", default-features = false } futures = { version = "0.3.31", default-features = false } futures-util = "0.3.31" +hashlink = "0.10.0" hex = "0.4.3" hickory-resolver = "0.25.2" hmac = "0.12.1" diff --git a/apps/app-frontend/src/components/ui/JavaSelector.vue b/apps/app-frontend/src/components/ui/JavaSelector.vue index 4a82794299..9beea538a7 100644 --- a/apps/app-frontend/src/components/ui/JavaSelector.vue +++ b/apps/app-frontend/src/components/ui/JavaSelector.vue @@ -127,7 +127,7 @@ async function handleJavaFileInput() { const filePath = await open() if (filePath) { - let result = await get_jre(filePath.path ?? filePath) + let result = await get_jre(filePath.path ?? filePath).catch(handleError) if (!result) { result = { path: filePath.path ?? filePath, diff --git a/apps/app/src/api/jre.rs b/apps/app/src/api/jre.rs index 036d5889b2..71c72257cf 100644 --- a/apps/app/src/api/jre.rs +++ b/apps/app/src/api/jre.rs @@ -41,8 +41,8 @@ pub async fn jre_find_filtered_jres( // Validates JRE at a given path // Returns None if the path is not a valid JRE #[tauri::command] -pub async fn jre_get_jre(path: PathBuf) -> Result> { - jre::check_jre(path).await.map_err(|e| e.into()) +pub async fn jre_get_jre(path: PathBuf) -> Result { + Ok(jre::check_jre(path).await?) } // Tests JRE of a certain version diff --git a/apps/app/tauri.conf.json b/apps/app/tauri.conf.json index 72e380d8da..8223c313ad 100644 --- a/apps/app/tauri.conf.json +++ b/apps/app/tauri.conf.json @@ -30,7 +30,6 @@ "providerShortName": null, "signingIdentity": null }, - "resources": [], "shortDescription": "", "linux": { "deb": { diff --git a/packages/app-lib/Cargo.toml b/packages/app-lib/Cargo.toml index e9ee7f6bfe..179e887840 100644 --- a/packages/app-lib/Cargo.toml +++ b/packages/app-lib/Cargo.toml @@ -23,6 +23,7 @@ quick-xml = { workspace = true, features = ["async-tokio"] } enumset.workspace = true chardetng.workspace = true encoding_rs.workspace = true +hashlink.workspace = true chrono = { workspace = true, features = ["serde"] } daedalus.workspace = true diff --git a/packages/app-lib/library/JavaInfo.java b/packages/app-lib/library/JavaInfo.java index 542fc60782..b10ed93633 100644 --- a/packages/app-lib/library/JavaInfo.java +++ b/packages/app-lib/library/JavaInfo.java @@ -1,22 +1,22 @@ public final class JavaInfo { - private static final String[] CHECKED_PROPERTIES = new String[] { - "os.arch", - "java.version" - }; + private static final String[] CHECKED_PROPERTIES = new String[] { + "os.arch", + "java.version" + }; - public static void main(String[] args) { - int returnCode = 0; + public static void main(String[] args) { + int returnCode = 0; - for (String key : CHECKED_PROPERTIES) { - String property = System.getProperty(key); + for (String key : CHECKED_PROPERTIES) { + String property = System.getProperty(key); - if (property != null) { - System.out.println(key + "=" + property); - } else { - returnCode = 1; - } - } - - System.exit(returnCode); + if (property != null) { + System.out.println(key + "=" + property); + } else { + returnCode = 1; + } } -} \ No newline at end of file + + System.exit(returnCode); + } +} diff --git a/packages/app-lib/library/MinecraftLaunch.class b/packages/app-lib/library/MinecraftLaunch.class new file mode 100644 index 0000000000..4d3af86e44 Binary files /dev/null and b/packages/app-lib/library/MinecraftLaunch.class differ diff --git a/packages/app-lib/library/MinecraftLaunch.java b/packages/app-lib/library/MinecraftLaunch.java new file mode 100644 index 0000000000..2ed3cb8201 --- /dev/null +++ b/packages/app-lib/library/MinecraftLaunch.java @@ -0,0 +1,118 @@ +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; + +public final class MinecraftLaunch { + public static void main(String[] args) throws IOException, ReflectiveOperationException { + final String mainClass = args[0]; + final String[] gameArgs = Arrays.copyOfRange(args, 1, args.length); + + System.setProperty("modrinth.process.args", String.join("\u001f", gameArgs)); + parseInput(); + + relaunch(mainClass, gameArgs); + } + + private static void parseInput() throws IOException { + final ByteArrayOutputStream line = new ByteArrayOutputStream(); + while (true) { + final int b = System.in.read(); + if (b < 0) { + throw new IllegalStateException("Stdin terminated while parsing"); + } + if (b != '\n') { + line.write(b); + continue; + } + if (handleLine(line.toString("UTF-8"))) { + break; + } + line.reset(); + } + } + + private static boolean handleLine(String line) { + final String[] parts = line.split("\t", 2); + switch (parts[0]) { + case "property": { + final String[] keyValue = parts[1].split("\t", 2); + System.setProperty(keyValue[0], keyValue[1]); + return false; + } + case "launch": + return true; + } + + System.err.println("Unknown input line " + line); + return false; + } + + private static void relaunch(String mainClassName, String[] args) throws ReflectiveOperationException { + final int javaVersion = getJavaVersion(); + final Class mainClass = Class.forName(mainClassName); + + if (javaVersion >= 25) { + Method mainMethod; + try { + mainMethod = findMainMethodJep512(mainClass); + } catch (ReflectiveOperationException e) { + System.err + .println("[MODRINTH] Unable to call JDK findMainMethod. Falling back to pre-Java 25 main method finding."); + // If the above fails due to JDK implementation details changing + try { + mainMethod = findSimpleMainMethod(mainClass); + } catch (ReflectiveOperationException innerE) { + e.addSuppressed(innerE); + throw e; + } + } + if (mainMethod == null) { + throw new IllegalArgumentException("Could not find main() method"); + } + + Object thisObject = null; + if (!Modifier.isStatic(mainMethod.getModifiers())) { + thisObject = mainClass.getDeclaredConstructor().newInstance(); + } + + final Object[] parameters = mainMethod.getParameterCount() > 0 + ? new Object[] { args } + : new Object[] {}; + + mainMethod.invoke(thisObject, parameters); + } else { + findSimpleMainMethod(mainClass).invoke(null, new Object[] { args }); + } + } + + private static int getJavaVersion() { + String javaVersion = System.getProperty("java.version"); + + final int dotIndex = javaVersion.indexOf('.'); + if (dotIndex != -1) { + javaVersion = javaVersion.substring(0, dotIndex); + } + + final int dashIndex = javaVersion.indexOf('-'); + if (dashIndex != -1) { + javaVersion = javaVersion.substring(0, dashIndex); + } + + return Integer.parseInt(javaVersion); + } + + private static Method findMainMethodJep512(Class mainClass) throws ReflectiveOperationException { + // BEWARE BELOW: This code may break if JDK internals to find the main method + // change. + final Class methodFinderClass = Class.forName("jdk.internal.misc.MethodFinder"); + final Method methodFinderMethod = methodFinderClass.getDeclaredMethod("findMainMethod", Class.class); + final Object result = methodFinderMethod.invoke(null, mainClass); + return (Method) result; + } + + private static Method findSimpleMainMethod(Class mainClass) throws NoSuchMethodException { + return mainClass.getMethod("main", String[].class); + } +} diff --git a/packages/app-lib/src/api/jre.rs b/packages/app-lib/src/api/jre.rs index 1f6d8baa79..d416a3e2e4 100644 --- a/packages/app-lib/src/api/jre.rs +++ b/packages/app-lib/src/api/jre.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; use sysinfo::{MemoryRefreshKind, RefreshKind}; use crate::util::io; -use crate::util::jre::extract_java_majorminor_version; +use crate::util::jre::extract_java_version; use crate::{ LoadingBarType, State, util::jre::{self}, @@ -38,9 +38,9 @@ pub async fn find_filtered_jres( Ok(if let Some(java_version) = java_version { jres.into_iter() .filter(|jre| { - let jre_version = extract_java_majorminor_version(&jre.version); + let jre_version = extract_java_version(&jre.version); if let Ok(jre_version) = jre_version { - jre_version.1 == java_version + jre_version == java_version } else { false } @@ -157,8 +157,8 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result { } // Validates JRE at a given at a given path -pub async fn check_jre(path: PathBuf) -> crate::Result> { - Ok(jre::check_java_at_filepath(&path).await) +pub async fn check_jre(path: PathBuf) -> crate::Result { + jre::check_java_at_filepath(&path).await } // Test JRE at a given path @@ -166,11 +166,11 @@ pub async fn test_jre( path: PathBuf, major_version: u32, ) -> crate::Result { - let Some(jre) = jre::check_java_at_filepath(&path).await else { + let Ok(jre) = jre::check_java_at_filepath(&path).await else { return Ok(false); }; - let (major, _) = extract_java_majorminor_version(&jre.version)?; - Ok(major == major_version) + let version = extract_java_version(&jre.version)?; + Ok(version == major_version) } // Gets maximum memory in KiB. diff --git a/packages/app-lib/src/launcher/args.rs b/packages/app-lib/src/launcher/args.rs index 4ab3100729..0884fb5747 100644 --- a/packages/app-lib/src/launcher/args.rs +++ b/packages/app-lib/src/launcher/args.rs @@ -13,7 +13,7 @@ use daedalus::{ modded::SidedDataEntry, }; use dunce::canonicalize; -use std::collections::HashSet; +use hashlink::LinkedHashSet; use std::io::{BufRead, BufReader}; use std::{collections::HashMap, path::Path}; use uuid::Uuid; @@ -24,7 +24,7 @@ const TEMPORARY_REPLACE_CHAR: &str = "\n"; pub fn get_class_paths( libraries_path: &Path, libraries: &[Library], - client_path: &Path, + launcher_class_path: &[&Path], java_arch: &str, minecraft_updated: bool, ) -> crate::Result { @@ -48,20 +48,22 @@ pub fn get_class_paths( Some(get_lib_path(libraries_path, &library.name, false)) }) - .collect::, _>>()?; - - cps.insert( - canonicalize(client_path) - .map_err(|_| { - crate::ErrorKind::LauncherError(format!( - "Specified class path {} does not exist", - client_path.to_string_lossy() - )) - .as_error() - })? - .to_string_lossy() - .to_string(), - ); + .collect::, _>>()?; + + for launcher_path in launcher_class_path { + cps.insert( + canonicalize(launcher_path) + .map_err(|_| { + crate::ErrorKind::LauncherError(format!( + "Specified class path {} does not exist", + launcher_path.to_string_lossy() + )) + .as_error() + })? + .to_string_lossy() + .to_string(), + ); + } Ok(cps .into_iter() diff --git a/packages/app-lib/src/launcher/mod.rs b/packages/app-lib/src/launcher/mod.rs index 09139b10db..3f91b261f7 100644 --- a/packages/app-lib/src/launcher/mod.rs +++ b/packages/app-lib/src/launcher/mod.rs @@ -9,7 +9,7 @@ use crate::state::{ Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage, }; use crate::util::io; -use crate::{State, process, state as st}; +use crate::{State, get_resource_file, process, state as st}; use chrono::Utc; use daedalus as d; use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo}; @@ -19,6 +19,7 @@ use serde::Deserialize; use st::Profile; use std::fmt::Write; use std::path::PathBuf; +use tokio::io::AsyncWriteExt; use tokio::process::Command; mod args; @@ -124,12 +125,10 @@ pub async fn get_java_version_from_profile( version_info: &VersionInfo, ) -> crate::Result> { if let Some(java) = profile.java_path.as_ref() { - let java = crate::api::jre::check_jre(std::path::PathBuf::from(java)) - .await - .ok() - .flatten(); + let java = + crate::api::jre::check_jre(std::path::PathBuf::from(java)).await; - if let Some(java) = java { + if let Ok(java) = java { return Ok(Some(java)); } } @@ -289,13 +288,7 @@ pub async fn install_minecraft( }; // Test jre version - let java_version = crate::api::jre::check_jre(java_version.clone()) - .await? - .ok_or_else(|| { - crate::ErrorKind::LauncherError(format!( - "Java path invalid or non-functional: {java_version:?}" - )) - })?; + let java_version = crate::api::jre::check_jre(java_version.clone()).await?; if set_java { java_version.upsert(&state.pool).await?; @@ -560,14 +553,7 @@ pub async fn launch_minecraft( // Test jre version let java_version = - crate::api::jre::check_jre(java_version.path.clone().into()) - .await? - .ok_or_else(|| { - crate::ErrorKind::LauncherError(format!( - "Java path invalid or non-functional: {}", - java_version.path - )) - })?; + crate::api::jre::check_jre(java_version.path.clone().into()).await?; let client_path = state .directories @@ -603,33 +589,43 @@ pub async fn launch_minecraft( io::create_dir_all(&natives_dir).await?; } - command - .args( - args::get_jvm_arguments( - args.get(&d::minecraft::ArgumentType::Jvm) - .map(|x| x.as_slice()), - &natives_dir, + let (main_class_keep_alive, main_class_path) = + get_resource_file!("../../library" / "MinecraftLaunch.class")?; + + command.args( + args::get_jvm_arguments( + args.get(&d::minecraft::ArgumentType::Jvm) + .map(|x| x.as_slice()), + &natives_dir, + &state.directories.libraries_dir(), + &state.directories.log_configs_dir(), + &args::get_class_paths( &state.directories.libraries_dir(), - &state.directories.log_configs_dir(), - &args::get_class_paths( - &state.directories.libraries_dir(), - version_info.libraries.as_slice(), - &client_path, - &java_version.architecture, - minecraft_updated, - )?, - &version_jar, - *memory, - Vec::from(java_args), + version_info.libraries.as_slice(), + &[main_class_path.parent().unwrap(), &client_path], &java_version.architecture, - quick_play_type, - version_info - .logging - .as_ref() - .and_then(|x| x.get(&LoggingSide::Client)), - )? - .into_iter(), - ) + minecraft_updated, + )?, + &version_jar, + *memory, + Vec::from(java_args), + &java_version.architecture, + quick_play_type, + version_info + .logging + .as_ref() + .and_then(|x| x.get(&LoggingSide::Client)), + )? + .into_iter(), + ); + + // The java launcher code requires internal JDK code in Java 25+ in order to support JEP 512 + if java_version.parsed_version >= 25 { + command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED"); + } + + command + .arg("MinecraftLaunch") .arg(version_info.main_class.clone()) .args( args::get_minecraft_arguments( @@ -744,6 +740,40 @@ pub async fn launch_minecraft( post_exit_hook, state.directories.profile_logs_dir(&profile.path), version_info.logging.is_some(), + main_class_keep_alive, + async |process: &ProcessMetadata, stdin| { + let process_start_time = process.start_time.to_rfc3339(); + let profile_created_time = profile.created.to_rfc3339(); + let profile_modified_time = profile.modified.to_rfc3339(); + let system_properties = [ + ("modrinth.process.startTime", Some(&process_start_time)), + ("modrinth.profile.created", Some(&profile_created_time)), + ("modrinth.profile.icon", profile.icon_path.as_ref()), + ( + "modrinth.profile.link.project", + profile.linked_data.as_ref().map(|x| &x.project_id), + ), + ( + "modrinth.profile.link.version", + profile.linked_data.as_ref().map(|x| &x.version_id), + ), + ("modrinth.profile.modified", Some(&profile_modified_time)), + ("modrinth.profile.name", Some(&profile.name)), + ]; + for (key, value) in system_properties { + let Some(value) = value else { + continue; + }; + stdin.write_all(b"property\t").await?; + stdin.write_all(key.as_bytes()).await?; + stdin.write_u8(b'\t').await?; + stdin.write_all(value.as_bytes()).await?; + stdin.write_u8(b'\n').await?; + } + stdin.write_all(b"launch\n").await?; + stdin.flush().await?; + Ok(()) + }, ) .await } diff --git a/packages/app-lib/src/state/java_globals.rs b/packages/app-lib/src/state/java_globals.rs index e1fd2a0797..64d449e19a 100644 --- a/packages/app-lib/src/state/java_globals.rs +++ b/packages/app-lib/src/state/java_globals.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Clone)] pub struct JavaVersion { - pub major_version: u32, + pub parsed_version: u32, pub version: String, pub architecture: String, pub path: String, @@ -30,7 +30,7 @@ impl JavaVersion { .await?; Ok(res.map(|x| JavaVersion { - major_version, + parsed_version: major_version, version: x.full_version, architecture: x.architecture, path: x.path, @@ -52,7 +52,7 @@ impl JavaVersion { acc.insert( x.major_version as u32, JavaVersion { - major_version: x.major_version as u32, + parsed_version: x.major_version as u32, version: x.full_version, architecture: x.architecture, path: x.path, @@ -70,7 +70,7 @@ impl JavaVersion { &self, exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>, ) -> crate::Result<()> { - let major_version = self.major_version as i32; + let major_version = self.parsed_version as i32; sqlx::query!( " diff --git a/packages/app-lib/src/state/legacy_converter.rs b/packages/app-lib/src/state/legacy_converter.rs index 0d4a680463..9aa563a507 100644 --- a/packages/app-lib/src/state/legacy_converter.rs +++ b/packages/app-lib/src/state/legacy_converter.rs @@ -83,7 +83,7 @@ where settings.prev_custom_dir = Some(old_launcher_root_str.clone()); for (_, legacy_version) in legacy_settings.java_globals.0 { - if let Ok(Some(java_version)) = + if let Ok(java_version) = check_jre(PathBuf::from(legacy_version.path)).await { java_version.upsert(exec).await?; diff --git a/packages/app-lib/src/state/process.rs b/packages/app-lib/src/state/process.rs index 852de5991f..a4727468c1 100644 --- a/packages/app-lib/src/state/process.rs +++ b/packages/app-lib/src/state/process.rs @@ -8,12 +8,14 @@ use quick_xml::Reader; use quick_xml::events::Event; use serde::Deserialize; use serde::Serialize; +use std::fmt::Debug; use std::fs::OpenOptions; use std::io::Write; use std::path::{Path, PathBuf}; use std::process::ExitStatus; +use tempfile::TempDir; use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::process::{Child, Command}; +use tokio::process::{Child, ChildStdin, Command}; use uuid::Uuid; const LAUNCHER_LOG_PATH: &str = "launcher_log.txt"; @@ -35,6 +37,7 @@ impl ProcessManager { } } + #[allow(clippy::too_many_arguments)] pub async fn insert_new_process( &self, profile_path: &str, @@ -42,24 +45,42 @@ impl ProcessManager { post_exit_command: Option, logs_folder: PathBuf, xml_logging: bool, + main_class_keep_alive: TempDir, + post_process_init: impl AsyncFnOnce( + &ProcessMetadata, + &mut ChildStdin, + ) -> crate::Result<()>, ) -> crate::Result { mc_command.stdout(std::process::Stdio::piped()); mc_command.stderr(std::process::Stdio::piped()); + mc_command.stdin(std::process::Stdio::piped()); let mut mc_proc = mc_command.spawn().map_err(IOError::from)?; let stdout = mc_proc.stdout.take(); let stderr = mc_proc.stderr.take(); - let process = Process { + let mut process = Process { metadata: ProcessMetadata { uuid: Uuid::new_v4(), start_time: Utc::now(), profile_path: profile_path.to_string(), }, child: mc_proc, + _main_class_keep_alive: main_class_keep_alive, }; + if let Err(e) = post_process_init( + &process.metadata, + &mut process.child.stdin.as_mut().unwrap(), + ) + .await + { + tracing::error!("Failed to run post-process init: {e}"); + let _ = process.child.kill().await; + return Err(e); + } + let metadata = process.metadata.clone(); if !logs_folder.exists() { @@ -193,6 +214,7 @@ pub struct ProcessMetadata { struct Process { metadata: ProcessMetadata, child: Child, + _main_class_keep_alive: TempDir, } #[derive(Debug, Default)] diff --git a/packages/app-lib/src/util/io.rs b/packages/app-lib/src/util/io.rs index 0079f48099..c6abf9a0f0 100644 --- a/packages/app-lib/src/util/io.rs +++ b/packages/app-lib/src/util/io.rs @@ -2,7 +2,6 @@ // A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error. use std::{io::Write, path::Path}; - use tempfile::NamedTempFile; use tokio::task::spawn_blocking; @@ -299,3 +298,36 @@ pub async fn metadata( path: path.to_string_lossy().to_string(), }) } + +/// Gets a resource file from the executable. Returns `theseus::Result<(TempDir, PathBuf)>`. +#[macro_export] +macro_rules! get_resource_file { + ($relative_dir:literal / $file_name:literal) => { + 'get_resource_file: { + let dir = match tempfile::tempdir() { + Ok(dir) => dir, + Err(e) => { + break 'get_resource_file $crate::Result::Err( + $crate::util::io::IOError::from(e).into(), + ); + } + }; + let path = dir.path().join($file_name); + if let Err(e) = $crate::util::io::write( + &path, + include_bytes!(concat!($relative_dir, "/", $file_name)), + ) + .await + { + break 'get_resource_file $crate::Result::Err(e.into()); + } + let path = match $crate::util::io::canonicalize(path) { + Ok(path) => path, + Err(e) => { + break 'get_resource_file $crate::Result::Err(e.into()); + } + }; + $crate::Result::Ok((dir, path)) + } + }; +} diff --git a/packages/app-lib/src/util/jre.rs b/packages/app-lib/src/util/jre.rs index 781b54a37b..1c5e426538 100644 --- a/packages/app-lib/src/util/jre.rs +++ b/packages/app-lib/src/util/jre.rs @@ -7,7 +7,7 @@ use std::process::Command; use std::{collections::HashSet, path::Path}; use tokio::task::JoinError; -use crate::State; +use crate::{State, get_resource_file}; #[cfg(target_os = "windows")] use winreg::{ RegKey, @@ -183,7 +183,6 @@ pub async fn get_all_jre() -> Result, JREError> { // Gets all JREs from the PATH env variable #[tracing::instrument] - async fn get_all_autoinstalled_jre_path() -> Result, JREError> { Box::pin(async move { @@ -239,54 +238,49 @@ pub const JAVA_BIN: &str = if cfg!(target_os = "windows") { pub async fn check_java_at_filepaths( paths: HashSet, ) -> HashSet { - let jres = stream::iter(paths.into_iter()) + stream::iter(paths.into_iter()) .map(|p: PathBuf| { tokio::task::spawn(async move { check_java_at_filepath(&p).await }) }) .buffer_unordered(64) - .collect::>() - .await; - - jres.into_iter().filter_map(|x| x.ok()).flatten().collect() + .filter_map(async |x| x.ok().and_then(Result::ok)) + .collect() + .await } // 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 async fn check_java_at_filepath(path: &Path) -> Option { +pub async fn check_java_at_filepath(path: &Path) -> crate::Result { // 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) = io::canonicalize(path) else { - return None; - }; + let path = io::canonicalize(path)?; // Checks for existence of Java at this filepath // 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 { + let java = if path + .file_name() + .and_then(|x| x.to_str()) + .is_some_and(|x| x != JAVA_BIN) + { path.join(JAVA_BIN) } else { path }; if !java.exists() { - return None; + return Err(JREError::NoExecutable(java).into()); }; - let bytes = include_bytes!("../../library/JavaInfo.class"); - let Ok(tempdir) = tempfile::tempdir() else { - return None; - }; - let file_path = tempdir.path().join("JavaInfo.class"); - io::write(&file_path, bytes).await.ok()?; + let (_temp, file_path) = + get_resource_file!("../../library" / "JavaInfo.class")?; let output = Command::new(&java) .arg("-cp") .arg(file_path.parent().unwrap()) .arg("JavaInfo") .env_remove("_JAVA_OPTIONS") - .output() - .ok()?; + .output()?; let stdout = String::from_utf8_lossy(&output.stdout); @@ -308,64 +302,49 @@ pub async fn check_java_at_filepath(path: &Path) -> Option { // Extract version info from it if let Some(arch) = java_arch { if let Some(version) = java_version { - if let Ok((_, major_version)) = - extract_java_majorminor_version(version) - { + if let Ok(version) = extract_java_version(version) { let path = java.to_string_lossy().to_string(); - return Some(JavaVersion { - major_version, + return Ok(JavaVersion { + parsed_version: version, path, version: version.to_string(), architecture: arch.to_string(), }); } + + return Err(JREError::InvalidJREVersion(version.to_owned()).into()); } } - None + + Err(JREError::FailedJavaCheck(java).into()) } -/// 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> { +pub fn extract_java_version(version: &str) -> Result { 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; + let version = split.next().unwrap(); + let version = version.split_once('-').map_or(version, |(x, _)| x); + let mut version = version.parse::()?; + if version == 1 { + version = split.next().map_or(Ok(1), |x| x.parse::())?; } - Ok((major, minor)) + Ok(version) } #[derive(thiserror::Error, Debug)] pub enum JREError { - #[error("Command error : {0}")] + #[error("Command error: {0}")] IOError(#[from] std::io::Error), #[error("Env error: {0}")] EnvError(#[from] env::VarError), - #[error("No JRE found for required version: {0}")] - NoJREFound(String), + #[error("No executable found at {0}")] + NoExecutable(PathBuf), + + #[error("Could not check Java version at path {0}")] + FailedJavaCheck(PathBuf), #[error("Invalid JRE version string: {0}")] InvalidJREVersion(String), @@ -376,9 +355,9 @@ pub enum JREError { #[error("Join error: {0}")] JoinError(#[from] JoinError), - #[error("No stored tag for Minecraft Version {0}")] + #[error("No stored tag for Minecraft version {0}")] NoMinecraftVersionFound(String), - #[error("Error getting launcher sttae")] + #[error("Error getting launcher state")] StateError, }