OneLauncher-Cracked/packages/oneclient_java/src/checker.rs
2026-09-06 01:38:32 +02:00

184 lines
5.5 KiB
Rust

use std::{collections::HashMap, path::PathBuf};
use async_tempfile::TempDir;
use tokio::sync::OnceCell;
use crate::error::{JavaError, JavaResult};
const JAVA_INFO_CLASS: &[u8] = include_bytes!("../assets/java/JavaInfo.class");
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JavaCheckInfo {
pub version: String,
pub vendor: String,
pub os_arch: String,
/// Mod loaders and game tooling expect a JDK so the locator prefers one
/// when both satisfy the requested major
pub is_jdk: bool,
}
#[tracing::instrument(level = "debug", skip(absolute_path))]
pub async fn check_java_runtime(absolute_path: String) -> JavaResult<JavaCheckInfo> {
let temp_dir = get_java_info_dir().await?;
let mut command = tokio::process::Command::new(&absolute_path);
command
.arg("-cp")
.arg(temp_dir)
.arg("JavaInfo")
.env_remove("_JAVA_OPTIONS")
.env_remove("JAVA_TOOL_OPTIONS")
.env_remove("JDK_JAVA_OPTIONS");
oneclient_common::process::no_window(command.as_std_mut());
let program = command.as_std().get_program().to_string_lossy();
let args: Vec<String> = command
.as_std()
.get_args()
.map(|arg| arg.to_string_lossy().into_owned())
.collect();
tracing::debug!("running command: {} {}", program, args.join(" "));
let output = command.output().await
.map_err(|e| JavaError::RuntimeCheckError {
source: e,
path: absolute_path.clone(),
})?;
let java_info = String::from_utf8_lossy(&output.stdout);
let info = java_info
.lines()
.filter_map(|line| {
let (key, value) = line.split_once('=')?;
Some((key.to_string(), value.to_string()))
})
.collect::<HashMap<_, _>>();
let Some(version) = info.get("java.version").cloned() else {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::warn!(
path = %absolute_path,
status = ?output.status.code(),
"java probe did not report java.version; stderr: {}",
stderr.trim()
);
return Err(JavaError::InvalidJavaPath { path: absolute_path });
};
// Minecraft needs AWT a headless image launches fine and then dies mid-game
if !has_usable_awt(&info) {
tracing::warn!(
path = %absolute_path,
classes = probe_flag(&info, "java.awt"),
natives = probe_flag(&info, "java.awt.natives"),
links = probe_flag(&info, "java.awt.link"),
"java installation has no usable java.awt support"
);
return Err(JavaError::MissingAwtSupport { path: absolute_path });
}
Ok(JavaCheckInfo {
os_arch: info
.get("os.arch")
.cloned()
.unwrap_or_else(|| String::from("unknown")),
version,
vendor: info
.get("java.vendor")
.cloned()
.unwrap_or_else(|| String::from("unknown")),
is_jdk: probe_flag(&info, "java.jdk"),
})
}
fn has_usable_awt(info: &HashMap<String, String>) -> bool {
if !probe_flag(info, "java.awt.link") {
tracing::warn!("java.awt.link returned false. ignoring")
}
probe_flag(info, "java.awt")
&& probe_flag(info, "java.awt.natives")
}
fn probe_flag(info: &HashMap<String, String>, key: &str) -> bool {
info.get(key)
.is_some_and(|value| value.eq_ignore_ascii_case("true"))
}
static TEMP_JAVA_INFO: OnceCell<TempDir> = OnceCell::const_new();
#[tracing::instrument(level = "debug")]
async fn get_java_info_dir() -> Result<&'static PathBuf, polyio::IOError> {
let dir: Result<&TempDir, polyio::IOError> = TEMP_JAVA_INFO
.get_or_try_init(async || {
let dir = polyio::tempdir().await?;
let file = dir.dir_path().join("JavaInfo.class");
polyio::write(&file, JAVA_INFO_CLASS).await?;
Ok(dir)
})
.await;
Ok(dir?.dir_path())
}
#[cfg(test)]
mod tests {
use super::*;
fn info(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
.collect()
}
#[test]
fn a_flag_the_probe_never_printed_is_false() {
assert!(!probe_flag(&info(&[]), "java.awt"));
}
#[test]
fn a_flag_is_read_regardless_of_case() {
assert!(probe_flag(&info(&[("java.awt", "TRUE")]), "java.awt"));
assert!(!probe_flag(&info(&[("java.awt", "false")]), "java.awt"));
}
fn awt(classes: &str, natives: &str, link: &str) -> HashMap<String, String> {
info(&[
("java.awt", classes),
("java.awt.natives", natives),
("java.awt.link", link),
])
}
#[test]
fn a_complete_image_is_usable() {
assert!(has_usable_awt(&awt("true", "true", "true")));
}
#[test]
fn awt_classes_without_their_natives_are_not_usable() {
assert!(!has_usable_awt(&awt("true", "false", "true")));
}
#[test]
fn a_link_probe_that_fails_does_not_rule_the_image_out() {
assert!(has_usable_awt(&awt("true", "true", "false")));
assert!(has_usable_awt(&info(&[
("java.awt", "true"),
("java.awt.natives", "true"),
])));
}
#[test]
fn every_half_is_required() {
assert!(!has_usable_awt(&info(&[("java.awt", "true")])));
assert!(!has_usable_awt(&info(&[("java.awt.natives", "true")])));
assert!(!has_usable_awt(&info(&[("java.awt.link", "true")])));
}
}