Skip to content

Commit f10e0f2

Browse files
authored
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
1 parent 569d60c commit f10e0f2

File tree

17 files changed

+343
-158
lines changed

17 files changed

+343
-158
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ flate2 = "1.1.2"
6060
fs4 = { version = "0.13.1", default-features = false }
6161
futures = { version = "0.3.31", default-features = false }
6262
futures-util = "0.3.31"
63+
hashlink = "0.10.0"
6364
hex = "0.4.3"
6465
hickory-resolver = "0.25.2"
6566
hmac = "0.12.1"

apps/app-frontend/src/components/ui/JavaSelector.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ async function handleJavaFileInput() {
127127
const filePath = await open()
128128
129129
if (filePath) {
130-
let result = await get_jre(filePath.path ?? filePath)
130+
let result = await get_jre(filePath.path ?? filePath).catch(handleError)
131131
if (!result) {
132132
result = {
133133
path: filePath.path ?? filePath,

apps/app/src/api/jre.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ pub async fn jre_find_filtered_jres(
4141
// Validates JRE at a given path
4242
// Returns None if the path is not a valid JRE
4343
#[tauri::command]
44-
pub async fn jre_get_jre(path: PathBuf) -> Result<Option<JavaVersion>> {
45-
jre::check_jre(path).await.map_err(|e| e.into())
44+
pub async fn jre_get_jre(path: PathBuf) -> Result<JavaVersion> {
45+
Ok(jre::check_jre(path).await?)
4646
}
4747

4848
// Tests JRE of a certain version

apps/app/tauri.conf.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
"providerShortName": null,
3131
"signingIdentity": null
3232
},
33-
"resources": [],
3433
"shortDescription": "",
3534
"linux": {
3635
"deb": {

packages/app-lib/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ quick-xml = { workspace = true, features = ["async-tokio"] }
2323
enumset.workspace = true
2424
chardetng.workspace = true
2525
encoding_rs.workspace = true
26+
hashlink.workspace = true
2627

2728
chrono = { workspace = true, features = ["serde"] }
2829
daedalus.workspace = true
Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
public final class JavaInfo {
2-
private static final String[] CHECKED_PROPERTIES = new String[] {
3-
"os.arch",
4-
"java.version"
5-
};
2+
private static final String[] CHECKED_PROPERTIES = new String[] {
3+
"os.arch",
4+
"java.version"
5+
};
66

7-
public static void main(String[] args) {
8-
int returnCode = 0;
7+
public static void main(String[] args) {
8+
int returnCode = 0;
99

10-
for (String key : CHECKED_PROPERTIES) {
11-
String property = System.getProperty(key);
10+
for (String key : CHECKED_PROPERTIES) {
11+
String property = System.getProperty(key);
1212

13-
if (property != null) {
14-
System.out.println(key + "=" + property);
15-
} else {
16-
returnCode = 1;
17-
}
18-
}
19-
20-
System.exit(returnCode);
13+
if (property != null) {
14+
System.out.println(key + "=" + property);
15+
} else {
16+
returnCode = 1;
17+
}
2118
}
22-
}
19+
20+
System.exit(returnCode);
21+
}
22+
}
4.09 KB
Binary file not shown.
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import java.io.ByteArrayOutputStream;
2+
import java.io.IOException;
3+
import java.lang.reflect.Method;
4+
import java.lang.reflect.Modifier;
5+
import java.util.Arrays;
6+
7+
public final class MinecraftLaunch {
8+
public static void main(String[] args) throws IOException, ReflectiveOperationException {
9+
final String mainClass = args[0];
10+
final String[] gameArgs = Arrays.copyOfRange(args, 1, args.length);
11+
12+
System.setProperty("modrinth.process.args", String.join("\u001f", gameArgs));
13+
parseInput();
14+
15+
relaunch(mainClass, gameArgs);
16+
}
17+
18+
private static void parseInput() throws IOException {
19+
final ByteArrayOutputStream line = new ByteArrayOutputStream();
20+
while (true) {
21+
final int b = System.in.read();
22+
if (b < 0) {
23+
throw new IllegalStateException("Stdin terminated while parsing");
24+
}
25+
if (b != '\n') {
26+
line.write(b);
27+
continue;
28+
}
29+
if (handleLine(line.toString("UTF-8"))) {
30+
break;
31+
}
32+
line.reset();
33+
}
34+
}
35+
36+
private static boolean handleLine(String line) {
37+
final String[] parts = line.split("\t", 2);
38+
switch (parts[0]) {
39+
case "property": {
40+
final String[] keyValue = parts[1].split("\t", 2);
41+
System.setProperty(keyValue[0], keyValue[1]);
42+
return false;
43+
}
44+
case "launch":
45+
return true;
46+
}
47+
48+
System.err.println("Unknown input line " + line);
49+
return false;
50+
}
51+
52+
private static void relaunch(String mainClassName, String[] args) throws ReflectiveOperationException {
53+
final int javaVersion = getJavaVersion();
54+
final Class<?> mainClass = Class.forName(mainClassName);
55+
56+
if (javaVersion >= 25) {
57+
Method mainMethod;
58+
try {
59+
mainMethod = findMainMethodJep512(mainClass);
60+
} catch (ReflectiveOperationException e) {
61+
System.err
62+
.println("[MODRINTH] Unable to call JDK findMainMethod. Falling back to pre-Java 25 main method finding.");
63+
// If the above fails due to JDK implementation details changing
64+
try {
65+
mainMethod = findSimpleMainMethod(mainClass);
66+
} catch (ReflectiveOperationException innerE) {
67+
e.addSuppressed(innerE);
68+
throw e;
69+
}
70+
}
71+
if (mainMethod == null) {
72+
throw new IllegalArgumentException("Could not find main() method");
73+
}
74+
75+
Object thisObject = null;
76+
if (!Modifier.isStatic(mainMethod.getModifiers())) {
77+
thisObject = mainClass.getDeclaredConstructor().newInstance();
78+
}
79+
80+
final Object[] parameters = mainMethod.getParameterCount() > 0
81+
? new Object[] { args }
82+
: new Object[] {};
83+
84+
mainMethod.invoke(thisObject, parameters);
85+
} else {
86+
findSimpleMainMethod(mainClass).invoke(null, new Object[] { args });
87+
}
88+
}
89+
90+
private static int getJavaVersion() {
91+
String javaVersion = System.getProperty("java.version");
92+
93+
final int dotIndex = javaVersion.indexOf('.');
94+
if (dotIndex != -1) {
95+
javaVersion = javaVersion.substring(0, dotIndex);
96+
}
97+
98+
final int dashIndex = javaVersion.indexOf('-');
99+
if (dashIndex != -1) {
100+
javaVersion = javaVersion.substring(0, dashIndex);
101+
}
102+
103+
return Integer.parseInt(javaVersion);
104+
}
105+
106+
private static Method findMainMethodJep512(Class<?> mainClass) throws ReflectiveOperationException {
107+
// BEWARE BELOW: This code may break if JDK internals to find the main method
108+
// change.
109+
final Class<?> methodFinderClass = Class.forName("jdk.internal.misc.MethodFinder");
110+
final Method methodFinderMethod = methodFinderClass.getDeclaredMethod("findMainMethod", Class.class);
111+
final Object result = methodFinderMethod.invoke(null, mainClass);
112+
return (Method) result;
113+
}
114+
115+
private static Method findSimpleMainMethod(Class<?> mainClass) throws NoSuchMethodException {
116+
return mainClass.getMethod("main", String[].class);
117+
}
118+
}

packages/app-lib/src/api/jre.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::path::PathBuf;
99
use sysinfo::{MemoryRefreshKind, RefreshKind};
1010

1111
use crate::util::io;
12-
use crate::util::jre::extract_java_majorminor_version;
12+
use crate::util::jre::extract_java_version;
1313
use crate::{
1414
LoadingBarType, State,
1515
util::jre::{self},
@@ -38,9 +38,9 @@ pub async fn find_filtered_jres(
3838
Ok(if let Some(java_version) = java_version {
3939
jres.into_iter()
4040
.filter(|jre| {
41-
let jre_version = extract_java_majorminor_version(&jre.version);
41+
let jre_version = extract_java_version(&jre.version);
4242
if let Ok(jre_version) = jre_version {
43-
jre_version.1 == java_version
43+
jre_version == java_version
4444
} else {
4545
false
4646
}
@@ -157,20 +157,20 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
157157
}
158158

159159
// Validates JRE at a given at a given path
160-
pub async fn check_jre(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
161-
Ok(jre::check_java_at_filepath(&path).await)
160+
pub async fn check_jre(path: PathBuf) -> crate::Result<JavaVersion> {
161+
jre::check_java_at_filepath(&path).await
162162
}
163163

164164
// Test JRE at a given path
165165
pub async fn test_jre(
166166
path: PathBuf,
167167
major_version: u32,
168168
) -> crate::Result<bool> {
169-
let Some(jre) = jre::check_java_at_filepath(&path).await else {
169+
let Ok(jre) = jre::check_java_at_filepath(&path).await else {
170170
return Ok(false);
171171
};
172-
let (major, _) = extract_java_majorminor_version(&jre.version)?;
173-
Ok(major == major_version)
172+
let version = extract_java_version(&jre.version)?;
173+
Ok(version == major_version)
174174
}
175175

176176
// Gets maximum memory in KiB.

packages/app-lib/src/launcher/args.rs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use daedalus::{
1313
modded::SidedDataEntry,
1414
};
1515
use dunce::canonicalize;
16-
use std::collections::HashSet;
16+
use hashlink::LinkedHashSet;
1717
use std::io::{BufRead, BufReader};
1818
use std::{collections::HashMap, path::Path};
1919
use uuid::Uuid;
@@ -24,7 +24,7 @@ const TEMPORARY_REPLACE_CHAR: &str = "\n";
2424
pub fn get_class_paths(
2525
libraries_path: &Path,
2626
libraries: &[Library],
27-
client_path: &Path,
27+
launcher_class_path: &[&Path],
2828
java_arch: &str,
2929
minecraft_updated: bool,
3030
) -> crate::Result<String> {
@@ -48,20 +48,22 @@ pub fn get_class_paths(
4848

4949
Some(get_lib_path(libraries_path, &library.name, false))
5050
})
51-
.collect::<Result<HashSet<_>, _>>()?;
52-
53-
cps.insert(
54-
canonicalize(client_path)
55-
.map_err(|_| {
56-
crate::ErrorKind::LauncherError(format!(
57-
"Specified class path {} does not exist",
58-
client_path.to_string_lossy()
59-
))
60-
.as_error()
61-
})?
62-
.to_string_lossy()
63-
.to_string(),
64-
);
51+
.collect::<Result<LinkedHashSet<_>, _>>()?;
52+
53+
for launcher_path in launcher_class_path {
54+
cps.insert(
55+
canonicalize(launcher_path)
56+
.map_err(|_| {
57+
crate::ErrorKind::LauncherError(format!(
58+
"Specified class path {} does not exist",
59+
launcher_path.to_string_lossy()
60+
))
61+
.as_error()
62+
})?
63+
.to_string_lossy()
64+
.to_string(),
65+
);
66+
}
6567

6668
Ok(cps
6769
.into_iter()

0 commit comments

Comments
 (0)