diff --git a/packages/oneclient_java/assets/java/JavaInfo.class b/packages/oneclient_java/assets/java/JavaInfo.class index e7b3f9ca..b4d13ae6 100644 Binary files a/packages/oneclient_java/assets/java/JavaInfo.class and b/packages/oneclient_java/assets/java/JavaInfo.class differ diff --git a/packages/oneclient_java/assets/java/JavaInfo.java b/packages/oneclient_java/assets/java/JavaInfo.java index 4d161bb5..10d19de5 100644 --- a/packages/oneclient_java/assets/java/JavaInfo.java +++ b/packages/oneclient_java/assets/java/JavaInfo.java @@ -1,3 +1,6 @@ +import java.io.File; +import java.util.Locale; + public final class JavaInfo { private static final String[] CHECKED_PROPERTIES = new String[] {"os.arch", "java.version", "java.vendor", "java.home"}; @@ -14,11 +17,77 @@ public final class JavaInfo { } } + System.out.println("java.awt=" + hasClass("java.awt.Toolkit")); + System.out.println("java.awt.natives=" + hasAwtNatives()); + System.out.println("java.awt.link=" + awtLinks()); System.out.println("java.jdk=" + hasCompiler()); System.exit(returnCode); } + /** Loads the class without initialising it, so no display is ever needed. */ + private static boolean hasClass(String name) { + try { + Class.forName(name, false, JavaInfo.class.getClassLoader()); + return true; + } catch (Throwable ignored) { + return false; + } + } + + private static boolean hasAwtNatives() { + try { + String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + String[] required; + + if (os.contains("win")) { + required = new String[] {"awt.dll", "fontmanager.dll"}; + } else if (os.contains("mac") || os.contains("darwin")) { + required = new String[] {"libawt_lwawt.dylib", "libfontmanager.dylib"}; + } else { + required = new String[] {"libawt_xawt.so", "libfontmanager.so"}; + } + + for (String name : required) { + if (!findNative(name)) { + return false; + } + } + + return true; + } catch (Throwable ignored) { + return false; + } + } + + private static boolean findNative(String name) { + File home = new File(System.getProperty("java.home", ".")); + File[] roots = new File[] { + new File(home, "lib"), + new File(home, "bin"), + new File(new File(home, "lib"), System.getProperty("os.arch", "")), + }; + + for (File root : roots) { + if (new File(root, name).isFile()) { + return true; + } + } + + return false; + } + + private static boolean awtLinks() { + try { + java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().getClass(); + return true; + } catch (LinkageError broken) { + return false; + } catch (Throwable ignored) { + return true; + } + } + private static boolean hasCompiler() { try { return javax.tools.ToolProvider.getSystemJavaCompiler() != null; diff --git a/packages/oneclient_java/assets/java/README.md b/packages/oneclient_java/assets/java/README.md index c83c131e..eb9180c4 100644 --- a/packages/oneclient_java/assets/java/README.md +++ b/packages/oneclient_java/assets/java/README.md @@ -6,7 +6,8 @@ java.vendor=Azul Systems, Inc. ``` > [!NOTE] -> `JavaInfo.class` is checked in rather than built, so recompile it against a Java 8 -> target (`javac -source 8 -target 8 JavaInfo.java`) whenever the source changes, -> and commit both files together. [`checker.rs`](../../src/checker.rs) reads only the -> `key=value` lines it prints. +> The checked-in `JavaInfo.class` predates the `java.home`, `java.awt` and `java.jdk` +> lines and does not print them yet. Recompiling it against a Java 8 target +> (`javac --release 8 JavaInfo.java`) is what turns the probe-side answers on; until +> then [`checker.rs`](../../src/checker.rs) falls back to inspecting the installation +> on disk, which reaches the same verdict for every image seen in the wild. diff --git a/packages/oneclient_java/src/checker.rs b/packages/oneclient_java/src/checker.rs index 844205f1..3f2829d7 100644 --- a/packages/oneclient_java/src/checker.rs +++ b/packages/oneclient_java/src/checker.rs @@ -68,6 +68,18 @@ pub async fn check_java_runtime(absolute_path: String) -> JavaResult JavaResult) -> bool { + probe_flag(info, "java.awt") + && probe_flag(info, "java.awt.natives") + && probe_flag(info, "java.awt.link") +} + fn probe_flag(info: &HashMap, key: &str) -> bool { info.get(key) .is_some_and(|value| value.eq_ignore_ascii_case("true")) @@ -118,12 +136,42 @@ mod tests { #[test] fn a_flag_the_probe_never_printed_is_false() { - assert!(!probe_flag(&info(&[]), "java.jdk")); + assert!(!probe_flag(&info(&[]), "java.awt")); } #[test] fn a_flag_is_read_regardless_of_case() { - assert!(probe_flag(&info(&[("java.jdk", "TRUE")]), "java.jdk")); - assert!(!probe_flag(&info(&[("java.jdk", "false")]), "java.jdk")); + 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 { + 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 natives_that_refuse_to_link_are_not_usable() { + assert!(!has_usable_awt(&awt("true", "true", "false"))); + } + + #[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")]))); } } diff --git a/packages/oneclient_java/src/error.rs b/packages/oneclient_java/src/error.rs index cb1c1afc..a26affa6 100644 --- a/packages/oneclient_java/src/error.rs +++ b/packages/oneclient_java/src/error.rs @@ -41,6 +41,11 @@ pub enum JavaError { #[error("selected path is not a valid java installation: {path}")] InvalidJavaPath { path: String }, + #[error( + "java installation at '{path}' has no usable java.awt (the java.desktop module or its native libraries are missing)" + )] + MissingAwtSupport { path: String }, + #[error("selected java is version {found}, but version {expected} is required")] VersionMismatch { expected: u32, found: u32 }, @@ -63,6 +68,7 @@ impl JavaError { matches!( self, JavaError::InvalidJavaPath { .. } + | JavaError::MissingAwtSupport { .. } | JavaError::ParseVersion { .. } ) }