Pass system properties into Minecraft (+ some launch code cleanup) (#3822)
* Create get_resource_file macro to get an embedded resource If the tauri feature is enabled, the resource will be loaded from Tauri resources. If the tauri feature is disabled, the resource will be extracted to a temp directory. * Wrap process execution to inject system properties through stdin * Pass the time values as ISO 8601 datetimes * Remove entirely internal modrinth.process.uuid * Redo Java version checking somewhat and fix a few bugs with it * Fix game launch with early access versions of Java * Format Java code * Fix modrinth.profile.modified being the same as modrinth.profile.created * Revert to manually extracting class files
This commit is contained in:
parent
569d60cb57
commit
f10e0f2bf1
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -8889,6 +8889,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"fs4",
|
||||
"futures",
|
||||
"hashlink",
|
||||
"hickory-resolver",
|
||||
"indicatif",
|
||||
"notify",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<Option<JavaVersion>> {
|
||||
jre::check_jre(path).await.map_err(|e| e.into())
|
||||
pub async fn jre_get_jre(path: PathBuf) -> Result<JavaVersion> {
|
||||
Ok(jre::check_jre(path).await?)
|
||||
}
|
||||
|
||||
// Tests JRE of a certain version
|
||||
|
||||
@ -30,7 +30,6 @@
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"linux": {
|
||||
"deb": {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.exit(returnCode);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
packages/app-lib/library/MinecraftLaunch.class
Normal file
BIN
packages/app-lib/library/MinecraftLaunch.class
Normal file
Binary file not shown.
118
packages/app-lib/library/MinecraftLaunch.java
Normal file
118
packages/app-lib/library/MinecraftLaunch.java
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<PathBuf> {
|
||||
}
|
||||
|
||||
// Validates JRE at a given at a given path
|
||||
pub async fn check_jre(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
|
||||
Ok(jre::check_java_at_filepath(&path).await)
|
||||
pub async fn check_jre(path: PathBuf) -> crate::Result<JavaVersion> {
|
||||
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<bool> {
|
||||
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.
|
||||
|
||||
@ -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<String> {
|
||||
@ -48,20 +48,22 @@ pub fn get_class_paths(
|
||||
|
||||
Some(get_lib_path(libraries_path, &library.name, false))
|
||||
})
|
||||
.collect::<Result<HashSet<_>, _>>()?;
|
||||
.collect::<Result<LinkedHashSet<_>, _>>()?;
|
||||
|
||||
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(),
|
||||
);
|
||||
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()
|
||||
|
||||
@ -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<Option<JavaVersion>> {
|
||||
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
|
||||
}
|
||||
|
||||
@ -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!(
|
||||
"
|
||||
|
||||
@ -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?;
|
||||
|
||||
@ -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<String>,
|
||||
logs_folder: PathBuf,
|
||||
xml_logging: bool,
|
||||
main_class_keep_alive: TempDir,
|
||||
post_process_init: impl AsyncFnOnce(
|
||||
&ProcessMetadata,
|
||||
&mut ChildStdin,
|
||||
) -> crate::Result<()>,
|
||||
) -> crate::Result<ProcessMetadata> {
|
||||
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)]
|
||||
|
||||
@ -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))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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<Vec<JavaVersion>, JREError> {
|
||||
|
||||
// Gets all JREs from the PATH env variable
|
||||
#[tracing::instrument]
|
||||
|
||||
async fn get_all_autoinstalled_jre_path() -> Result<HashSet<PathBuf>, 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<PathBuf>,
|
||||
) -> HashSet<JavaVersion> {
|
||||
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::<Vec<_>>()
|
||||
.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<JavaVersion> {
|
||||
pub async fn check_java_at_filepath(path: &Path) -> crate::Result<JavaVersion> {
|
||||
// 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<JavaVersion> {
|
||||
// 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<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;
|
||||
let version = split.next().unwrap();
|
||||
let version = version.split_once('-').map_or(version, |(x, _)| x);
|
||||
let mut version = version.parse::<u32>()?;
|
||||
if version == 1 {
|
||||
version = split.next().map_or(Ok(1), |x| x.parse::<u32>())?;
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user